@driftengine/script 4.7.4 → 4.8.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/README.md +9 -4
- package/capabilities.json +229 -54
- package/dist/bindings/behavior.js +10 -6
- package/dist/bindings/chemistry.d.ts +4 -0
- package/dist/bindings/chemistry.js +40 -4
- package/dist/bindings/core.js +23 -4
- package/dist/bindings/editor.d.ts +10 -7
- package/dist/bindings/editor.js +72 -40
- package/dist/bindings/entities.js +16 -4
- package/dist/bindings/variants.d.ts +25 -0
- package/dist/bindings/variants.js +9 -0
- package/dist/bindings/xr.js +27 -14
- package/package.json +13 -13
- package/src/bindings/behavior.ts +19 -7
- package/src/bindings/chemistry.ts +54 -6
- package/src/bindings/core.ts +38 -6
- package/src/bindings/editor.ts +118 -44
- package/src/bindings/entities.ts +16 -4
- package/src/bindings/variants.ts +41 -0
- package/src/bindings/xr.ts +52 -25
|
@@ -40,6 +40,10 @@ import { AtmosphereField, ChemistryWorld, ParcelStore, SpeciesRegistry, Substanc
|
|
|
40
40
|
import { type ComponentType } from '@driftengine/entities';
|
|
41
41
|
import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
|
|
42
42
|
export declare const CHEMISTRY_MODULE = "drift/chemistry";
|
|
43
|
+
/** A parcel's phase, in the package's order: `PHASE_SOLID`, `_LIQUID`, `_GAS`, then `PHASE_MIXED`. */
|
|
44
|
+
export declare const MATTER_PHASE: import("./variants.ts").HostEnum;
|
|
45
|
+
/** What happened to a parcel this tick, in the order of the package's `EVENT_*` constants. */
|
|
46
|
+
export declare const CHEMISTRY_EVENT: import("./variants.ts").HostEnum;
|
|
43
47
|
/**
|
|
44
48
|
* The three opaque types `§20.2` names.
|
|
45
49
|
*
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { AtmosphereField, ChemistryWorld, ParcelStore, SpeciesRegistry, SubstanceRegistry, } from '@driftengine/chemistry';
|
|
40
40
|
import { defineComponent } from '@driftengine/entities';
|
|
41
41
|
import { defineCapability } from 'driftscript';
|
|
42
|
+
import { hostEnum } from './variants.js';
|
|
42
43
|
export const CHEMISTRY_MODULE = 'drift/chemistry';
|
|
43
44
|
const define = (name, params, returns, effects, doc) => defineCapability({
|
|
44
45
|
module: CHEMISTRY_MODULE,
|
|
@@ -52,6 +53,32 @@ const define = (name, params, returns, effects, doc) => defineCapability({
|
|
|
52
53
|
doc,
|
|
53
54
|
implementation: `${CHEMISTRY_MODULE}.${name}`,
|
|
54
55
|
});
|
|
56
|
+
/** A parcel's phase, in the package's order: `PHASE_SOLID`, `_LIQUID`, `_GAS`, then `PHASE_MIXED`. */
|
|
57
|
+
export const MATTER_PHASE = hostEnum(CHEMISTRY_MODULE, 'MatterPhase', ['Solid', 'Liquid', 'Gas', 'Mixed'], 'What a parcel is made of now. Mixed is a real answer: wet wood is a solid and a liquid at once.');
|
|
58
|
+
/** What happened to a parcel this tick, in the order of the package's `EVENT_*` constants. */
|
|
59
|
+
export const CHEMISTRY_EVENT = hostEnum(CHEMISTRY_MODULE, 'ChemistryEvent', [
|
|
60
|
+
'Ignited',
|
|
61
|
+
'Extinguished',
|
|
62
|
+
'SmoulderStart',
|
|
63
|
+
'SmoulderEnd',
|
|
64
|
+
'Consumed',
|
|
65
|
+
'Charred',
|
|
66
|
+
'StructuralFail',
|
|
67
|
+
'Frozen',
|
|
68
|
+
'Melted',
|
|
69
|
+
'Boiled',
|
|
70
|
+
'Condensed',
|
|
71
|
+
'Sublimed',
|
|
72
|
+
'Corroded',
|
|
73
|
+
'Calcined',
|
|
74
|
+
'Dissolved',
|
|
75
|
+
'Denatured',
|
|
76
|
+
'Browned',
|
|
77
|
+
'Caramelised',
|
|
78
|
+
'Fermented',
|
|
79
|
+
'Decayed',
|
|
80
|
+
'Exploded',
|
|
81
|
+
], 'What happened to a parcel this tick: it caught, went out, melted, boiled and the rest.');
|
|
55
82
|
/**
|
|
56
83
|
* The three opaque types `§20.2` names.
|
|
57
84
|
*
|
|
@@ -75,6 +102,8 @@ export const CHEMISTRY_TYPES = [
|
|
|
75
102
|
name: 'Species',
|
|
76
103
|
doc: 'A registered chemical species, such as `O2` or `cellulose`.',
|
|
77
104
|
},
|
|
105
|
+
MATTER_PHASE.type,
|
|
106
|
+
CHEMISTRY_EVENT.type,
|
|
78
107
|
];
|
|
79
108
|
const READ = ['chemistry.read'];
|
|
80
109
|
const WRITE = ['chemistry.write'];
|
|
@@ -104,7 +133,7 @@ export const CHEMISTRY_CAPABILITIES = [
|
|
|
104
133
|
define('charDepth', [P, PARCEL], 'f32', READ, 'Metres of char measured inward from the surface.'),
|
|
105
134
|
define('wetness', [P, PARCEL], 'f32', READ, 'kg of liquid water on the surface, per square metre of it.'),
|
|
106
135
|
define('wettable', [P, PARCEL], 'bool', READ, 'Whether this material can hold liquid water, which is what `wet` asks. Rain over everything asks first, since `wet` refuses an iron nail.'),
|
|
107
|
-
define('phase', [P, PARCEL], '
|
|
136
|
+
define('phase', [P, PARCEL], 'MatterPhase', READ, 'Whether the parcel is solid, liquid, gas or mixed. Mixed is a real answer: wet wood is a solid and a liquid at once.'),
|
|
108
137
|
define('burning', [P, PARCEL], 'bool', READ, 'Whether a flame stands over it.'),
|
|
109
138
|
define('smouldering', [P, PARCEL], 'bool', READ, 'Whether it is glowing with no flame. A smoulder survives air a flame cannot, which is why smothering leaves embers.'),
|
|
110
139
|
define('heatRelease', [P, PARCEL], 'f32', READ, "kW the reactions released last tick. A fire's size is its heat release rate, not its temperature. Negative while a surface is gasifying, which is correct."),
|
|
@@ -153,7 +182,7 @@ export const CHEMISTRY_CAPABILITIES = [
|
|
|
153
182
|
define('addAirHeat', [P, { name: 'joules', type: 'f32' }, ...XYZ], 'void', WRITE, 'Joules into a cell of air, which is what a heater does and what a fire does to the room.'),
|
|
154
183
|
/* The event buffer, §20.5, in the shape `drift/physics`'s contact buffer established. */
|
|
155
184
|
define('eventCount', [P], 'i32', READ, 'Events this tick. Cleared at the top of every one.'),
|
|
156
|
-
define('eventKind', [P, { name: 'index', type: 'i32' }], '
|
|
185
|
+
define('eventKind', [P, { name: 'index', type: 'i32' }], 'ChemistryEvent', READ, 'What happened in one of this tick’s events. An index outside `eventCount` is refused, naming both.'),
|
|
157
186
|
define('eventParcel', [P, { name: 'index', type: 'i32' }], 'i32', READ, 'Which parcel it happened to.'),
|
|
158
187
|
define('eventSpecies', [P, { name: 'index', type: 'i32' }], 'i32', READ, 'Which species, or -1 where the kind does not name one.'),
|
|
159
188
|
define('eventValue', [P, { name: 'index', type: 'i32' }], 'f32', READ, "The kind's own quantity."),
|
|
@@ -213,7 +242,7 @@ export function chemistryImplementation(services) {
|
|
|
213
242
|
charDepth: (_, parcel) => parcels.charDepthOf(parcel),
|
|
214
243
|
wetness: (_, parcel) => parcels.wetnessOf(parcel),
|
|
215
244
|
wettable: (_, parcel) => parcels.wettable(parcel),
|
|
216
|
-
phase: (_, parcel) => parcels.parcelPhaseOf(parcel),
|
|
245
|
+
phase: (_, parcel) => MATTER_PHASE.of[parcels.parcelPhaseOf(parcel)],
|
|
217
246
|
burning: (_, parcel) => parcels.burning(parcel),
|
|
218
247
|
smouldering: (_, parcel) => parcels.smouldering(parcel),
|
|
219
248
|
/* Watts inside, kilowatts out: a fire is quoted in kW everywhere a person reads one. */
|
|
@@ -247,7 +276,14 @@ export function chemistryImplementation(services) {
|
|
|
247
276
|
release: (_, s, kilograms, x, y, z) => air.addSpecies(x, y, z, s, kilograms),
|
|
248
277
|
addAirHeat: (_, joules, x, y, z) => air.addHeat(x, y, z, joules),
|
|
249
278
|
eventCount: () => world.events.count,
|
|
250
|
-
eventKind
|
|
279
|
+
eventKind(_, index) {
|
|
280
|
+
/* The buffer is read unchecked, and past `count` it holds the last tick's events or nothing:
|
|
281
|
+
a kind that did not happen this tick, or no variant at all. */
|
|
282
|
+
if (!(index >= 0 && index < world.events.count)) {
|
|
283
|
+
throw new RangeError(`event ${index} is outside this tick's ${world.events.count}`);
|
|
284
|
+
}
|
|
285
|
+
return CHEMISTRY_EVENT.of[world.events.kindOf(index)];
|
|
286
|
+
},
|
|
251
287
|
eventParcel: (_, index) => world.events.parcelOf(index),
|
|
252
288
|
eventSpecies: (_, index) => world.events.speciesOf(index),
|
|
253
289
|
eventValue: (_, index) => world.events.valueOf(index),
|
package/dist/bindings/core.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { ActionMap, CinematicCamera, ColliderSet, RemoteSaveStore, MessageQueue, PhysicsWorld, SceneNode, angleDelta, boxCollider, createRayHit, hashToUnit, mulberry32, wrapAngle, } from '@driftengine/core';
|
|
22
22
|
import { defineCapability } from 'driftscript';
|
|
23
|
+
import { hostEnum } from './variants.js';
|
|
23
24
|
export const TIME_MODULE = 'drift/time';
|
|
24
25
|
export const RANDOM_MODULE = 'drift/random';
|
|
25
26
|
export const EVENTS_MODULE = 'drift/events';
|
|
@@ -39,6 +40,16 @@ const define = (module, name, params, returns, effects, deterministic, doc) => d
|
|
|
39
40
|
doc,
|
|
40
41
|
implementation: `${module}.${name}`,
|
|
41
42
|
});
|
|
43
|
+
/** A contact event's kind, in `@driftengine/physics`'s order: `EVENT_ENTER`, `_STAY`, `_EXIT`. */
|
|
44
|
+
const CONTACT_KIND = hostEnum('drift/physics', 'ContactKind', ['Enter', 'Stay', 'Exit'], 'Whether two bodies began touching this step, are still touching, or have parted.');
|
|
45
|
+
/** A remote store's `SaveStatus`, one variant per string the store answers with. */
|
|
46
|
+
const SAVE_STATUS = hostEnum('drift/persistence', 'SaveStatus', ['Idle', 'Pending', 'Saving', 'Failed'], 'Whether a store’s writes have landed: idle, waiting to send, sending, or failed.');
|
|
47
|
+
const SAVE_STATUS_OF = {
|
|
48
|
+
idle: SAVE_STATUS.of[0],
|
|
49
|
+
pending: SAVE_STATUS.of[1],
|
|
50
|
+
saving: SAVE_STATUS.of[2],
|
|
51
|
+
failed: SAVE_STATUS.of[3],
|
|
52
|
+
};
|
|
42
53
|
/**
|
|
43
54
|
* Handles a script holds and passes back, never reads into.
|
|
44
55
|
*
|
|
@@ -86,6 +97,8 @@ export const CORE_TYPES = [
|
|
|
86
97
|
doc: 'A priority message queue with dedupe and a ceiling.',
|
|
87
98
|
},
|
|
88
99
|
{ module: PERSISTENCE_MODULE, name: 'Store', doc: 'A key-value store the consumer supplied.' },
|
|
100
|
+
CONTACT_KIND.type,
|
|
101
|
+
SAVE_STATUS.type,
|
|
89
102
|
];
|
|
90
103
|
/**
|
|
91
104
|
* `drift/time` — the loop's three clocks, and the one that is deterministic is not here.
|
|
@@ -161,7 +174,7 @@ export const PERSISTENCE_CAPABILITIES = [
|
|
|
161
174
|
* a script cannot hold. A consumer flushes from TypeScript, at the one place it knows it is
|
|
162
175
|
* leaving.
|
|
163
176
|
*/
|
|
164
|
-
define(PERSISTENCE_MODULE, 'saveStatus', [{ name: 'store', type: 'Store' }], '
|
|
177
|
+
define(PERSISTENCE_MODULE, 'saveStatus', [{ name: 'store', type: 'Store' }], 'SaveStatus', ['persistence.read'], false, 'Whether writes have landed: idle, pending, saving or failed. A store that writes synchronously is always idle, which is the true answer rather than a stub.'),
|
|
165
178
|
define(PERSISTENCE_MODULE, 'pendingSaves', [{ name: 'store', type: 'Store' }], 'u32', ['persistence.read'], false, 'How many keys are waiting to reach the backend. Zero for a store that writes synchronously.'),
|
|
166
179
|
];
|
|
167
180
|
/**
|
|
@@ -381,7 +394,7 @@ export const PHYSICS_CAPABILITIES = [
|
|
|
381
394
|
define(PHYSICS_MODULE, 'contactKind', [
|
|
382
395
|
{ name: 'world', type: 'PhysicsWorld' },
|
|
383
396
|
{ name: 'index', type: 'i32' },
|
|
384
|
-
], '
|
|
397
|
+
], 'ContactKind', ['physics.read'], true, "An event's kind: the two bodies began touching, are still touching, or have parted."),
|
|
385
398
|
define(PHYSICS_MODULE, 'contactA', [
|
|
386
399
|
{ name: 'world', type: 'PhysicsWorld' },
|
|
387
400
|
{ name: 'index', type: 'i32' },
|
|
@@ -599,7 +612,7 @@ export function persistenceImplementation() {
|
|
|
599
612
|
},
|
|
600
613
|
/* `instanceof` rather than a duck-typed property check: a store that happens to carry a
|
|
601
614
|
`status` field meaning something else would otherwise be read as this one. */
|
|
602
|
-
saveStatus: (store) => store instanceof RemoteSaveStore ? store.status : 'idle',
|
|
615
|
+
saveStatus: (store) => SAVE_STATUS_OF[store instanceof RemoteSaveStore ? store.status : 'idle'],
|
|
603
616
|
pendingSaves: (store) => (store instanceof RemoteSaveStore ? store.pending : 0),
|
|
604
617
|
};
|
|
605
618
|
}
|
|
@@ -778,7 +791,13 @@ export function physicsImplementation() {
|
|
|
778
791
|
hitNormalZ: (world) => f(hitOf(world).nz),
|
|
779
792
|
hitFraction: (world) => f(hitOf(world).fraction),
|
|
780
793
|
contactCount: (world) => world.events.count,
|
|
781
|
-
contactKind
|
|
794
|
+
contactKind(world, index) {
|
|
795
|
+
/* Past `count` the buffer holds an older step's events or nothing, so neither is answered. */
|
|
796
|
+
if (!(index >= 0 && index < world.events.count)) {
|
|
797
|
+
throw new RangeError(`contact ${index} is outside this step's ${world.events.count}`);
|
|
798
|
+
}
|
|
799
|
+
return CONTACT_KIND.of[world.events.data[index * 3]];
|
|
800
|
+
},
|
|
782
801
|
contactA: (world, index) => world.events.data[index * 3 + 1] ?? 0,
|
|
783
802
|
contactB: (world, index) => world.events.data[index * 3 + 2] ?? 0,
|
|
784
803
|
applyImpulse: (world, body, px, py, pz, atX, atY, atZ) => world.applyImpulse(body, px, py, pz, atX, atY, atZ),
|
|
@@ -32,13 +32,16 @@ export declare const EDITOR_MODULE = "drift/editor";
|
|
|
32
32
|
*
|
|
33
33
|
* ## Three capabilities where an argument would have done, deliberately
|
|
34
34
|
*
|
|
35
|
-
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`.
|
|
36
|
-
*
|
|
37
|
-
* options — change nothing, the silent no-op `host.ts`
|
|
38
|
-
* `AGENTS.md` rules out. Three names make the invalid call
|
|
39
|
-
* rather than a runtime branch is what refuses it.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
35
|
+
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`. When these were
|
|
36
|
+
* written a host could not declare an enum, so the argument would have been a string or a number,
|
|
37
|
+
* and an unrecognised one leaves exactly two options — change nothing, the silent no-op `host.ts`
|
|
38
|
+
* rules out, or throw inside a frame, which `AGENTS.md` rules out. Three names make the invalid call
|
|
39
|
+
* impossible to write, and the checker rather than a runtime branch is what refuses it. A
|
|
40
|
+
* `GizmoMode` argument would do the same now; the three stay because scripts call them. `space` is
|
|
41
|
+
* the same argument again.
|
|
42
|
+
*
|
|
43
|
+
* The *readers* answer variants, a `GizmoMode`, a `GizmoSpace` and a `GizmoHandle`, so a script
|
|
44
|
+
* that matches on one is told when a mode or a handle is added.
|
|
42
45
|
*/
|
|
43
46
|
export declare const EDITOR_TYPES: readonly OpaqueType[];
|
|
44
47
|
export declare const EDITOR_CAPABILITIES: readonly CapabilityDefinition[];
|
package/dist/bindings/editor.js
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { defineCapability } from 'driftscript';
|
|
2
|
+
import { hostEnum } from './variants.js';
|
|
2
3
|
export const EDITOR_MODULE = 'drift/editor';
|
|
4
|
+
/** Which tool the gizmo is, one variant per `Gizmo.mode`. */
|
|
5
|
+
const GIZMO_MODE = hostEnum(EDITOR_MODULE, 'GizmoMode', ['Translate', 'Rotate', 'Scale'], 'Which tool a gizmo is: it moves, turns or scales.');
|
|
6
|
+
/** Which axes the handles follow, one variant per `Gizmo.space`. */
|
|
7
|
+
const GIZMO_SPACE = hostEnum(EDITOR_MODULE, 'GizmoSpace', ['World', 'Local'], 'Which axes a gizmo’s handles point along: the world’s or the object’s own.');
|
|
8
|
+
/**
|
|
9
|
+
* The handle under the pointer, in the order of `Gizmo`'s integer constants, `None` first.
|
|
10
|
+
*
|
|
11
|
+
* A variant per handle where this used to answer a name such as `"translate.yz"`, which a script
|
|
12
|
+
* compared as a string: a misspelt name compared false forever and nothing said so. A handle added
|
|
13
|
+
* in the middle now changes this list, and every script `match` over it that misses it stops
|
|
14
|
+
* compiling.
|
|
15
|
+
*/
|
|
16
|
+
const GIZMO_HANDLE = hostEnum(EDITOR_MODULE, 'GizmoHandle', [
|
|
17
|
+
'None',
|
|
18
|
+
'TranslateX',
|
|
19
|
+
'TranslateY',
|
|
20
|
+
'TranslateZ',
|
|
21
|
+
'TranslateYZ',
|
|
22
|
+
'TranslateZX',
|
|
23
|
+
'TranslateXY',
|
|
24
|
+
'RotateX',
|
|
25
|
+
'RotateY',
|
|
26
|
+
'RotateZ',
|
|
27
|
+
'ScaleX',
|
|
28
|
+
'ScaleY',
|
|
29
|
+
'ScaleZ',
|
|
30
|
+
'ScaleUniform',
|
|
31
|
+
], 'Which handle the pointer is over, or `None`.');
|
|
32
|
+
/** What the editor is doing, one variant per `EditorMode`. */
|
|
33
|
+
const EDITOR_MODE = hostEnum(EDITOR_MODULE, 'EditorMode', ['Edit', 'Play', 'Paused'], 'Whether the editor is editing, playing the world, or paused part way through playing it.');
|
|
34
|
+
/** How the inspector shows a field, one variant per `FieldKind`. */
|
|
35
|
+
const FIELD_KIND = hostEnum(EDITOR_MODULE, 'FieldKind', ['Number', 'Integer', 'Boolean', 'Text', 'Entity', 'Enum'], 'How an inspector field is shown, taken from the type it was declared with.');
|
|
36
|
+
const byName = (names, of) => new Map(names.map((name, index) => [name, of[index]]));
|
|
37
|
+
const GIZMO_MODE_OF = byName(['translate', 'rotate', 'scale'], GIZMO_MODE.of);
|
|
38
|
+
const GIZMO_SPACE_OF = byName(['world', 'local'], GIZMO_SPACE.of);
|
|
39
|
+
const EDITOR_MODE_OF = byName(['edit', 'play', 'paused'], EDITOR_MODE.of);
|
|
40
|
+
const FIELD_KIND_OF = byName(['number', 'integer', 'boolean', 'text', 'entity', 'enum'], FIELD_KIND.of);
|
|
3
41
|
/**
|
|
4
42
|
* `drift/editor` — the transform tool, driven from a script that never ships.
|
|
5
43
|
*
|
|
@@ -32,13 +70,16 @@ export const EDITOR_MODULE = 'drift/editor';
|
|
|
32
70
|
*
|
|
33
71
|
* ## Three capabilities where an argument would have done, deliberately
|
|
34
72
|
*
|
|
35
|
-
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`.
|
|
36
|
-
*
|
|
37
|
-
* options — change nothing, the silent no-op `host.ts`
|
|
38
|
-
* `AGENTS.md` rules out. Three names make the invalid call
|
|
39
|
-
* rather than a runtime branch is what refuses it.
|
|
73
|
+
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`. When these were
|
|
74
|
+
* written a host could not declare an enum, so the argument would have been a string or a number,
|
|
75
|
+
* and an unrecognised one leaves exactly two options — change nothing, the silent no-op `host.ts`
|
|
76
|
+
* rules out, or throw inside a frame, which `AGENTS.md` rules out. Three names make the invalid call
|
|
77
|
+
* impossible to write, and the checker rather than a runtime branch is what refuses it. A
|
|
78
|
+
* `GizmoMode` argument would do the same now; the three stay because scripts call them. `space` is
|
|
79
|
+
* the same argument again.
|
|
40
80
|
*
|
|
41
|
-
* The *readers*
|
|
81
|
+
* The *readers* answer variants, a `GizmoMode`, a `GizmoSpace` and a `GizmoHandle`, so a script
|
|
82
|
+
* that matches on one is told when a mode or a handle is added.
|
|
42
83
|
*/
|
|
43
84
|
export const EDITOR_TYPES = [
|
|
44
85
|
{
|
|
@@ -51,6 +92,11 @@ export const EDITOR_TYPES = [
|
|
|
51
92
|
name: 'Editor',
|
|
52
93
|
doc: 'An editing session: what is selected, the tree of what exists, and whether the world is playing.',
|
|
53
94
|
},
|
|
95
|
+
GIZMO_MODE.type,
|
|
96
|
+
GIZMO_SPACE.type,
|
|
97
|
+
GIZMO_HANDLE.type,
|
|
98
|
+
EDITOR_MODE.type,
|
|
99
|
+
FIELD_KIND.type,
|
|
54
100
|
];
|
|
55
101
|
const define = (name, params, returns, doc) => defineCapability({
|
|
56
102
|
module: EDITOR_MODULE,
|
|
@@ -81,18 +127,19 @@ export const EDITOR_CAPABILITIES = [
|
|
|
81
127
|
define('translateMode', OF, 'void', 'Make this an arrow gizmo, which moves what it is on.'),
|
|
82
128
|
define('rotateMode', OF, 'void', 'Make this a ring gizmo, which turns what it is on.'),
|
|
83
129
|
define('scaleMode', OF, 'void', 'Make this a scale gizmo. Scale is always in the object’s own axes, whatever the space says, because a non-uniform scale along a world axis would shear it.'),
|
|
84
|
-
define('gizmoMode', OF, '
|
|
130
|
+
define('gizmoMode', OF, 'GizmoMode', 'Which of the three the gizmo is: translate, rotate or scale.'),
|
|
85
131
|
define('worldSpace', OF, 'void', 'Point the handles along the world axes.'),
|
|
86
132
|
define('localSpace', OF, 'void', 'Point the handles along the object’s own axes.'),
|
|
87
|
-
define('space', OF, '
|
|
133
|
+
define('space', OF, 'GizmoSpace', 'Which the handles are pointing along: the world or the object.'),
|
|
88
134
|
define('size', OF, 'f32', 'How big the gizmo is in metres. The host keeps this the same on screen as the camera moves.'),
|
|
89
135
|
define('setSize', [...OF, { name: 'metres', type: 'f32' }], 'void', 'Set how big the gizmo is in metres. Non-positive draws nothing and picks nothing rather than failing.'),
|
|
90
136
|
/*
|
|
91
|
-
* **A
|
|
92
|
-
*
|
|
93
|
-
*
|
|
137
|
+
* **A variant, and total.** The handles are integer constants in TypeScript, and a script comparing
|
|
138
|
+
* against a magic number breaks silently the day a handle is added in the middle; a script
|
|
139
|
+
* comparing against a name breaks silently the day it misspells one. `None` is a real answer
|
|
140
|
+
* rather than a missing one.
|
|
94
141
|
*/
|
|
95
|
-
define('hovered', OF, '
|
|
142
|
+
define('hovered', OF, 'GizmoHandle', 'What the pointer is over: `None`, or a handle such as `TranslateX`, `TranslateYZ`, `RotateY` or `ScaleUniform`.'),
|
|
96
143
|
define('dragging', OF, 'bool', 'Whether a drag is running.'),
|
|
97
144
|
define('dragAngle', OF, 'f32', 'How far a rotation drag has turned, in radians, signed and past a full turn if it went that far. Zero when the drag is not a rotation. This is what to snap or clamp; the rotation itself cannot tell a three-quarter turn from a quarter turn back.'),
|
|
98
145
|
define('positionX', OF, 'f32', 'Where the thing being edited is, along x.'),
|
|
@@ -116,7 +163,7 @@ export const EDITOR_CAPABILITIES = [
|
|
|
116
163
|
* **The session, from `@driftengine/editor`.** Everything above drives one tool; everything below
|
|
117
164
|
* drives the editor around it — which is what an `@editor` script is actually for.
|
|
118
165
|
*/
|
|
119
|
-
define('mode', ON, '
|
|
166
|
+
define('mode', ON, 'EditorMode', 'What the editor is doing: editing, playing or paused.'),
|
|
120
167
|
define('playing', ON, 'bool', 'Whether the world is advancing. False while paused and while editing.'),
|
|
121
168
|
define('play', ON, 'bool', 'Start playing, taking a snapshot of the world first so `stop` can put it back. Resuming from paused does not re-snapshot. False when no world is bound to snapshot.'),
|
|
122
169
|
define('pause', ON, 'void', 'Hold the world where it is. The frame keeps drawing.'),
|
|
@@ -133,31 +180,10 @@ export const EDITOR_CAPABILITIES = [
|
|
|
133
180
|
define('fieldCount', ON, 'i32', 'How many fields the inspector is showing.'),
|
|
134
181
|
define('fieldLabel', [...ON, { name: 'field', type: 'i32' }], 'String', 'A field\u2019s name as it was declared, or "" for a field that does not exist.'),
|
|
135
182
|
define('fieldGroup', [...ON, { name: 'field', type: 'i32' }], 'String', 'What a field belongs to: a component\u2019s name, or "transform" for a node.'),
|
|
136
|
-
define('fieldKind', [...ON, { name: 'field', type: 'i32' }], '
|
|
183
|
+
define('fieldKind', [...ON, { name: 'field', type: 'i32' }], 'FieldKind', 'How to show a field: a number, an integer, a boolean, text, an entity or an enum. Taken from the declared type, because a read cannot tell them apart. A field outside `fieldCount` is refused, naming both.'),
|
|
137
184
|
define('fieldNumber', [...ON, { name: 'field', type: 'i32' }], 'f32', 'A field\u2019s value as a number. Zero for a field that is not one, and for one that does not exist.'),
|
|
138
185
|
define('setFieldNumber', [...ON, { name: 'field', type: 'i32' }, { name: 'value', type: 'f32' }], 'bool', 'Write a number into a field. False where the field does not exist or will not take one.'),
|
|
139
186
|
];
|
|
140
|
-
/**
|
|
141
|
-
* Handle constants as the names a script reads, in the order the constants run.
|
|
142
|
-
*
|
|
143
|
-
* A table rather than a `switch`, so adding a handle is one line here and the reader stays total.
|
|
144
|
-
*/
|
|
145
|
-
const HANDLE_NAMES = [
|
|
146
|
-
'none',
|
|
147
|
-
'translate.x',
|
|
148
|
-
'translate.y',
|
|
149
|
-
'translate.z',
|
|
150
|
-
'translate.yz',
|
|
151
|
-
'translate.zx',
|
|
152
|
-
'translate.xy',
|
|
153
|
-
'rotate.x',
|
|
154
|
-
'rotate.y',
|
|
155
|
-
'rotate.z',
|
|
156
|
-
'scale.x',
|
|
157
|
-
'scale.y',
|
|
158
|
-
'scale.z',
|
|
159
|
-
'scale.uniform',
|
|
160
|
-
];
|
|
161
187
|
/**
|
|
162
188
|
* **No services, and that is the shape rather than an omission.**
|
|
163
189
|
*
|
|
@@ -177,19 +203,19 @@ export function editorImplementation() {
|
|
|
177
203
|
scaleMode: (gizmo) => {
|
|
178
204
|
gizmo.mode = 'scale';
|
|
179
205
|
},
|
|
180
|
-
gizmoMode: (gizmo) => gizmo.mode,
|
|
206
|
+
gizmoMode: (gizmo) => GIZMO_MODE_OF.get(gizmo.mode),
|
|
181
207
|
worldSpace: (gizmo) => {
|
|
182
208
|
gizmo.space = 'world';
|
|
183
209
|
},
|
|
184
210
|
localSpace: (gizmo) => {
|
|
185
211
|
gizmo.space = 'local';
|
|
186
212
|
},
|
|
187
|
-
space: (gizmo) => gizmo.space,
|
|
213
|
+
space: (gizmo) => GIZMO_SPACE_OF.get(gizmo.space),
|
|
188
214
|
size: (gizmo) => gizmo.size,
|
|
189
215
|
setSize: (gizmo, metres) => {
|
|
190
216
|
gizmo.size = metres;
|
|
191
217
|
},
|
|
192
|
-
hovered: (gizmo) =>
|
|
218
|
+
hovered: (gizmo) => GIZMO_HANDLE.of[gizmo.hovered] ?? GIZMO_HANDLE.of[0],
|
|
193
219
|
dragging: (gizmo) => gizmo.dragging,
|
|
194
220
|
dragAngle: (gizmo) => gizmo.dragAngle,
|
|
195
221
|
positionX: (gizmo) => gizmo.position[0] ?? 0,
|
|
@@ -207,7 +233,7 @@ export function editorImplementation() {
|
|
|
207
233
|
rotationY: (gizmo) => gizmo.rotation[1] ?? 0,
|
|
208
234
|
rotationZ: (gizmo) => gizmo.rotation[2] ?? 0,
|
|
209
235
|
rotationW: (gizmo) => gizmo.rotation[3] ?? 1,
|
|
210
|
-
mode: (editor) => editor.mode,
|
|
236
|
+
mode: (editor) => EDITOR_MODE_OF.get(editor.mode),
|
|
211
237
|
playing: (editor) => editor.mode === 'play',
|
|
212
238
|
play: (editor) => editor.play(),
|
|
213
239
|
pause: (editor) => {
|
|
@@ -238,7 +264,13 @@ export function editorImplementation() {
|
|
|
238
264
|
fieldCount: (editor) => editor.inspector.fields.length,
|
|
239
265
|
fieldLabel: (editor, field) => editor.inspector.fields[field]?.label ?? '',
|
|
240
266
|
fieldGroup: (editor, field) => editor.inspector.fields[field]?.group ?? '',
|
|
241
|
-
fieldKind
|
|
267
|
+
fieldKind(editor, field) {
|
|
268
|
+
const found = editor.inspector.fields[field];
|
|
269
|
+
if (found === undefined) {
|
|
270
|
+
throw new RangeError(`field ${field} is outside the ${editor.inspector.fields.length} shown`);
|
|
271
|
+
}
|
|
272
|
+
return FIELD_KIND_OF.get(found.kind);
|
|
273
|
+
},
|
|
242
274
|
fieldNumber: (editor, field) => {
|
|
243
275
|
const value = editor.inspector.fields[field]?.value;
|
|
244
276
|
return typeof value === 'number' ? value : 0;
|
|
@@ -221,10 +221,18 @@ export function entitiesImplementation(services) {
|
|
|
221
221
|
attach: (w, entity, component) => w.add(entity, resolve(component)),
|
|
222
222
|
detach: (w, entity, component) => w.remove(entity, resolve(component)),
|
|
223
223
|
read: (w, entity, component, field) => {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
const type = resolve(component);
|
|
225
|
+
const value = w.read(entity, type, field);
|
|
226
|
+
if (value !== undefined)
|
|
227
|
+
return value;
|
|
228
|
+
/*
|
|
229
|
+
* Nothing to read: an absent optional field, or an entity without the component. An optional
|
|
230
|
+
* field answers `undefined`, which the generated code turns into `none`; the compiler wraps
|
|
231
|
+
* every read of one. Any other field answers zero, since a script's `f64` cannot hold
|
|
232
|
+
* nothing. Answering zero for an optional field too was what made it unreadable: `none` and
|
|
233
|
+
* a stored zero were the same answer.
|
|
234
|
+
*/
|
|
235
|
+
return optionalField(type, field) ? undefined : 0;
|
|
228
236
|
},
|
|
229
237
|
write: (w, entity, component, field, value) => w.write(entity, resolve(component), field, value),
|
|
230
238
|
/*
|
|
@@ -342,3 +350,7 @@ export function entitiesImplementation(services) {
|
|
|
342
350
|
},
|
|
343
351
|
};
|
|
344
352
|
}
|
|
353
|
+
/** Whether a component declared this field as an option, `option:<inner>` in its schema. */
|
|
354
|
+
function optionalField(type, field) {
|
|
355
|
+
return type.schema.fields.some((f) => f.name === field && f.type.startsWith('option:'));
|
|
356
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host enum's variants, as the values a script holds and hands back.
|
|
3
|
+
*
|
|
4
|
+
* An engine answer that is one of a few things is an enum on the script's side: DriftScript lets a
|
|
5
|
+
* host declare one (`OpaqueType.variants`), a script names its variants and a `match` over it has
|
|
6
|
+
* to cover them all. The engine keeps its own numbering, and this is the one place a number becomes
|
|
7
|
+
* a variant and back. Before it, each such capability answered with the number and a sentence
|
|
8
|
+
* saying what each number meant, and every script restated that sentence as constants.
|
|
9
|
+
*
|
|
10
|
+
* One frozen object per variant, made once, so answering allocates nothing. A script compares a
|
|
11
|
+
* variant by its tag, so a script's own copy of `{ tag: 'Solid' }` equals this one.
|
|
12
|
+
*/
|
|
13
|
+
import type { OpaqueType } from 'driftscript';
|
|
14
|
+
export interface Variant {
|
|
15
|
+
readonly tag: string;
|
|
16
|
+
}
|
|
17
|
+
export interface HostEnum {
|
|
18
|
+
/** The type to register, so a script can name it. */
|
|
19
|
+
readonly type: OpaqueType;
|
|
20
|
+
/** The variant for an engine code, in the order the engine numbers them. */
|
|
21
|
+
readonly of: readonly Variant[];
|
|
22
|
+
/** The engine's code for a variant a script handed back, or -1 for anything else. */
|
|
23
|
+
code(variant: Variant): number;
|
|
24
|
+
}
|
|
25
|
+
export declare function hostEnum(module: string, name: string, variants: readonly string[], doc: string): HostEnum;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function hostEnum(module, name, variants, doc) {
|
|
2
|
+
const of = variants.map((tag) => Object.freeze({ tag }));
|
|
3
|
+
const codes = new Map(variants.map((tag, index) => [tag, index]));
|
|
4
|
+
return {
|
|
5
|
+
type: { module, name, doc, variants },
|
|
6
|
+
of,
|
|
7
|
+
code: (variant) => codes.get(variant.tag) ?? -1,
|
|
8
|
+
};
|
|
9
|
+
}
|
package/dist/bindings/xr.js
CHANGED
|
@@ -36,9 +36,22 @@
|
|
|
36
36
|
* that reason, which is the one this needs.
|
|
37
37
|
*/
|
|
38
38
|
import { defineCapability } from 'driftscript';
|
|
39
|
-
import {
|
|
39
|
+
import { hostEnum } from './variants.js';
|
|
40
|
+
import { HAND_JOINTS, JOINT_INDEX } from '@driftengine/xr';
|
|
40
41
|
export const XR_MODULE = 'drift/xr';
|
|
41
|
-
|
|
42
|
+
/** Which hand, one variant per `Handedness`; `None` is the source a runtime gives no side. */
|
|
43
|
+
const HAND = hostEnum(XR_MODULE, 'Hand', ['Left', 'Right', 'None'], 'Which hand: left, right, or the input source with no side.');
|
|
44
|
+
const HANDEDNESS = ['left', 'right', 'none'];
|
|
45
|
+
/**
|
|
46
|
+
* A hand joint, one variant per name in `@driftengine/xr`'s `HAND_JOINTS`, in its order:
|
|
47
|
+
* `index-finger-tip` is `IndexFingerTip`. Derived from that list, so a joint the package adds is a
|
|
48
|
+
* variant here with nothing else to change, and a script cannot ask for one it misspelt.
|
|
49
|
+
*/
|
|
50
|
+
const HAND_JOINT = hostEnum(XR_MODULE, 'HandJoint', HAND_JOINTS.map((name) => name
|
|
51
|
+
.split('-')
|
|
52
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
53
|
+
.join('')), 'One of the twenty-five joints WebXR tracks on a hand, from the wrist to each fingertip.');
|
|
54
|
+
export const XR_TYPES = [HAND.type, HAND_JOINT.type];
|
|
42
55
|
const define = (name, params, returns, effects, doc, implementation) => defineCapability({
|
|
43
56
|
module: XR_MODULE,
|
|
44
57
|
name,
|
|
@@ -54,8 +67,8 @@ const define = (name, params, returns, effects, doc, implementation) => defineCa
|
|
|
54
67
|
doc,
|
|
55
68
|
implementation,
|
|
56
69
|
});
|
|
57
|
-
|
|
58
|
-
const
|
|
70
|
+
const HAND_PARAM = { name: 'hand', type: 'Hand' };
|
|
71
|
+
const JOINT_PARAM = { name: 'joint', type: 'HandJoint' };
|
|
59
72
|
export const XR_CAPABILITIES = [
|
|
60
73
|
define('presenting', [], 'bool', ['scene.read'], 'Whether a session is running. False before one starts and after the user takes the headset off.', 'XrRuntime.presenting'),
|
|
61
74
|
define('supported', [], 'bool', ['scene.read'], 'Whether this device could present at all. False on a machine with no headset attached.', 'XrRuntime.supported'),
|
|
@@ -76,9 +89,9 @@ export const XR_CAPABILITIES = [
|
|
|
76
89
|
define('headX', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headX'),
|
|
77
90
|
define('headY', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headY'),
|
|
78
91
|
define('headZ', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headZ'),
|
|
79
|
-
define('trigger', [
|
|
80
|
-
define('squeeze', [
|
|
81
|
-
define('holding', [
|
|
92
|
+
define('trigger', [HAND_PARAM], 'f32', ['input.read'], 'How far a hand’s trigger is pulled, 0 to 1. Zero for a hand that is not there.', 'XrRuntime.trigger'),
|
|
93
|
+
define('squeeze', [HAND_PARAM], 'f32', ['input.read'], 'How far a hand’s grip is squeezed, 0 to 1. Zero for a hand that is not there.', 'XrRuntime.squeeze'),
|
|
94
|
+
define('holding', [HAND_PARAM], 'bool', ['input.read'], 'Whether a hand is tracked this frame. False while a controller is set down or out of view.', 'XrRuntime.holding'),
|
|
82
95
|
/*
|
|
83
96
|
* A joint by name and not by index.
|
|
84
97
|
*
|
|
@@ -86,15 +99,15 @@ export const XR_CAPABILITIES = [
|
|
|
86
99
|
* number a script would have to keep its own copy of and watch drift against a specification.
|
|
87
100
|
* A name is checked at the call and answers false for one nobody has heard of.
|
|
88
101
|
*/
|
|
89
|
-
define('jointX', [
|
|
90
|
-
define('jointY', [
|
|
91
|
-
define('jointZ', [
|
|
92
|
-
define('pinching', [
|
|
102
|
+
define('jointX', [HAND_PARAM, JOINT_PARAM], 'f32', ['input.read'], 'Where a hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointX'),
|
|
103
|
+
define('jointY', [HAND_PARAM, JOINT_PARAM], 'f32', ['input.read'], 'Where a hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointY'),
|
|
104
|
+
define('jointZ', [HAND_PARAM, JOINT_PARAM], 'f32', ['input.read'], 'Where a hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointZ'),
|
|
105
|
+
define('pinching', [HAND_PARAM], 'bool', ['input.read'], 'Whether a hand’s thumb and index tips are close enough to count as a pinch.', 'XrRuntime.pinching'),
|
|
93
106
|
];
|
|
94
107
|
/** How close two fingertips count as a pinch, in metres. */
|
|
95
108
|
const PINCH_METRES = 0.025;
|
|
96
|
-
function handOf(
|
|
97
|
-
return
|
|
109
|
+
function handOf(hand) {
|
|
110
|
+
return HANDEDNESS[HAND.code(hand)] ?? 'none';
|
|
98
111
|
}
|
|
99
112
|
export function xrImplementation(runtime) {
|
|
100
113
|
const at = new Float32Array(3);
|
|
@@ -107,7 +120,7 @@ export function xrImplementation(runtime) {
|
|
|
107
120
|
/* The flat arrays the package already fills are read directly. `jointPosition` would answer the
|
|
108
121
|
same question through a `Float32Array` this would then have to unpack, three times per joint
|
|
109
122
|
per frame. */
|
|
110
|
-
const index =
|
|
123
|
+
const index = HAND_JOINT.code(name);
|
|
111
124
|
if (index < 0 || skeleton.tracked[index] !== 1)
|
|
112
125
|
return 0;
|
|
113
126
|
return skeleton.matrices[index * 16 + 12 + axis] ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/script",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "The drift/* capability bindings: what this engine provides to DriftScript, and the only place the two are coupled",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,20 +28,20 @@
|
|
|
28
28
|
],
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"driftscript": "1.
|
|
31
|
+
"driftscript": "1.16.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/ai": "4.
|
|
35
|
-
"@driftengine/animation": "4.
|
|
36
|
-
"@driftengine/audio": "4.
|
|
37
|
-
"@driftengine/chemistry": "4.
|
|
38
|
-
"@driftengine/core": "4.
|
|
39
|
-
"@driftengine/editor": "4.
|
|
40
|
-
"@driftengine/entities": "4.
|
|
41
|
-
"@driftengine/network": "4.
|
|
42
|
-
"@driftengine/terrain": "4.
|
|
43
|
-
"@driftengine/ui2d": "4.
|
|
44
|
-
"@driftengine/xr": "4.
|
|
34
|
+
"@driftengine/ai": "4.8.0",
|
|
35
|
+
"@driftengine/animation": "4.8.0",
|
|
36
|
+
"@driftengine/audio": "4.8.0",
|
|
37
|
+
"@driftengine/chemistry": "4.8.0",
|
|
38
|
+
"@driftengine/core": "4.8.0",
|
|
39
|
+
"@driftengine/editor": "4.8.0",
|
|
40
|
+
"@driftengine/entities": "4.8.0",
|
|
41
|
+
"@driftengine/network": "4.8.0",
|
|
42
|
+
"@driftengine/terrain": "4.8.0",
|
|
43
|
+
"@driftengine/ui2d": "4.8.0",
|
|
44
|
+
"@driftengine/xr": "4.8.0"
|
|
45
45
|
},
|
|
46
46
|
"author": "Drift Technologies",
|
|
47
47
|
"repository": {
|