@opsydyn/effect-three-foldkit 0.3.0 → 0.6.4
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 +51 -0
- package/README.md +4 -2
- package/dist/assetRegistry.d.ts +49 -0
- package/dist/assetRegistry.d.ts.map +1 -0
- package/dist/assets.d.ts +18 -5
- package/dist/assets.d.ts.map +1 -1
- package/dist/boxSelect.d.ts +150 -0
- package/dist/boxSelect.d.ts.map +1 -0
- package/dist/dom/canvas.d.ts +7 -0
- package/dist/dom/canvas.d.ts.map +1 -1
- package/dist/dom/pointer.d.ts.map +1 -1
- package/dist/dom/webgl.d.ts.map +1 -1
- package/dist/dom/webgpu.d.ts.map +1 -1
- package/dist/element.d.ts +60 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/environment.d.ts +6 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +745 -60
- package/dist/index.js.map +19 -12
- package/dist/inspector.d.ts +24 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/measuringTape.d.ts +76 -0
- package/dist/measuringTape.d.ts.map +1 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/pivotControls.d.ts +106 -0
- package/dist/pivotControls.d.ts.map +1 -0
- package/dist/transformGizmo.d.ts +158 -0
- package/dist/transformGizmo.d.ts.map +1 -0
- package/dist/view.d.ts +23 -0
- package/dist/view.d.ts.map +1 -1
- package/dist/viewportGizmo.d.ts +146 -0
- package/dist/viewportGizmo.d.ts.map +1 -0
- package/package.json +8 -8
- package/src/assetRegistry.ts +218 -0
- package/src/assets.ts +96 -39
- package/src/boxSelect.ts +158 -0
- package/src/dom/canvas.ts +12 -0
- package/src/dom/pointer.ts +14 -1
- package/src/dom/webgl.ts +15 -0
- package/src/dom/webgpu.ts +15 -0
- package/src/element.ts +6 -0
- package/src/environment.ts +6 -1
- package/src/index.ts +10 -0
- package/src/inspector.ts +112 -0
- package/src/measuringTape.ts +124 -0
- package/src/mount.ts +24 -1
- package/src/pivotControls.ts +118 -0
- package/src/transformGizmo.ts +131 -0
- package/src/view.ts +28 -0
- package/src/viewportGizmo.ts +119 -0
package/src/dom/webgpu.ts
CHANGED
|
@@ -177,6 +177,21 @@ export const webgpuRendererFactory = (
|
|
|
177
177
|
scissored = true;
|
|
178
178
|
renderer.render(scene, camera);
|
|
179
179
|
},
|
|
180
|
+
renderOver: (scene, camera) => {
|
|
181
|
+
if (renderer.xr.isPresenting) return;
|
|
182
|
+
if (scissored) {
|
|
183
|
+
const { width, height } = renderer.getSize(drawingSize);
|
|
184
|
+
renderer.setScissorTest(false);
|
|
185
|
+
renderer.setViewport(0, 0, width, height);
|
|
186
|
+
scissored = false;
|
|
187
|
+
}
|
|
188
|
+
// As Drei's Hud: keep what is drawn, clear depth so the HUD is never hidden by it.
|
|
189
|
+
const autoClear = renderer.autoClear;
|
|
190
|
+
renderer.autoClear = false;
|
|
191
|
+
renderer.clearDepth();
|
|
192
|
+
renderer.render(scene, camera);
|
|
193
|
+
renderer.autoClear = autoClear;
|
|
194
|
+
},
|
|
180
195
|
createTarget: (width, height) => createTarget(targets, width, height),
|
|
181
196
|
createEnvironment: () => createRoom(renderer),
|
|
182
197
|
configure: (settings) => {
|
package/src/element.ts
CHANGED
|
@@ -59,9 +59,11 @@ export const sceneElement = CustomElement.define({
|
|
|
59
59
|
'scene-drag': SceneSchema.DragDetail,
|
|
60
60
|
'scene-drag-end': SceneSchema.DragDetail,
|
|
61
61
|
'scene-gesture': SceneSchema.GestureDetail,
|
|
62
|
+
'scene-gesture-end': SceneSchema.GestureEndDetail,
|
|
62
63
|
'scene-auto-rotated': SceneSchema.AutoRotatedDetail,
|
|
63
64
|
'scene-collision': SceneSchema.CollisionDetail,
|
|
64
65
|
'scene-contact-force': SceneSchema.ContactForceDetail,
|
|
66
|
+
'scene-physics-reached': SceneSchema.PhysicsReachedDetail,
|
|
65
67
|
'scene-measured': SceneSchema.MeasuredDetail,
|
|
66
68
|
'scene-label-measured': SceneSchema.LabelMeasuredDetail,
|
|
67
69
|
'scene-xr': XrSessionDetail,
|
|
@@ -73,6 +75,8 @@ export const sceneElement = CustomElement.define({
|
|
|
73
75
|
'scene-xr-meshes': SceneSchema.XrMeshesDetail,
|
|
74
76
|
'scene-performance': SceneSchema.PerformanceDetail,
|
|
75
77
|
'scene-error': Schema.Struct({ error: SceneError }),
|
|
78
|
+
'scene-asset-loaded': SceneSchema.AssetLoadedDetail,
|
|
79
|
+
'scene-asset-failed': SceneSchema.AssetFailedDetail,
|
|
76
80
|
'scene-reconciled': ReconciledDetail,
|
|
77
81
|
},
|
|
78
82
|
});
|
|
@@ -92,9 +96,11 @@ export const domEventName = {
|
|
|
92
96
|
Drag: 'scene-drag',
|
|
93
97
|
DragEnd: 'scene-drag-end',
|
|
94
98
|
Gesture: 'scene-gesture',
|
|
99
|
+
GestureEnd: 'scene-gesture-end',
|
|
95
100
|
AutoRotated: 'scene-auto-rotated',
|
|
96
101
|
Collision: 'scene-collision',
|
|
97
102
|
ContactForce: 'scene-contact-force',
|
|
103
|
+
PhysicsReached: 'scene-physics-reached',
|
|
98
104
|
Measured: 'scene-measured',
|
|
99
105
|
LabelMeasured: 'scene-label-measured',
|
|
100
106
|
XrStick: 'scene-xr-stick',
|
package/src/environment.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
type ResourceStore,
|
|
14
14
|
} from '@opsydyn/effect-three';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { makeAssetRegistry } from './assetRegistry.ts';
|
|
17
|
+
import { assetLoader, makeAssets } from './assets.ts';
|
|
17
18
|
import { animationFrameScheduler, type AnimationFrameHost } from './dom/animationFrames.ts';
|
|
18
19
|
import { defaultPixelRatio, type PixelRatio } from './dom/viewport.ts';
|
|
19
20
|
import { webglRendererFactory } from './dom/webgl.ts';
|
|
@@ -49,6 +50,7 @@ export type EnvironmentOptions = {
|
|
|
49
50
|
export const makeEnvironment = (host: EnvironmentHost, options: EnvironmentOptions = {}) => {
|
|
50
51
|
const store: ResourceStore = createResourceStore();
|
|
51
52
|
const assets = makeAssets(store);
|
|
53
|
+
const assetRegistry = makeAssetRegistry(assetLoader(store));
|
|
52
54
|
const xrHub = createXrHub(() => host.navigator?.xr);
|
|
53
55
|
return {
|
|
54
56
|
host,
|
|
@@ -63,12 +65,15 @@ export const makeEnvironment = (host: EnvironmentHost, options: EnvironmentOptio
|
|
|
63
65
|
labels: options.labels,
|
|
64
66
|
/** Loads textures and glTF models by URL and lends them to the store. */
|
|
65
67
|
assets,
|
|
68
|
+
/** The assets scenes ask for in their descriptions: loaded, shared, cancelled, freed. */
|
|
69
|
+
assetRegistry,
|
|
66
70
|
/** Enters and leaves immersive (WebXR) sessions, from Commands. */
|
|
67
71
|
xr: makeXr(xrHub),
|
|
68
72
|
/** The page's presentable scenes and its session; scene mounts register here. */
|
|
69
73
|
xrHub,
|
|
70
74
|
/** Disposes every loaded asset and every GPU resource still held. */
|
|
71
75
|
dispose: () => {
|
|
76
|
+
assetRegistry.dispose();
|
|
72
77
|
assets.dispose();
|
|
73
78
|
store.dispose();
|
|
74
79
|
},
|
package/src/index.ts
CHANGED
|
@@ -44,21 +44,31 @@ export {
|
|
|
44
44
|
type EnvironmentOptions,
|
|
45
45
|
makeEnvironment,
|
|
46
46
|
} from './environment.ts';
|
|
47
|
+
export { inspectorView, type OnEdit } from './inspector.ts';
|
|
47
48
|
export { makeSceneMount, type SceneMount } from './mount.ts';
|
|
49
|
+
export * as BoxSelect from './boxSelect.ts';
|
|
50
|
+
export * as MeasuringTape from './measuringTape.ts';
|
|
51
|
+
export * as PivotControls from './pivotControls.ts';
|
|
52
|
+
export * as TransformGizmo from './transformGizmo.ts';
|
|
53
|
+
export * as ViewportGizmo from './viewportGizmo.ts';
|
|
48
54
|
export { type OverlayView } from './overlayView.ts';
|
|
49
55
|
export {
|
|
56
|
+
type AssetFailedDetail,
|
|
57
|
+
type AssetLoadedDetail,
|
|
50
58
|
type AutoRotatedDetail,
|
|
51
59
|
type ButtonDetail,
|
|
52
60
|
type CollisionDetail,
|
|
53
61
|
type ContactForceDetail,
|
|
54
62
|
type DragDetail,
|
|
55
63
|
type Gesture,
|
|
64
|
+
type GestureEndDetail,
|
|
56
65
|
type HitDetail,
|
|
57
66
|
type KeyDetail,
|
|
58
67
|
type LabelMeasuredDetail,
|
|
59
68
|
type MeasuredDetail,
|
|
60
69
|
type MissDetail,
|
|
61
70
|
type PerformanceDetail,
|
|
71
|
+
type PhysicsReachedDetail,
|
|
62
72
|
view,
|
|
63
73
|
type ViewConfig,
|
|
64
74
|
type WheelDetail,
|
package/src/inspector.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An inspector panel for Foldkit views (roadmap, "Beyond parity" 4): the fields the
|
|
3
|
+
* core's `Inspector.fields` reads from a schema, drawn as a form. Each control reports
|
|
4
|
+
* an edit as the app's own Message, with the field's path and the new value; the app
|
|
5
|
+
* applies it with `Inspector.edit`, which the schema checks, and keeps the result (or
|
|
6
|
+
* shows why it was refused).
|
|
7
|
+
*
|
|
8
|
+
* Numbers, text and choices report when they change (on Enter or leaving the field),
|
|
9
|
+
* colours as they are picked, switches when clicked, and each of a vector's three
|
|
10
|
+
* numbers on its own. Kinds, absent parts and lists are shown, not edited.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { FieldPath, FieldValue, InspectedField } from '@opsydyn/effect-three';
|
|
15
|
+
import type { Html, HtmlBuilder } from 'foldkit/html';
|
|
16
|
+
|
|
17
|
+
/** Turns an edit (where, and the new value) into the app's Message. */
|
|
18
|
+
export type OnEdit<Message> = (path: FieldPath, value: FieldValue) => Message;
|
|
19
|
+
|
|
20
|
+
const axisNames = ['x', 'y', 'z'] as const;
|
|
21
|
+
|
|
22
|
+
/** The field's name for people: its property names, without list indices. */
|
|
23
|
+
const nameOf = (field: InspectedField): string =>
|
|
24
|
+
field.path.filter((part) => typeof part === 'string').join(' ') || field.label;
|
|
25
|
+
|
|
26
|
+
const hex = (colour: number): string => `#${colour.toString(16).padStart(6, '0')}`;
|
|
27
|
+
|
|
28
|
+
const numberInput = <Message>(
|
|
29
|
+
h: HtmlBuilder<Message>,
|
|
30
|
+
label: string,
|
|
31
|
+
value: number,
|
|
32
|
+
report: (raw: string) => Message,
|
|
33
|
+
): Html =>
|
|
34
|
+
h.input([
|
|
35
|
+
h.Type('number'),
|
|
36
|
+
h.Step('any'),
|
|
37
|
+
h.Value(String(value)),
|
|
38
|
+
h.AriaLabel(label),
|
|
39
|
+
h.OnChange(report),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const controlOf = <Message>(
|
|
43
|
+
h: HtmlBuilder<Message>,
|
|
44
|
+
field: InspectedField,
|
|
45
|
+
onEdit: OnEdit<Message>,
|
|
46
|
+
): Html => {
|
|
47
|
+
const name = nameOf(field);
|
|
48
|
+
switch (field._tag) {
|
|
49
|
+
case 'Number':
|
|
50
|
+
return numberInput(h, name, field.value, (raw) => onEdit(field.path, Number(raw)));
|
|
51
|
+
case 'Text':
|
|
52
|
+
return h.input([
|
|
53
|
+
h.Type('text'),
|
|
54
|
+
h.Value(field.value),
|
|
55
|
+
h.AriaLabel(name),
|
|
56
|
+
h.OnChange((raw) => onEdit(field.path, raw)),
|
|
57
|
+
]);
|
|
58
|
+
case 'Toggle':
|
|
59
|
+
return h.input([
|
|
60
|
+
h.Type('checkbox'),
|
|
61
|
+
h.Checked(field.value),
|
|
62
|
+
h.AriaLabel(name),
|
|
63
|
+
h.OnClick(onEdit(field.path, !field.value)),
|
|
64
|
+
]);
|
|
65
|
+
case 'Choice':
|
|
66
|
+
return h.select(
|
|
67
|
+
[h.AriaLabel(name), h.OnChange((raw) => onEdit(field.path, raw))],
|
|
68
|
+
field.options.map((option) =>
|
|
69
|
+
h.option([h.Value(option), h.Selected(option === field.value)], [option]),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
case 'Colour':
|
|
73
|
+
return h.input([
|
|
74
|
+
h.Type('color'),
|
|
75
|
+
h.Value(hex(field.value)),
|
|
76
|
+
h.AriaLabel(name),
|
|
77
|
+
h.OnInput((raw) => onEdit(field.path, Number.parseInt(raw.slice(1), 16))),
|
|
78
|
+
]);
|
|
79
|
+
case 'Vector':
|
|
80
|
+
return h.span(
|
|
81
|
+
[h.Class('e3-inspector-vector')],
|
|
82
|
+
field.value.map((component, index) =>
|
|
83
|
+
numberInput(h, `${name} ${axisNames[index] ?? index}`, component, (raw) =>
|
|
84
|
+
onEdit([...field.path, index], Number(raw)),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
case 'Fixed':
|
|
89
|
+
return h.span([h.Class('e3-inspector-fixed')], [field.text]);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The fields as a form: a row each, its name and its control, keyed by path. Style it
|
|
95
|
+
* with the classes `e3-inspector`, `e3-inspector-row`, `e3-inspector-name`,
|
|
96
|
+
* `e3-inspector-vector` and `e3-inspector-fixed`.
|
|
97
|
+
*/
|
|
98
|
+
export const inspectorView = <Message>(
|
|
99
|
+
h: HtmlBuilder<Message>,
|
|
100
|
+
fields: ReadonlyArray<InspectedField>,
|
|
101
|
+
onEdit: OnEdit<Message>,
|
|
102
|
+
): Html =>
|
|
103
|
+
h.div(
|
|
104
|
+
[h.Class('e3-inspector')],
|
|
105
|
+
fields.map((field) =>
|
|
106
|
+
h.keyed('label')(
|
|
107
|
+
field.path.join('.'),
|
|
108
|
+
[h.Class('e3-inspector-row')],
|
|
109
|
+
[h.span([h.Class('e3-inspector-name')], [nameOf(field)]), controlOf(h, field, onEdit)],
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A measuring tape as a Foldkit submodel (roadmap, "Beyond parity" 5): the core's pure
|
|
3
|
+
* `tape` with Messages for clicks and pointer moves on the scene, an OutMessage when a
|
|
4
|
+
* length is measured, and its length as HTML at the tape's middle. The parent decides
|
|
5
|
+
* when the tape is in use and which clicks are on the scene:
|
|
6
|
+
*
|
|
7
|
+
* - `Placed` (a click on the scene, with the point it hit) places an end, or starts a
|
|
8
|
+
* new tape; placing the second reports `Measured { from, to, length }`;
|
|
9
|
+
* - `Pointed` (the pointer moving over the scene, or `null` off it) runs the tape to
|
|
10
|
+
* the pointer while the second end is placed;
|
|
11
|
+
* - `Cleared` empties it; `ChoseSnap` rounds new ends to a step, or not.
|
|
12
|
+
*
|
|
13
|
+
* `nodes(model)` draws it for the scene and `label(model, h)` is the length for the
|
|
14
|
+
* view's `overlays`.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
clearTape,
|
|
20
|
+
overlay,
|
|
21
|
+
placeTapeEnd,
|
|
22
|
+
pointTape,
|
|
23
|
+
SceneSchema,
|
|
24
|
+
type SceneNode,
|
|
25
|
+
tape,
|
|
26
|
+
tapeLength,
|
|
27
|
+
type TapeLook,
|
|
28
|
+
tapeMiddle,
|
|
29
|
+
tapeNodes,
|
|
30
|
+
} from '@opsydyn/effect-three';
|
|
31
|
+
import { Option, Schema } from 'effect';
|
|
32
|
+
import type { HtmlBuilder } from 'foldkit/html';
|
|
33
|
+
import { defineMessageUnion } from 'foldkit/message';
|
|
34
|
+
import type { ReturnWithOutMessage } from 'foldkit/update';
|
|
35
|
+
|
|
36
|
+
import type { OverlayView } from './overlayView.ts';
|
|
37
|
+
|
|
38
|
+
export const Model = Schema.Struct({
|
|
39
|
+
/** The key its nodes and label are drawn under. */
|
|
40
|
+
key: Schema.String,
|
|
41
|
+
tape: SceneSchema.Tape,
|
|
42
|
+
});
|
|
43
|
+
export type Model = typeof Model.Type;
|
|
44
|
+
|
|
45
|
+
export const Message = defineMessageUnion({
|
|
46
|
+
Placed: { point: SceneSchema.Vec3 },
|
|
47
|
+
Pointed: { point: Schema.NullOr(SceneSchema.Vec3) },
|
|
48
|
+
Cleared: {},
|
|
49
|
+
ChoseSnap: { snap: SceneSchema.Tape.fields.snap },
|
|
50
|
+
});
|
|
51
|
+
export type Message = typeof Message.Type;
|
|
52
|
+
|
|
53
|
+
export const OutMessage = defineMessageUnion({
|
|
54
|
+
/** Both ends are placed: where, and how far apart. */
|
|
55
|
+
Measured: { from: SceneSchema.Vec3, to: SceneSchema.Vec3, length: Schema.Finite },
|
|
56
|
+
});
|
|
57
|
+
export type OutMessage = typeof OutMessage.Type;
|
|
58
|
+
|
|
59
|
+
/** An empty tape drawn under `key`, snapping to `snap` when given. */
|
|
60
|
+
export const init = (key: string, options: { readonly snap?: number | null } = {}): Model => ({
|
|
61
|
+
key,
|
|
62
|
+
tape: tape(options),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
type Return = ReturnWithOutMessage<Model, Message, OutMessage>;
|
|
66
|
+
|
|
67
|
+
/** The OutMessage for a tape that has just been measured, if it has. */
|
|
68
|
+
const measured = (next: Model): Return =>
|
|
69
|
+
Option.match(
|
|
70
|
+
Option.all([Option.fromNullishOr(next.tape.from), Option.fromNullishOr(next.tape.to)]),
|
|
71
|
+
{
|
|
72
|
+
onNone: () => ({ model: next }),
|
|
73
|
+
onSome: ([from, to]) => ({
|
|
74
|
+
model: next,
|
|
75
|
+
outMessage: OutMessage.Measured({ from, to, length: tapeLength(next.tape) ?? 0 }),
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const update = (model: Model, message: Message): Return =>
|
|
81
|
+
Message.match<Return>(message, {
|
|
82
|
+
Placed: ({ point }) => measured({ ...model, tape: placeTapeEnd(model.tape, point) }),
|
|
83
|
+
Pointed: ({ point }) => ({ model: { ...model, tape: pointTape(model.tape, point) } }),
|
|
84
|
+
Cleared: () => ({ model: { ...model, tape: clearTape(model.tape) } }),
|
|
85
|
+
ChoseSnap: ({ snap }) => ({ model: { ...model, tape: { ...model.tape, snap } } }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** The tape's length, while it has one (to the pointer while the second end is placed). */
|
|
89
|
+
export const length = (model: Model): Option.Option<number> =>
|
|
90
|
+
Option.fromNullishOr(tapeLength(model.tape));
|
|
91
|
+
|
|
92
|
+
/** The tape, for the parent's scene. */
|
|
93
|
+
export const nodes = (model: Model, look: Partial<TapeLook> = {}): SceneNode =>
|
|
94
|
+
tapeNodes(model.key, model.tape, look);
|
|
95
|
+
|
|
96
|
+
/** The label's class, when one is given. */
|
|
97
|
+
const classOf = <Message>(h: HtmlBuilder<Message>, className: string | undefined) =>
|
|
98
|
+
Option.match(Option.fromNullishOr(className), {
|
|
99
|
+
onNone: () => [],
|
|
100
|
+
onSome: (name) => [h.Class(name)],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The length at the tape's middle, for the view's `overlays`: two decimals and `unit`
|
|
105
|
+
* (`m` unless given), in an element with the class `className` (unstyled unless given).
|
|
106
|
+
* Pointer events pass through it to the scene.
|
|
107
|
+
*/
|
|
108
|
+
export const label = <Message>(
|
|
109
|
+
model: Model,
|
|
110
|
+
h: HtmlBuilder<Message>,
|
|
111
|
+
options: { readonly unit?: string; readonly className?: string } = {},
|
|
112
|
+
): ReadonlyArray<OverlayView> =>
|
|
113
|
+
Option.match(Option.all([length(model), Option.fromNullishOr(tapeMiddle(model.tape))]), {
|
|
114
|
+
onNone: () => [],
|
|
115
|
+
onSome: ([measuredLength, middle]) => [
|
|
116
|
+
{
|
|
117
|
+
overlay: overlay({ key: `${model.key}:label`, position: middle, center: true }),
|
|
118
|
+
content: h.span(classOf(h, options.className), [
|
|
119
|
+
`${measuredLength.toFixed(2)} ${options.unit ?? 'm'}`,
|
|
120
|
+
]),
|
|
121
|
+
pointerEvents: 'none',
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
package/src/mount.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
removal,
|
|
28
28
|
sceneErrorDispatch,
|
|
29
29
|
sceneEventDispatch,
|
|
30
|
+
dispatchAssetNotice,
|
|
30
31
|
} from './dom/canvas.ts';
|
|
31
32
|
import { applying, createLatch, type Latch, routedWhileOpen } from './dom/latch.ts';
|
|
32
33
|
import { containOverlays, createOverlayWriter, type OverlayWriter } from './dom/overlays.ts';
|
|
@@ -140,6 +141,27 @@ const connectObserver =
|
|
|
140
141
|
const observed = <A>(observe: (emit: (value: A) => void) => () => void) =>
|
|
141
142
|
Stream.callback<A>(connectObserver(observe));
|
|
142
143
|
|
|
144
|
+
/** What the scene asks for now, given to the environment's asset registry. */
|
|
145
|
+
const asking = (mounted: Mounted, scene: SceneDescription) => () =>
|
|
146
|
+
mounted.environment.assetRegistry.want(mounted.element, scene.assets, (notice) =>
|
|
147
|
+
dispatchAssetNotice(mounted.element, notice),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const lettingGo = (mounted: Mounted) => () =>
|
|
151
|
+
mounted.environment.assetRegistry.release(mounted.element);
|
|
152
|
+
|
|
153
|
+
/** The scene's assets, asked for while it is mounted: going, it asks for none. */
|
|
154
|
+
const acquireAssets = (mounted: Mounted, initial: SceneDescription) => {
|
|
155
|
+
const ask = Effect.sync(asking(mounted, initial));
|
|
156
|
+
return Effect.acquireRelease(ask, () => Effect.sync(lettingGo(mounted)));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** An update applied, then what it asks for given to the registry (a refused one asks nothing). */
|
|
160
|
+
const updated = (mounted: Mounted) => (scene: SceneDescription) => {
|
|
161
|
+
const ask = Effect.sync(asking(mounted, scene));
|
|
162
|
+
return Effect.tap(mounted.handle.update(scene), () => ask);
|
|
163
|
+
};
|
|
164
|
+
|
|
143
165
|
const applyUpdate = (mounted: Mounted) => (value: unknown) => {
|
|
144
166
|
const reportError = sceneErrorDispatch(mounted.element);
|
|
145
167
|
const reportReconciled = afterUpdate(
|
|
@@ -148,7 +170,7 @@ const applyUpdate = (mounted: Mounted) => (value: unknown) => {
|
|
|
148
170
|
);
|
|
149
171
|
const decoded = decodeScene(value);
|
|
150
172
|
const applied = decoded.pipe(
|
|
151
|
-
Effect.flatMap(mounted
|
|
173
|
+
Effect.flatMap(updated(mounted)),
|
|
152
174
|
Effect.flatMap((ops) => Effect.sync(reportReconciled(ops))),
|
|
153
175
|
);
|
|
154
176
|
return applied.pipe(
|
|
@@ -258,6 +280,7 @@ function* runScene(
|
|
|
258
280
|
gate: createFrameGate(frames),
|
|
259
281
|
live: createLatch(true),
|
|
260
282
|
};
|
|
283
|
+
yield* acquireAssets(mounted, initial);
|
|
261
284
|
yield* acquirePointer(canvas, mounted);
|
|
262
285
|
yield* acquireXrTarget(mounted);
|
|
263
286
|
yield* Effect.sync(touchActionSync(canvas, handle.graph));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pivot controls as a Foldkit submodel (roadmap, "Beyond parity" 5): the core's pure
|
|
3
|
+
* `pivotNodes`, `pressPivot` and `dragPivot` (after Drei's `PivotControls`) with
|
|
4
|
+
* Messages for the scene's pointer events and OutMessages for what the parent does with
|
|
5
|
+
* them, as `TransformGizmo` is for three's controls. The parent owns where its objects
|
|
6
|
+
* are; the submodel owns only the handles:
|
|
7
|
+
*
|
|
8
|
+
* - `Pressed` (a pointer down on one of its handles, with the point it hit and the
|
|
9
|
+
* object's placement) takes hold;
|
|
10
|
+
* - `Dragged` reports `Moved { placement }` for the parent to apply (`snap` rounds turns
|
|
11
|
+
* to ten degrees and scales to tenths, as Drei's shift does);
|
|
12
|
+
* - `Released` lets go and reports `Finished { placement }`, a step for history;
|
|
13
|
+
* - `Hovered` highlights a handle.
|
|
14
|
+
*
|
|
15
|
+
* `handleOf(model, key)` says whether a hit is on a handle, and `nodes(model, placement)`
|
|
16
|
+
* draws them.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
dragPivot,
|
|
22
|
+
hoverPivot,
|
|
23
|
+
type Pivot,
|
|
24
|
+
pivot,
|
|
25
|
+
type PivotHandle,
|
|
26
|
+
pivotHandleOf,
|
|
27
|
+
pivotNodes,
|
|
28
|
+
type Placement,
|
|
29
|
+
pressPivot,
|
|
30
|
+
releasePivot,
|
|
31
|
+
SceneSchema,
|
|
32
|
+
type SceneNode,
|
|
33
|
+
type Vec3,
|
|
34
|
+
} from '@opsydyn/effect-three';
|
|
35
|
+
import { Option, Schema } from 'effect';
|
|
36
|
+
import { defineMessageUnion } from 'foldkit/message';
|
|
37
|
+
import type { ReturnWithOutMessage } from 'foldkit/update';
|
|
38
|
+
|
|
39
|
+
export const Model = Schema.Struct({
|
|
40
|
+
/** The key its handles are drawn under: `${key}:arrow-x`, `${key}:slider-z` and so on. */
|
|
41
|
+
key: Schema.String,
|
|
42
|
+
pivot: SceneSchema.Pivot,
|
|
43
|
+
/** The placement the drag last reported, so `Released` can report where it ended. */
|
|
44
|
+
last: Schema.Option(SceneSchema.Placement),
|
|
45
|
+
});
|
|
46
|
+
export type Model = typeof Model.Type;
|
|
47
|
+
|
|
48
|
+
const Handle = SceneSchema.PivotHandle;
|
|
49
|
+
|
|
50
|
+
export const Message = defineMessageUnion({
|
|
51
|
+
Pressed: { handle: Handle, point: SceneSchema.Vec3, placement: SceneSchema.Placement },
|
|
52
|
+
Dragged: { origin: SceneSchema.Vec3, direction: SceneSchema.Vec3, snap: Schema.Boolean },
|
|
53
|
+
Released: {},
|
|
54
|
+
Hovered: { handle: Schema.NullOr(Handle) },
|
|
55
|
+
});
|
|
56
|
+
export type Message = typeof Message.Type;
|
|
57
|
+
|
|
58
|
+
export const OutMessage = defineMessageUnion({
|
|
59
|
+
/** The object's placement now, during a drag. */
|
|
60
|
+
Moved: { placement: SceneSchema.Placement },
|
|
61
|
+
/** The drag is over, the object here: a step for history. */
|
|
62
|
+
Finished: { placement: SceneSchema.Placement },
|
|
63
|
+
});
|
|
64
|
+
export type OutMessage = typeof OutMessage.Type;
|
|
65
|
+
|
|
66
|
+
/** Pivot controls drawn under `key`, with Drei's defaults or the options given. */
|
|
67
|
+
export const init = (key: string, options: Parameters<typeof pivot>[0] = {}): Model => ({
|
|
68
|
+
key,
|
|
69
|
+
pivot: pivot(options),
|
|
70
|
+
last: Option.none(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
type Return = ReturnWithOutMessage<Model, Message, OutMessage>;
|
|
74
|
+
|
|
75
|
+
const withPivot = (model: Model, next: Pivot): Model => ({ ...model, pivot: next });
|
|
76
|
+
|
|
77
|
+
const finished = (model: Model): Return => ({
|
|
78
|
+
model: { ...withPivot(model, releasePivot(model.pivot)), last: Option.none() },
|
|
79
|
+
...Option.match(model.last, {
|
|
80
|
+
onNone: () => ({}),
|
|
81
|
+
onSome: (placement) => ({ outMessage: OutMessage.Finished({ placement }) }),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const dragged = (model: Model, origin: Vec3, direction: Vec3, snap: boolean): Return => {
|
|
86
|
+
const placement = Option.fromNullishOr(dragPivot(model.pivot, { origin, direction }, { snap }));
|
|
87
|
+
return Option.match(placement, {
|
|
88
|
+
onNone: () => ({ model }),
|
|
89
|
+
onSome: (placement) => ({
|
|
90
|
+
model: { ...model, last: Option.some(placement) },
|
|
91
|
+
outMessage: OutMessage.Moved({ placement }),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const update = (model: Model, message: Message): Return =>
|
|
97
|
+
Message.match<Return>(message, {
|
|
98
|
+
Pressed: ({ handle, point, placement }) => ({
|
|
99
|
+
model: {
|
|
100
|
+
...withPivot(model, pressPivot(model.pivot, placement, handle, point)),
|
|
101
|
+
last: Option.none(),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
Dragged: ({ origin, direction, snap }) => dragged(model, origin, direction, snap),
|
|
105
|
+
Released: () => finished(model),
|
|
106
|
+
Hovered: ({ handle }) => ({ model: withPivot(model, hoverPivot(model.pivot, handle)) }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Whether a hit `key` is one of these controls' handles, and which. */
|
|
110
|
+
export const handleOf = (model: Model, key: string): Option.Option<PivotHandle> =>
|
|
111
|
+
Option.fromNullishOr(pivotHandleOf(model.key, key));
|
|
112
|
+
|
|
113
|
+
/** Whether a handle is held: the parent leaves the camera alone while it is. */
|
|
114
|
+
export const held = (model: Model): boolean => model.pivot.grab !== null;
|
|
115
|
+
|
|
116
|
+
/** The handles for `placement`, for the parent's scene. */
|
|
117
|
+
export const nodes = (model: Model, placement: Placement): SceneNode =>
|
|
118
|
+
pivotNodes(model.key, model.pivot, placement);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A transform gizmo as a Foldkit submodel (roadmap, "Beyond parity" 5): the core's pure
|
|
3
|
+
* gizmo (`gizmoNodes`, `pressGizmo`, `dragGizmo`, after three's `TransformControls`)
|
|
4
|
+
* with Messages for the scene's pointer events and OutMessages for what the parent does
|
|
5
|
+
* with them. The parent owns where its objects are; the submodel owns only the gizmo:
|
|
6
|
+
*
|
|
7
|
+
* - `Pressed` (a press on one of its handles, with the object's placement and the
|
|
8
|
+
* camera) takes hold;
|
|
9
|
+
* - `Dragged` reports `Moved { placement }` for the parent to apply;
|
|
10
|
+
* - `Released` lets go and reports `Finished { placement }`, the moment to record a step
|
|
11
|
+
* of history;
|
|
12
|
+
* - `Hovered` highlights a handle; `ChoseMode` and `ChoseSpace` switch them.
|
|
13
|
+
*
|
|
14
|
+
* `handleOf(model, key)` says whether a hit is on a handle, and `nodes(model, placement,
|
|
15
|
+
* camera)` draws them, for the parent's scene and view.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
type Camera,
|
|
21
|
+
dragGizmo,
|
|
22
|
+
type Gizmo,
|
|
23
|
+
gizmo,
|
|
24
|
+
type GizmoHandle,
|
|
25
|
+
gizmoHandleOf,
|
|
26
|
+
gizmoNodes,
|
|
27
|
+
hoverGizmo,
|
|
28
|
+
type Placement,
|
|
29
|
+
pressGizmo,
|
|
30
|
+
releaseGizmo,
|
|
31
|
+
SceneSchema,
|
|
32
|
+
type SceneNode,
|
|
33
|
+
type Vec3,
|
|
34
|
+
} from '@opsydyn/effect-three';
|
|
35
|
+
import { Option, Schema } from 'effect';
|
|
36
|
+
import { defineMessageUnion } from 'foldkit/message';
|
|
37
|
+
import type { ReturnWithOutMessage } from 'foldkit/update';
|
|
38
|
+
|
|
39
|
+
export const Model = Schema.Struct({
|
|
40
|
+
/** The key its handles are drawn under: `${key}:x`, `${key}:xy` and so on. */
|
|
41
|
+
key: Schema.String,
|
|
42
|
+
gizmo: SceneSchema.Gizmo,
|
|
43
|
+
/** The placement the drag last reported, so `Released` can report where it ended. */
|
|
44
|
+
last: Schema.Option(SceneSchema.Placement),
|
|
45
|
+
});
|
|
46
|
+
export type Model = typeof Model.Type;
|
|
47
|
+
|
|
48
|
+
const Handle = Schema.Literals(['x', 'y', 'z', 'xy', 'yz', 'xz', 'xyz']);
|
|
49
|
+
|
|
50
|
+
export const Message = defineMessageUnion({
|
|
51
|
+
Pressed: {
|
|
52
|
+
handle: Handle,
|
|
53
|
+
origin: SceneSchema.Vec3,
|
|
54
|
+
direction: SceneSchema.Vec3,
|
|
55
|
+
placement: SceneSchema.Placement,
|
|
56
|
+
camera: SceneSchema.Camera,
|
|
57
|
+
},
|
|
58
|
+
Dragged: { origin: SceneSchema.Vec3, direction: SceneSchema.Vec3 },
|
|
59
|
+
Released: {},
|
|
60
|
+
Hovered: { handle: Schema.NullOr(Handle) },
|
|
61
|
+
ChoseMode: { mode: Schema.Literals(['translate', 'rotate', 'scale']) },
|
|
62
|
+
ChoseSpace: { space: Schema.Literals(['world', 'local']) },
|
|
63
|
+
});
|
|
64
|
+
export type Message = typeof Message.Type;
|
|
65
|
+
|
|
66
|
+
export const OutMessage = defineMessageUnion({
|
|
67
|
+
/** The object's placement now, during a drag. */
|
|
68
|
+
Moved: { placement: SceneSchema.Placement },
|
|
69
|
+
/** The drag is over, the object here: a step for history. */
|
|
70
|
+
Finished: { placement: SceneSchema.Placement },
|
|
71
|
+
});
|
|
72
|
+
export type OutMessage = typeof OutMessage.Type;
|
|
73
|
+
|
|
74
|
+
/** A gizmo drawn under `key`, with the core's defaults or the options given. */
|
|
75
|
+
export const init = (key: string, options: Parameters<typeof gizmo>[0] = {}): Model => ({
|
|
76
|
+
key,
|
|
77
|
+
gizmo: gizmo(options),
|
|
78
|
+
last: Option.none(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
type Return = ReturnWithOutMessage<Model, Message, OutMessage>;
|
|
82
|
+
|
|
83
|
+
const withGizmo = (model: Model, next: Gizmo): Model => ({ ...model, gizmo: next });
|
|
84
|
+
|
|
85
|
+
const finished = (model: Model): Return => ({
|
|
86
|
+
model: { ...withGizmo(model, releaseGizmo(model.gizmo)), last: Option.none() },
|
|
87
|
+
...Option.match(model.last, {
|
|
88
|
+
onNone: () => ({}),
|
|
89
|
+
onSome: (placement) => ({ outMessage: OutMessage.Finished({ placement }) }),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const dragged = (model: Model, origin: Vec3, direction: Vec3): Return => {
|
|
94
|
+
const placement = Option.fromNullishOr(dragGizmo(model.gizmo, { origin, direction }));
|
|
95
|
+
return Option.match(placement, {
|
|
96
|
+
onNone: () => ({ model }),
|
|
97
|
+
onSome: (placement) => ({
|
|
98
|
+
model: { ...model, last: Option.some(placement) },
|
|
99
|
+
outMessage: OutMessage.Moved({ placement }),
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const update = (model: Model, message: Message): Return =>
|
|
105
|
+
Message.match<Return>(message, {
|
|
106
|
+
Pressed: ({ handle, origin, direction, placement, camera }) => ({
|
|
107
|
+
model: {
|
|
108
|
+
...withGizmo(
|
|
109
|
+
model,
|
|
110
|
+
pressGizmo(model.gizmo, placement, handle, { origin, direction }, camera),
|
|
111
|
+
),
|
|
112
|
+
last: Option.none(),
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
Dragged: ({ origin, direction }) => dragged(model, origin, direction),
|
|
116
|
+
Released: () => finished(model),
|
|
117
|
+
Hovered: ({ handle }) => ({ model: withGizmo(model, hoverGizmo(model.gizmo, handle)) }),
|
|
118
|
+
ChoseMode: ({ mode }) => ({ model: withGizmo(model, { ...model.gizmo, mode }) }),
|
|
119
|
+
ChoseSpace: ({ space }) => ({ model: withGizmo(model, { ...model.gizmo, space }) }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** Whether a hit `key` is one of this gizmo's handles, and which. */
|
|
123
|
+
export const handleOf = (model: Model, key: string): Option.Option<GizmoHandle> =>
|
|
124
|
+
Option.fromNullishOr(gizmoHandleOf(model.key, key));
|
|
125
|
+
|
|
126
|
+
/** Whether the gizmo is held: the parent leaves the camera alone while it is. */
|
|
127
|
+
export const held = (model: Model): boolean => model.gizmo.grab !== null;
|
|
128
|
+
|
|
129
|
+
/** The handles for `placement`, seen from `camera`, for the parent's scene. */
|
|
130
|
+
export const nodes = (model: Model, placement: Placement, camera: Camera): SceneNode =>
|
|
131
|
+
gizmoNodes(model.key, model.gizmo, placement, camera);
|