@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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/assets.d.ts +53 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/dom/animationFrames.d.ts +12 -0
- package/dist/dom/animationFrames.d.ts.map +1 -0
- package/dist/dom/canvas.d.ts +26 -0
- package/dist/dom/canvas.d.ts.map +1 -0
- package/dist/dom/latch.d.ts +15 -0
- package/dist/dom/latch.d.ts.map +1 -0
- package/dist/dom/models.d.ts +10 -0
- package/dist/dom/models.d.ts.map +1 -0
- package/dist/dom/overlays.d.ts +24 -0
- package/dist/dom/overlays.d.ts.map +1 -0
- package/dist/dom/pointer.d.ts +49 -0
- package/dist/dom/pointer.d.ts.map +1 -0
- package/dist/dom/postprocessing.d.ts +22 -0
- package/dist/dom/postprocessing.d.ts.map +1 -0
- package/dist/dom/property.d.ts +15 -0
- package/dist/dom/property.d.ts.map +1 -0
- package/dist/dom/reconciled.d.ts +10 -0
- package/dist/dom/reconciled.d.ts.map +1 -0
- package/dist/dom/renderSettings.d.ts +58 -0
- package/dist/dom/renderSettings.d.ts.map +1 -0
- package/dist/dom/typedScenes.d.ts +14 -0
- package/dist/dom/typedScenes.d.ts.map +1 -0
- package/dist/dom/updates.d.ts +8 -0
- package/dist/dom/updates.d.ts.map +1 -0
- package/dist/dom/viewport.d.ts +40 -0
- package/dist/dom/viewport.d.ts.map +1 -0
- package/dist/dom/webgl.d.ts +12 -0
- package/dist/dom/webgl.d.ts.map +1 -0
- package/dist/dom/webgpu.d.ts +24 -0
- package/dist/dom/webgpu.d.ts.map +1 -0
- package/dist/dom/webgpuPostprocessing.d.ts +4 -0
- package/dist/dom/webgpuPostprocessing.d.ts.map +1 -0
- package/dist/dom/xr.d.ts +53 -0
- package/dist/dom/xr.d.ts.map +1 -0
- package/dist/dom/xrGlide.d.ts +49 -0
- package/dist/dom/xrGlide.d.ts.map +1 -0
- package/dist/dom/xrHub.d.ts +45 -0
- package/dist/dom/xrHub.d.ts.map +1 -0
- package/dist/dom/xrInputs.d.ts +40 -0
- package/dist/dom/xrInputs.d.ts.map +1 -0
- package/dist/dom/xrSpaces.d.ts +33 -0
- package/dist/dom/xrSpaces.d.ts.map +1 -0
- package/dist/dom/xrTarget.d.ts +36 -0
- package/dist/dom/xrTarget.d.ts.map +1 -0
- package/dist/element.d.ts +771 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/environment.d.ts +64 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2041 -0
- package/dist/index.js.map +38 -0
- package/dist/mount.d.ts +14 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/overlayView.d.ts +23 -0
- package/dist/overlayView.d.ts.map +1 -0
- package/dist/view.d.ts +179 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/xr.d.ts +34 -0
- package/dist/xr.d.ts.map +1 -0
- package/package.json +72 -4
- package/src/assets.ts +151 -0
- package/src/dom/animationFrames.ts +32 -0
- package/src/dom/canvas.ts +54 -0
- package/src/dom/latch.ts +36 -0
- package/src/dom/models.ts +37 -0
- package/src/dom/overlays.ts +63 -0
- package/src/dom/pointer.ts +163 -0
- package/src/dom/postprocessing.ts +116 -0
- package/src/dom/property.ts +43 -0
- package/src/dom/reconciled.ts +51 -0
- package/src/dom/renderSettings.ts +117 -0
- package/src/dom/typedScenes.ts +21 -0
- package/src/dom/updates.ts +14 -0
- package/src/dom/viewport.ts +97 -0
- package/src/dom/webgl.ts +216 -0
- package/src/dom/webgpu.ts +205 -0
- package/src/dom/webgpuPostprocessing.ts +102 -0
- package/src/dom/xr.ts +211 -0
- package/src/dom/xrGlide.ts +114 -0
- package/src/dom/xrHub.ts +118 -0
- package/src/dom/xrInputs.ts +309 -0
- package/src/dom/xrSpaces.ts +287 -0
- package/src/dom/xrTarget.ts +71 -0
- package/src/element.ts +114 -0
- package/src/environment.ts +78 -0
- package/src/index.ts +71 -0
- package/src/mount.ts +305 -0
- package/src/overlayView.ts +71 -0
- package/src/view.ts +254 -0
- package/src/xr.ts +36 -0
package/src/element.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<effect-three-scene>` element: a typed `scene` property that Foldkit diffs on
|
|
3
|
+
* every render, and Schema-decoded events that become the app's typed Messages.
|
|
4
|
+
*
|
|
5
|
+
* The tag is never registered with `customElements`; the scene Mount intercepts the
|
|
6
|
+
* property instead (ADR-002), so there is nothing to re-register under HMR or SSR.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { SceneSchema, type SceneEvent } from '@opsydyn/effect-three';
|
|
11
|
+
import { Schema } from 'effect';
|
|
12
|
+
import { CustomElement } from 'foldkit';
|
|
13
|
+
import { defineMessageUnion } from 'foldkit/message';
|
|
14
|
+
|
|
15
|
+
/** A scene update that could not be applied. The scene keeps its last good state. */
|
|
16
|
+
export const SceneError = Schema.Union([
|
|
17
|
+
...SceneSchema.DescriptionError.members,
|
|
18
|
+
Schema.TaggedStruct('MalformedScene', { issue: Schema.String }),
|
|
19
|
+
]);
|
|
20
|
+
export type SceneError = typeof SceneError.Type;
|
|
21
|
+
|
|
22
|
+
const Counts = Schema.Struct({ live: Schema.Int, created: Schema.Int, disposed: Schema.Int });
|
|
23
|
+
|
|
24
|
+
/** What one scene update did, and the GPU resources held afterwards. */
|
|
25
|
+
export const ReconciledDetail = Schema.Struct({
|
|
26
|
+
ops: Schema.Struct({
|
|
27
|
+
created: Schema.Int,
|
|
28
|
+
updated: Schema.Int,
|
|
29
|
+
moved: Schema.Int,
|
|
30
|
+
removed: Schema.Int,
|
|
31
|
+
replaced: Schema.Int,
|
|
32
|
+
reordered: Schema.Int,
|
|
33
|
+
configured: Schema.Int,
|
|
34
|
+
}),
|
|
35
|
+
resources: Schema.Struct({ geometries: Counts, materials: Counts }),
|
|
36
|
+
});
|
|
37
|
+
export type ReconciledDetail = typeof ReconciledDetail.Type;
|
|
38
|
+
|
|
39
|
+
/** An immersive session began presenting the scene in `mode`, or ended (`null`). */
|
|
40
|
+
export const XrSessionDetail = Schema.Struct({
|
|
41
|
+
mode: Schema.NullOr(Schema.Literals(['immersive-vr', 'immersive-ar'])),
|
|
42
|
+
});
|
|
43
|
+
export type XrSessionDetail = typeof XrSessionDetail.Type;
|
|
44
|
+
|
|
45
|
+
export const sceneElement = CustomElement.define({
|
|
46
|
+
tag: 'effect-three-scene',
|
|
47
|
+
properties: { scene: SceneSchema.SceneDescription },
|
|
48
|
+
events: {
|
|
49
|
+
'scene-click': SceneSchema.ButtonDetail,
|
|
50
|
+
'scene-double-click': SceneSchema.ButtonDetail,
|
|
51
|
+
'scene-context-menu': SceneSchema.HitDetail,
|
|
52
|
+
'scene-pointer-down': SceneSchema.ButtonDetail,
|
|
53
|
+
'scene-pointer-up': SceneSchema.ButtonDetail,
|
|
54
|
+
'scene-pointer-enter': SceneSchema.HitDetail,
|
|
55
|
+
'scene-pointer-move': SceneSchema.HitDetail,
|
|
56
|
+
'scene-pointer-leave': SceneSchema.KeyDetail,
|
|
57
|
+
'scene-pointer-missed': SceneSchema.MissDetail,
|
|
58
|
+
'scene-wheel': SceneSchema.WheelDetail,
|
|
59
|
+
'scene-drag': SceneSchema.DragDetail,
|
|
60
|
+
'scene-drag-end': SceneSchema.DragDetail,
|
|
61
|
+
'scene-gesture': SceneSchema.GestureDetail,
|
|
62
|
+
'scene-auto-rotated': SceneSchema.AutoRotatedDetail,
|
|
63
|
+
'scene-collision': SceneSchema.CollisionDetail,
|
|
64
|
+
'scene-contact-force': SceneSchema.ContactForceDetail,
|
|
65
|
+
'scene-measured': SceneSchema.MeasuredDetail,
|
|
66
|
+
'scene-label-measured': SceneSchema.LabelMeasuredDetail,
|
|
67
|
+
'scene-xr': XrSessionDetail,
|
|
68
|
+
'scene-xr-stick': SceneSchema.XrStickDetail,
|
|
69
|
+
'scene-xr-button': SceneSchema.XrButtonDetail,
|
|
70
|
+
'scene-xr-surface': SceneSchema.XrSurfaceDetail,
|
|
71
|
+
'scene-xr-moved': SceneSchema.XrMovedDetail,
|
|
72
|
+
'scene-xr-planes': SceneSchema.XrPlanesDetail,
|
|
73
|
+
'scene-xr-meshes': SceneSchema.XrMeshesDetail,
|
|
74
|
+
'scene-performance': SceneSchema.PerformanceDetail,
|
|
75
|
+
'scene-error': Schema.Struct({ error: SceneError }),
|
|
76
|
+
'scene-reconciled': ReconciledDetail,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** The DOM event name for each kernel scene event. */
|
|
81
|
+
export const domEventName = {
|
|
82
|
+
Click: 'scene-click',
|
|
83
|
+
DoubleClick: 'scene-double-click',
|
|
84
|
+
ContextMenu: 'scene-context-menu',
|
|
85
|
+
PointerDown: 'scene-pointer-down',
|
|
86
|
+
PointerUp: 'scene-pointer-up',
|
|
87
|
+
PointerEnter: 'scene-pointer-enter',
|
|
88
|
+
PointerMove: 'scene-pointer-move',
|
|
89
|
+
PointerLeave: 'scene-pointer-leave',
|
|
90
|
+
PointerMissed: 'scene-pointer-missed',
|
|
91
|
+
Wheel: 'scene-wheel',
|
|
92
|
+
Drag: 'scene-drag',
|
|
93
|
+
DragEnd: 'scene-drag-end',
|
|
94
|
+
Gesture: 'scene-gesture',
|
|
95
|
+
AutoRotated: 'scene-auto-rotated',
|
|
96
|
+
Collision: 'scene-collision',
|
|
97
|
+
ContactForce: 'scene-contact-force',
|
|
98
|
+
Measured: 'scene-measured',
|
|
99
|
+
LabelMeasured: 'scene-label-measured',
|
|
100
|
+
XrStick: 'scene-xr-stick',
|
|
101
|
+
XrButton: 'scene-xr-button',
|
|
102
|
+
XrSurface: 'scene-xr-surface',
|
|
103
|
+
XrMoved: 'scene-xr-moved',
|
|
104
|
+
XrPlanes: 'scene-xr-planes',
|
|
105
|
+
XrMeshes: 'scene-xr-meshes',
|
|
106
|
+
PerformanceChanged: 'scene-performance',
|
|
107
|
+
} as const satisfies Record<SceneEvent['_tag'], keyof typeof sceneElement.events>;
|
|
108
|
+
|
|
109
|
+
/** Outcome of mounting a scene. Map these into the app's Messages with `onLifecycle`. */
|
|
110
|
+
export const SceneLifecycle = defineMessageUnion({
|
|
111
|
+
StartedScene: {},
|
|
112
|
+
FailedScene: { reason: Schema.String },
|
|
113
|
+
});
|
|
114
|
+
export type SceneLifecycle = typeof SceneLifecycle.Type;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-wide state shared by every scene element: one GPU resource store and one
|
|
3
|
+
* animation-frame scheduler. Create it once at the application entry point.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
type Behaviour,
|
|
9
|
+
createResourceStore,
|
|
10
|
+
type LabelEngine,
|
|
11
|
+
type PhysicsEngine,
|
|
12
|
+
type RendererFactory,
|
|
13
|
+
type ResourceStore,
|
|
14
|
+
} from '@opsydyn/effect-three';
|
|
15
|
+
|
|
16
|
+
import { makeAssets } from './assets.ts';
|
|
17
|
+
import { animationFrameScheduler, type AnimationFrameHost } from './dom/animationFrames.ts';
|
|
18
|
+
import { defaultPixelRatio, type PixelRatio } from './dom/viewport.ts';
|
|
19
|
+
import { webglRendererFactory } from './dom/webgl.ts';
|
|
20
|
+
import { createXrHub } from './dom/xrHub.ts';
|
|
21
|
+
import { makeXr } from './xr.ts';
|
|
22
|
+
|
|
23
|
+
/** The browser APIs the adapter uses; normally `window`. */
|
|
24
|
+
export type EnvironmentHost = AnimationFrameHost &
|
|
25
|
+
Pick<Window, 'matchMedia' | 'devicePixelRatio'> &
|
|
26
|
+
Partial<Pick<Window, 'navigator'>>;
|
|
27
|
+
|
|
28
|
+
export type EnvironmentOptions = {
|
|
29
|
+
/** Creates each scene's renderer from its canvas. Defaults to WebGL2. */
|
|
30
|
+
readonly renderer?: (canvas: HTMLCanvasElement) => RendererFactory;
|
|
31
|
+
/** Drawing-buffer pixel ratio: fixed, or `[min, max]` around the device's. Default `[1, 2]`. */
|
|
32
|
+
readonly pixelRatio?: PixelRatio;
|
|
33
|
+
/** Behaviours scenes may name with `behave(name)`, beside the built-in `float`. */
|
|
34
|
+
readonly behaviours?: Readonly<Record<string, Behaviour>>;
|
|
35
|
+
/**
|
|
36
|
+
* The physics engine for scenes with `physics`, or the promise of one: pass
|
|
37
|
+
* `loadRapier()` from `@opsydyn/effect-three-rapier` as it is, and scenes start once it
|
|
38
|
+
* has loaded. Without one, bodies stay where they are described.
|
|
39
|
+
*/
|
|
40
|
+
readonly physics?: PhysicsEngine | Promise<PhysicsEngine>;
|
|
41
|
+
/**
|
|
42
|
+
* Draws `label` nodes: pass `troikaLabels()` from `@opsydyn/effect-three-troika`.
|
|
43
|
+
* Without one, labels are empty.
|
|
44
|
+
*/
|
|
45
|
+
readonly labels?: LabelEngine;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Creates the shared environment for `host`. Call `dispose` when the app shuts down. */
|
|
49
|
+
export const makeEnvironment = (host: EnvironmentHost, options: EnvironmentOptions = {}) => {
|
|
50
|
+
const store: ResourceStore = createResourceStore();
|
|
51
|
+
const assets = makeAssets(store);
|
|
52
|
+
const xrHub = createXrHub(() => host.navigator?.xr);
|
|
53
|
+
return {
|
|
54
|
+
host,
|
|
55
|
+
store,
|
|
56
|
+
frames: animationFrameScheduler(host),
|
|
57
|
+
renderer: options.renderer ?? ((canvas: HTMLCanvasElement) => webglRendererFactory(canvas)),
|
|
58
|
+
pixelRatio: options.pixelRatio ?? defaultPixelRatio,
|
|
59
|
+
behaviours: options.behaviours ?? {},
|
|
60
|
+
/** The physics engine, once loaded; `undefined` when there is none. */
|
|
61
|
+
physics: Promise.resolve(options.physics),
|
|
62
|
+
/** The label engine; `undefined` when there is none. */
|
|
63
|
+
labels: options.labels,
|
|
64
|
+
/** Loads textures and glTF models by URL and lends them to the store. */
|
|
65
|
+
assets,
|
|
66
|
+
/** Enters and leaves immersive (WebXR) sessions, from Commands. */
|
|
67
|
+
xr: makeXr(xrHub),
|
|
68
|
+
/** The page's presentable scenes and its session; scene mounts register here. */
|
|
69
|
+
xrHub,
|
|
70
|
+
/** Disposes every loaded asset and every GPU resource still held. */
|
|
71
|
+
dispose: () => {
|
|
72
|
+
assets.dispose();
|
|
73
|
+
store.dispose();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type Environment = ReturnType<typeof makeEnvironment>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foldkit adapter for `@opsydyn/effect-three`.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // entry.ts: once per app
|
|
6
|
+
* const environment = EffectThreeFoldkit.makeEnvironment(window);
|
|
7
|
+
* export const sceneMount = EffectThreeFoldkit.makeSceneMount(environment);
|
|
8
|
+
*
|
|
9
|
+
* // view.ts
|
|
10
|
+
* EffectThreeFoldkit.view(
|
|
11
|
+
* { scene: sceneFor(model), mount: sceneMount, onLifecycle: GotSceneLifecycle, onClick: ClickedNode },
|
|
12
|
+
* h,
|
|
13
|
+
* );
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
export { AssetLoadError, type Assets, makeAssets, type ModelInfo } from './assets.ts';
|
|
19
|
+
export { animationFrameScheduler, type AnimationFrameHost } from './dom/animationFrames.ts';
|
|
20
|
+
export {
|
|
21
|
+
capturesPointer,
|
|
22
|
+
claimsContextMenu,
|
|
23
|
+
type Rect,
|
|
24
|
+
toNdc,
|
|
25
|
+
toPointerInput,
|
|
26
|
+
} from './dom/pointer.ts';
|
|
27
|
+
export { defaultPixelRatio, type PixelRatio, resolvePixelRatio } from './dom/viewport.ts';
|
|
28
|
+
export { webglRendererFactory, type WebGLOptions } from './dom/webgl.ts';
|
|
29
|
+
export { type WebGPUOptions, webgpuRendererFactory } from './dom/webgpu.ts';
|
|
30
|
+
export {
|
|
31
|
+
domEventName,
|
|
32
|
+
ReconciledDetail,
|
|
33
|
+
SceneError,
|
|
34
|
+
sceneElement,
|
|
35
|
+
SceneLifecycle,
|
|
36
|
+
XrSessionDetail,
|
|
37
|
+
} from './element.ts';
|
|
38
|
+
export { type XrMode } from './dom/xrHub.ts';
|
|
39
|
+
export { type XrControls, XrUnavailable } from './xr.ts';
|
|
40
|
+
export { summarise } from './dom/reconciled.ts';
|
|
41
|
+
export {
|
|
42
|
+
type Environment,
|
|
43
|
+
type EnvironmentHost,
|
|
44
|
+
type EnvironmentOptions,
|
|
45
|
+
makeEnvironment,
|
|
46
|
+
} from './environment.ts';
|
|
47
|
+
export { makeSceneMount, type SceneMount } from './mount.ts';
|
|
48
|
+
export { type OverlayView } from './overlayView.ts';
|
|
49
|
+
export {
|
|
50
|
+
type AutoRotatedDetail,
|
|
51
|
+
type ButtonDetail,
|
|
52
|
+
type CollisionDetail,
|
|
53
|
+
type ContactForceDetail,
|
|
54
|
+
type DragDetail,
|
|
55
|
+
type Gesture,
|
|
56
|
+
type HitDetail,
|
|
57
|
+
type KeyDetail,
|
|
58
|
+
type LabelMeasuredDetail,
|
|
59
|
+
type MeasuredDetail,
|
|
60
|
+
type MissDetail,
|
|
61
|
+
type PerformanceDetail,
|
|
62
|
+
view,
|
|
63
|
+
type ViewConfig,
|
|
64
|
+
type WheelDetail,
|
|
65
|
+
type XrButtonDetail,
|
|
66
|
+
type XrMeshesDetail,
|
|
67
|
+
type XrMovedDetail,
|
|
68
|
+
type XrPlanesDetail,
|
|
69
|
+
type XrStickDetail,
|
|
70
|
+
type XrSurfaceDetail,
|
|
71
|
+
} from './view.ts';
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene Mount (ADR-002): runs one scene for the lifetime of its element inside
|
|
3
|
+
* Foldkit's runtime. Everything acquired here is released, in reverse order, when the
|
|
4
|
+
* element unmounts.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
createGatedFrames,
|
|
10
|
+
FrameSource,
|
|
11
|
+
type GatedFrames,
|
|
12
|
+
type PhysicsEngine,
|
|
13
|
+
type PointerInput,
|
|
14
|
+
ResourceRegistry,
|
|
15
|
+
type SceneEvent,
|
|
16
|
+
type SceneDescription,
|
|
17
|
+
SceneRuntime,
|
|
18
|
+
SceneSchema,
|
|
19
|
+
} from '@opsydyn/effect-three';
|
|
20
|
+
import { type Cause, Data, Effect, Option, Queue, Schema, Stream } from 'effect';
|
|
21
|
+
import { Mount } from 'foldkit';
|
|
22
|
+
import type { ViewState } from 'foldkit/mount';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
canvasIn,
|
|
26
|
+
reconciledDispatch,
|
|
27
|
+
removal,
|
|
28
|
+
sceneErrorDispatch,
|
|
29
|
+
sceneEventDispatch,
|
|
30
|
+
} from './dom/canvas.ts';
|
|
31
|
+
import { applying, createLatch, type Latch, routedWhileOpen } from './dom/latch.ts';
|
|
32
|
+
import { containOverlays, createOverlayWriter, type OverlayWriter } from './dom/overlays.ts';
|
|
33
|
+
import { claimsGestures, listenForPointer, touchActionSync } from './dom/pointer.ts';
|
|
34
|
+
import { interceptProperty } from './dom/property.ts';
|
|
35
|
+
import { typedScene } from './dom/typedScenes.ts';
|
|
36
|
+
import { afterUpdate } from './dom/updates.ts';
|
|
37
|
+
import {
|
|
38
|
+
measureViewport,
|
|
39
|
+
observePixelRatio,
|
|
40
|
+
observeReducedMotion,
|
|
41
|
+
observeSize,
|
|
42
|
+
observeVisibility,
|
|
43
|
+
} from './dom/viewport.ts';
|
|
44
|
+
import { createFrameGate, type FrameGate, xrTarget } from './dom/xrTarget.ts';
|
|
45
|
+
import { SceneLifecycle } from './element.ts';
|
|
46
|
+
import type { Environment } from './environment.ts';
|
|
47
|
+
|
|
48
|
+
type SceneHandle = SceneRuntime.SceneHandle;
|
|
49
|
+
|
|
50
|
+
/** Everything one mounted scene needs, gathered so helpers take one argument. */
|
|
51
|
+
type Mounted = {
|
|
52
|
+
readonly environment: Environment;
|
|
53
|
+
readonly element: Element;
|
|
54
|
+
readonly canvas: HTMLCanvasElement;
|
|
55
|
+
readonly overlays: OverlayWriter;
|
|
56
|
+
readonly handle: SceneHandle;
|
|
57
|
+
readonly live: Latch;
|
|
58
|
+
readonly frames: GatedFrames;
|
|
59
|
+
readonly gate: FrameGate;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const decodeUnknownScene = Schema.decodeUnknownEffect(SceneSchema.SceneDescription);
|
|
63
|
+
|
|
64
|
+
/** Typed scenes from `view` skip validation; anything else is Schema-decoded. */
|
|
65
|
+
const decodeScene = (value: unknown) =>
|
|
66
|
+
Option.fromNullishOr(typedScene(value)).pipe(
|
|
67
|
+
Option.match({ onNone: () => decodeUnknownScene(value), onSome: Effect.succeed }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const isLive = Schema.is(Schema.Literal('Live'));
|
|
71
|
+
|
|
72
|
+
const acquireCanvas = (element: Element) => {
|
|
73
|
+
const create = Effect.sync(canvasIn(element));
|
|
74
|
+
return Effect.acquireRelease(create, (canvas) => Effect.sync(removal(canvas)));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const acquireSceneUpdates = () => {
|
|
78
|
+
const create = Queue.sliding<unknown>(1);
|
|
79
|
+
return Effect.acquireRelease(create, Queue.shutdown);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const interception = (element: Element, updates: Queue.Queue<unknown>) => () =>
|
|
83
|
+
interceptProperty(element, 'scene', (value) => Queue.offerUnsafe(updates, value));
|
|
84
|
+
|
|
85
|
+
const acquireInterception = (element: Element, updates: Queue.Queue<unknown>) => {
|
|
86
|
+
const intercept = Effect.sync(interception(element, updates));
|
|
87
|
+
return Effect.acquireRelease(intercept, (intercepted) => Effect.sync(intercepted.restore));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const pointerListening =
|
|
91
|
+
(
|
|
92
|
+
canvas: HTMLElement,
|
|
93
|
+
onInput: (input: PointerInput) => ReadonlyArray<SceneEvent>,
|
|
94
|
+
claimsGestures: () => boolean,
|
|
95
|
+
) =>
|
|
96
|
+
() =>
|
|
97
|
+
listenForPointer(canvas, onInput, claimsGestures);
|
|
98
|
+
|
|
99
|
+
const acquirePointer = (canvas: HTMLElement, mounted: Mounted) => {
|
|
100
|
+
const listen = Effect.sync(
|
|
101
|
+
pointerListening(
|
|
102
|
+
canvas,
|
|
103
|
+
routedWhileOpen(mounted.live, mounted.handle.pointer),
|
|
104
|
+
claimsGestures(mounted.handle.graph),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return Effect.acquireRelease(listen, Effect.sync);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Makes the scene presentable in the page's immersive sessions while it is mounted. */
|
|
111
|
+
const xrRegistration = (mounted: Mounted) => () =>
|
|
112
|
+
mounted.environment.xrHub.register(
|
|
113
|
+
xrTarget({
|
|
114
|
+
element: mounted.element,
|
|
115
|
+
canvas: mounted.canvas,
|
|
116
|
+
live: mounted.live,
|
|
117
|
+
gate: mounted.gate,
|
|
118
|
+
settings: mounted.handle.graph.xr,
|
|
119
|
+
draw: mounted.handle.drawNow,
|
|
120
|
+
scene: mounted.handle.graph.scene,
|
|
121
|
+
follow: mounted.handle.graph.follow,
|
|
122
|
+
lend: mounted.handle.graph.store.lendGeometry,
|
|
123
|
+
input: mounted.handle.xrInput,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const acquireXrTarget = (mounted: Mounted) => {
|
|
128
|
+
const register = Effect.sync(xrRegistration(mounted));
|
|
129
|
+
return Effect.acquireRelease(register, Effect.sync);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** A stream of values from a DOM observer that returns a disconnect function. */
|
|
133
|
+
const connectObserver =
|
|
134
|
+
<A>(observe: (emit: (value: A) => void) => () => void) =>
|
|
135
|
+
(queue: Queue.Queue<A, Cause.Done>) => {
|
|
136
|
+
const connect = Effect.sync(applying(observe, (value: A) => Queue.offerUnsafe(queue, value)));
|
|
137
|
+
return Effect.acquireRelease(connect, Effect.sync);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const observed = <A>(observe: (emit: (value: A) => void) => () => void) =>
|
|
141
|
+
Stream.callback<A>(connectObserver(observe));
|
|
142
|
+
|
|
143
|
+
const applyUpdate = (mounted: Mounted) => (value: unknown) => {
|
|
144
|
+
const reportError = sceneErrorDispatch(mounted.element);
|
|
145
|
+
const reportReconciled = afterUpdate(
|
|
146
|
+
reconciledDispatch(mounted.element, mounted.environment.store),
|
|
147
|
+
[touchActionSync(mounted.canvas, mounted.handle.graph), mounted.overlays.rewrite],
|
|
148
|
+
);
|
|
149
|
+
const decoded = decodeScene(value);
|
|
150
|
+
const applied = decoded.pipe(
|
|
151
|
+
Effect.flatMap(mounted.handle.update),
|
|
152
|
+
Effect.flatMap((ops) => Effect.sync(reportReconciled(ops))),
|
|
153
|
+
);
|
|
154
|
+
return applied.pipe(
|
|
155
|
+
Effect.catchTags({
|
|
156
|
+
SchemaError: (error) =>
|
|
157
|
+
Effect.sync(reportError({ _tag: 'MalformedScene', issue: error.message })),
|
|
158
|
+
InvalidScene: ({ error }) => Effect.sync(reportError(error)),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const resize = (mounted: Mounted) => () =>
|
|
164
|
+
mounted.handle.resize(
|
|
165
|
+
measureViewport(
|
|
166
|
+
mounted.element,
|
|
167
|
+
mounted.environment.host.devicePixelRatio,
|
|
168
|
+
mounted.environment.pixelRatio,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const watchers = (
|
|
173
|
+
mounted: Mounted,
|
|
174
|
+
updates: Queue.Queue<unknown>,
|
|
175
|
+
viewStateChanges: Stream.Stream<ViewState>,
|
|
176
|
+
) => {
|
|
177
|
+
const dispatchEvent = sceneEventDispatch(mounted.element, mounted.live);
|
|
178
|
+
const sizes = observed<void>((emit) => observeSize(mounted.element, emit));
|
|
179
|
+
const ratios = observed<void>((emit) => observePixelRatio(mounted.environment.host, emit));
|
|
180
|
+
const visibility = observed<boolean>((emit) => observeVisibility(mounted.element, emit));
|
|
181
|
+
const reducedMotion = observed<boolean>((emit) =>
|
|
182
|
+
observeReducedMotion(mounted.environment.host, emit),
|
|
183
|
+
);
|
|
184
|
+
return [
|
|
185
|
+
Stream.runForEach(Stream.fromQueue(updates), applyUpdate(mounted)),
|
|
186
|
+
Stream.runForEach(mounted.handle.events, (event) => Effect.sync(dispatchEvent(event))),
|
|
187
|
+
Stream.runForEach(viewStateChanges, (state) =>
|
|
188
|
+
Effect.sync(applying(mounted.live.set, isLive(state))),
|
|
189
|
+
),
|
|
190
|
+
Stream.runForEach(sizes, resize(mounted)),
|
|
191
|
+
Stream.runForEach(ratios, resize(mounted)),
|
|
192
|
+
Stream.runForEach(visibility, (visible) =>
|
|
193
|
+
Effect.sync(applying(mounted.gate.setVisible, visible)),
|
|
194
|
+
),
|
|
195
|
+
Stream.runForEach(reducedMotion, (reduced) => mounted.handle.setAnimating(!reduced)),
|
|
196
|
+
];
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** The physics engine could not be loaded (Rapier's WebAssembly, say). */
|
|
200
|
+
class PhysicsUnavailable extends Data.TaggedError('PhysicsUnavailable')<{
|
|
201
|
+
readonly cause: unknown;
|
|
202
|
+
}> {}
|
|
203
|
+
|
|
204
|
+
const sceneRuntimeFor =
|
|
205
|
+
(
|
|
206
|
+
environment: Environment,
|
|
207
|
+
element: Element,
|
|
208
|
+
canvas: HTMLCanvasElement,
|
|
209
|
+
overlays: OverlayWriter,
|
|
210
|
+
physics: PhysicsEngine | undefined,
|
|
211
|
+
) =>
|
|
212
|
+
(scene: SceneDescription) => {
|
|
213
|
+
const frames = createGatedFrames(environment.frames);
|
|
214
|
+
const started = SceneRuntime.make({
|
|
215
|
+
scene,
|
|
216
|
+
renderer: environment.renderer(canvas),
|
|
217
|
+
behaviours: environment.behaviours,
|
|
218
|
+
physics,
|
|
219
|
+
labels: environment.labels,
|
|
220
|
+
placeOverlays: overlays.place,
|
|
221
|
+
viewport: measureViewport(element, environment.host.devicePixelRatio, environment.pixelRatio),
|
|
222
|
+
}).pipe(
|
|
223
|
+
Effect.provideService(FrameSource.FrameSource, FrameSource.fromScheduler(frames)),
|
|
224
|
+
Effect.provideService(ResourceRegistry.ResourceRegistry, { store: environment.store }),
|
|
225
|
+
);
|
|
226
|
+
return Effect.map(started, (handle) => ({ handle, frames }));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function* runScene(
|
|
230
|
+
environment: Environment,
|
|
231
|
+
element: Element,
|
|
232
|
+
viewStateChanges: Stream.Stream<ViewState>,
|
|
233
|
+
) {
|
|
234
|
+
const canvas = yield* acquireCanvas(element);
|
|
235
|
+
const updates = yield* acquireSceneUpdates();
|
|
236
|
+
const intercepted = yield* acquireInterception(element, updates);
|
|
237
|
+
const initial = yield* decodeScene(intercepted.initial);
|
|
238
|
+
const overlays = createOverlayWriter(element);
|
|
239
|
+
yield* Effect.sync(containOverlays(element));
|
|
240
|
+
const physics = yield* Effect.tryPromise({
|
|
241
|
+
try: () => environment.physics,
|
|
242
|
+
catch: (cause) => new PhysicsUnavailable({ cause }),
|
|
243
|
+
});
|
|
244
|
+
const { handle, frames } = yield* sceneRuntimeFor(
|
|
245
|
+
environment,
|
|
246
|
+
element,
|
|
247
|
+
canvas,
|
|
248
|
+
overlays,
|
|
249
|
+
physics,
|
|
250
|
+
)(initial);
|
|
251
|
+
const mounted: Mounted = {
|
|
252
|
+
environment,
|
|
253
|
+
element,
|
|
254
|
+
canvas,
|
|
255
|
+
overlays,
|
|
256
|
+
handle,
|
|
257
|
+
frames,
|
|
258
|
+
gate: createFrameGate(frames),
|
|
259
|
+
live: createLatch(true),
|
|
260
|
+
};
|
|
261
|
+
yield* acquirePointer(canvas, mounted);
|
|
262
|
+
yield* acquireXrTarget(mounted);
|
|
263
|
+
yield* Effect.sync(touchActionSync(canvas, handle.graph));
|
|
264
|
+
yield* Effect.sync(reconciledDispatch(element, environment.store)(handle.startupOps));
|
|
265
|
+
const forks = watchers(mounted, updates, viewStateChanges).map((watcher) =>
|
|
266
|
+
Effect.forkScoped(watcher),
|
|
267
|
+
);
|
|
268
|
+
yield* Effect.all(forks, { concurrency: 'unbounded' });
|
|
269
|
+
return SceneLifecycle.StartedScene();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const failure = (reason: string) => SceneLifecycle.FailedScene({ reason });
|
|
273
|
+
|
|
274
|
+
const recoverStart = {
|
|
275
|
+
SchemaError: (error: Schema.SchemaError) =>
|
|
276
|
+
Effect.succeed(failure(`malformed scene: ${error.message}`)),
|
|
277
|
+
InvalidScene: ({ error }: { readonly error: { readonly _tag: string } }) =>
|
|
278
|
+
Effect.succeed(failure(`invalid scene: ${error._tag}`)),
|
|
279
|
+
RendererUnavailable: () => Effect.succeed(failure('renderer unavailable')),
|
|
280
|
+
PhysicsUnavailable: () => Effect.succeed(failure('physics unavailable')),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
type ExecuteInput = {
|
|
284
|
+
readonly element: Element;
|
|
285
|
+
readonly viewStateChanges: Stream.Stream<ViewState>;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const mountScene =
|
|
289
|
+
(environment: Environment) =>
|
|
290
|
+
({ element, viewStateChanges }: ExecuteInput) => {
|
|
291
|
+
const scene = Effect.gen(() => runScene(environment, element, viewStateChanges));
|
|
292
|
+
return scene.pipe(Effect.catchTags(recoverStart));
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Defines the scene Mount for an environment. Call once at the application entry point
|
|
297
|
+
* and pass the result to `view` as `mount`.
|
|
298
|
+
*/
|
|
299
|
+
export const makeSceneMount = (environment: Environment) =>
|
|
300
|
+
Mount.define('EffectThreeScene', {
|
|
301
|
+
messages: [SceneLifecycle.StartedScene, SceneLifecycle.FailedScene],
|
|
302
|
+
execute: mountScene(environment),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
export type SceneMount = ReturnType<typeof makeSceneMount>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay HTML in the scene view: the application's content for each overlay, in an
|
|
3
|
+
* element the scene moves to the overlay's point every frame it draws.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { Overlay, SceneDescription } from '@opsydyn/effect-three';
|
|
8
|
+
import type { Html, HtmlBuilder } from 'foldkit/html';
|
|
9
|
+
|
|
10
|
+
import { overlayData } from './dom/overlays.ts';
|
|
11
|
+
|
|
12
|
+
/** HTML shown at a point in the scene, as Drei's `Html`: where, and what. */
|
|
13
|
+
export type OverlayView = {
|
|
14
|
+
readonly overlay: Overlay;
|
|
15
|
+
readonly content: Html;
|
|
16
|
+
/**
|
|
17
|
+
* `'auto'` (the default, as Drei): the HTML takes pointer events. `'none'`: they pass
|
|
18
|
+
* through to the canvas, so the scene under a label stays pickable.
|
|
19
|
+
*/
|
|
20
|
+
readonly pointerEvents?: 'auto' | 'none';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The scene with the overlays' points added, so the scene places them. */
|
|
24
|
+
export const withOverlays = (
|
|
25
|
+
scene: SceneDescription,
|
|
26
|
+
overlays: ReadonlyArray<OverlayView>,
|
|
27
|
+
): SceneDescription =>
|
|
28
|
+
overlays.length === 0
|
|
29
|
+
? scene
|
|
30
|
+
: { ...scene, overlays: [...scene.overlays, ...overlays.map((view) => view.overlay)] };
|
|
31
|
+
|
|
32
|
+
// The layer covers the canvas, lets events through, and keeps Drei's large z-indexes
|
|
33
|
+
// inside the scene element rather than competing with the page.
|
|
34
|
+
const layerStyle = {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
inset: '0',
|
|
37
|
+
overflow: 'hidden',
|
|
38
|
+
pointerEvents: 'none',
|
|
39
|
+
isolation: 'isolate',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The overlay layer, or nothing when there are no overlays. */
|
|
43
|
+
export const overlayLayer = <Message>(
|
|
44
|
+
h: HtmlBuilder<Message>,
|
|
45
|
+
overlays: ReadonlyArray<OverlayView>,
|
|
46
|
+
): ReadonlyArray<Html> =>
|
|
47
|
+
overlays.length === 0
|
|
48
|
+
? []
|
|
49
|
+
: [
|
|
50
|
+
h.div(
|
|
51
|
+
[h.Style(layerStyle)],
|
|
52
|
+
overlays.map(({ overlay, content, pointerEvents = 'auto' }) =>
|
|
53
|
+
h.keyed('div')(
|
|
54
|
+
overlay.key,
|
|
55
|
+
[
|
|
56
|
+
h.DataAttribute(overlayData.key, overlay.key),
|
|
57
|
+
// Hidden until the scene first places it.
|
|
58
|
+
h.Style({
|
|
59
|
+
position: 'absolute',
|
|
60
|
+
top: '0',
|
|
61
|
+
left: '0',
|
|
62
|
+
transformOrigin: '0 0',
|
|
63
|
+
visibility: 'hidden',
|
|
64
|
+
pointerEvents,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
[content],
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
];
|