@vulfram/engine 0.14.8-alpha → 0.17.1-alpha
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 +106 -0
- package/package.json +55 -4
- package/src/core.ts +14 -0
- package/src/ecs.ts +1 -0
- package/src/engine/api.ts +234 -23
- package/src/engine/bridge/dispatch.ts +265 -40
- package/src/engine/bridge/guards.ts +4 -1
- package/src/engine/bridge/protocol.ts +69 -52
- package/src/engine/ecs/index.ts +185 -42
- package/src/engine/state.ts +133 -2
- package/src/engine/systems/command-intent.ts +153 -3
- package/src/engine/systems/constraint-solve.ts +167 -0
- package/src/engine/systems/core-command-builder.ts +9 -268
- package/src/engine/systems/diagnostics.ts +20 -19
- package/src/engine/systems/index.ts +3 -1
- package/src/engine/systems/input-mirror.ts +101 -3
- package/src/engine/systems/resource-upload.ts +96 -44
- package/src/engine/systems/response-decode.ts +69 -15
- package/src/engine/systems/scene-sync.ts +306 -0
- package/src/engine/systems/ui-bridge.ts +360 -0
- package/src/engine/systems/utils.ts +43 -1
- package/src/engine/systems/world-lifecycle.ts +37 -102
- package/src/engine/window/manager.ts +168 -0
- package/src/engine/world/entities.ts +821 -78
- package/src/engine/world/mount.ts +174 -0
- package/src/engine/world/types.ts +71 -0
- package/src/engine/world/world-ui.ts +266 -0
- package/src/engine/world/world3d.ts +280 -0
- package/src/index.ts +30 -1
- package/src/mount.ts +2 -0
- package/src/types/cmds/audio.ts +73 -48
- package/src/types/cmds/camera.ts +12 -8
- package/src/types/cmds/environment.ts +9 -3
- package/src/types/cmds/geometry.ts +13 -14
- package/src/types/cmds/index.ts +198 -168
- package/src/types/cmds/light.ts +12 -11
- package/src/types/cmds/material.ts +9 -11
- package/src/types/cmds/model.ts +17 -15
- package/src/types/cmds/realm.ts +25 -0
- package/src/types/cmds/system.ts +19 -0
- package/src/types/cmds/target.ts +82 -0
- package/src/types/cmds/texture.ts +13 -3
- package/src/types/cmds/ui.ts +220 -0
- package/src/types/cmds/window.ts +41 -204
- package/src/types/events/index.ts +4 -1
- package/src/types/events/pointer.ts +42 -13
- package/src/types/events/system.ts +144 -30
- package/src/types/events/ui.ts +21 -0
- package/src/types/index.ts +1 -0
- package/src/types/json.ts +15 -0
- package/src/window.ts +8 -0
- package/src/world-ui.ts +2 -0
- package/src/world3d.ts +10 -0
- package/tsconfig.json +0 -29
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { mat4 } from 'gl-matrix';
|
|
2
|
+
import type { ParentComponent, System, TransformComponent } from '../ecs';
|
|
3
|
+
import { mat4EqualsApprox, getEntityLocalTransformMatrix } from './utils';
|
|
4
|
+
|
|
5
|
+
type ConstraintStrategyContext = {
|
|
6
|
+
worldId: number;
|
|
7
|
+
entityId: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface ConstraintStrategy {
|
|
11
|
+
name: string;
|
|
12
|
+
apply(a: mat4, b: mat4, context: ConstraintStrategyContext): mat4;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ParentConstraintStrategy: ConstraintStrategy = {
|
|
16
|
+
name: 'parent',
|
|
17
|
+
apply(a, b) {
|
|
18
|
+
const out = mat4.create();
|
|
19
|
+
mat4.multiply(out, a, b);
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ResolveState = {
|
|
25
|
+
worldId: number;
|
|
26
|
+
world: Parameters<System>[0];
|
|
27
|
+
affected: Set<number>;
|
|
28
|
+
resolved: Map<number, mat4>;
|
|
29
|
+
visiting: Set<number>;
|
|
30
|
+
cycleLogged: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function resolveEntityMatrix(state: ResolveState, entityId: number): mat4 {
|
|
34
|
+
if (!state.affected.has(entityId)) {
|
|
35
|
+
const existing = state.world.resolvedEntityTransforms.get(entityId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
return existing as unknown as mat4;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cached = state.resolved.get(entityId);
|
|
42
|
+
if (cached) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (state.visiting.has(entityId)) {
|
|
47
|
+
if (!state.cycleLogged) {
|
|
48
|
+
console.error(
|
|
49
|
+
`[World ${state.worldId}] Constraint cycle detected in parent hierarchy. Falling back to local transforms for cyclic nodes.`,
|
|
50
|
+
);
|
|
51
|
+
state.cycleLogged = true;
|
|
52
|
+
}
|
|
53
|
+
return getEntityLocalTransformMatrix(state.world, entityId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
state.visiting.add(entityId);
|
|
57
|
+
const local = getEntityLocalTransformMatrix(state.world, entityId);
|
|
58
|
+
const store = state.world.components.get(entityId);
|
|
59
|
+
const parent = store?.get('Parent') as ParentComponent | undefined;
|
|
60
|
+
|
|
61
|
+
let resolved = local;
|
|
62
|
+
if (parent) {
|
|
63
|
+
const parentStore = state.world.components.get(parent.parentId);
|
|
64
|
+
const parentTransform = parentStore?.get('Transform') as
|
|
65
|
+
| TransformComponent
|
|
66
|
+
| undefined;
|
|
67
|
+
if (parentTransform) {
|
|
68
|
+
const parentMatrix = resolveEntityMatrix(state, parent.parentId);
|
|
69
|
+
resolved = ParentConstraintStrategy.apply(parentMatrix, local, {
|
|
70
|
+
worldId: state.worldId,
|
|
71
|
+
entityId,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
state.visiting.delete(entityId);
|
|
77
|
+
state.resolved.set(entityId, resolved);
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectAffectedEntities(world: Parameters<System>[0]): Set<number> {
|
|
82
|
+
const affected = new Set<number>();
|
|
83
|
+
const queue: number[] = [];
|
|
84
|
+
|
|
85
|
+
for (const entityId of world.constraintDirtyEntities) {
|
|
86
|
+
affected.add(entityId);
|
|
87
|
+
queue.push(entityId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < queue.length; i++) {
|
|
91
|
+
const parentId = queue[i]!;
|
|
92
|
+
const children = world.constraintChildrenByParent.get(parentId);
|
|
93
|
+
if (!children) continue;
|
|
94
|
+
for (const childId of children) {
|
|
95
|
+
if (affected.has(childId)) continue;
|
|
96
|
+
affected.add(childId);
|
|
97
|
+
queue.push(childId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return affected;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolves transform constraints into world matrices.
|
|
106
|
+
*
|
|
107
|
+
* Current built-in strategy:
|
|
108
|
+
* - `parent`: composes parent world matrix with child local matrix (`A * B`).
|
|
109
|
+
*
|
|
110
|
+
* The solver walks dependencies from higher nodes to leaves and caches each
|
|
111
|
+
* resolved entity matrix so render sync can emit final transforms to core.
|
|
112
|
+
*/
|
|
113
|
+
export const ConstraintSolveSystem: System = (world, context) => {
|
|
114
|
+
if (world.constraintDirtyEntities.size === 0) {
|
|
115
|
+
world.constraintChangedEntities.clear();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const matrices = world.resolvedEntityTransforms;
|
|
120
|
+
const changed = world.constraintChangedEntities;
|
|
121
|
+
changed.clear();
|
|
122
|
+
const affected = collectAffectedEntities(world);
|
|
123
|
+
|
|
124
|
+
const resolveState: ResolveState = {
|
|
125
|
+
worldId: context.worldId,
|
|
126
|
+
world,
|
|
127
|
+
affected,
|
|
128
|
+
resolved: world.constraintScratchResolved,
|
|
129
|
+
visiting: world.constraintScratchVisiting,
|
|
130
|
+
cycleLogged: false,
|
|
131
|
+
};
|
|
132
|
+
resolveState.resolved.clear();
|
|
133
|
+
resolveState.visiting.clear();
|
|
134
|
+
|
|
135
|
+
for (const entityId of affected) {
|
|
136
|
+
if (!world.entities.has(entityId)) {
|
|
137
|
+
if (matrices.delete(entityId)) {
|
|
138
|
+
changed.add(entityId);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const store = world.components.get(entityId);
|
|
144
|
+
const transform = store?.get('Transform') as TransformComponent | undefined;
|
|
145
|
+
if (!transform) {
|
|
146
|
+
if (matrices.delete(entityId)) {
|
|
147
|
+
changed.add(entityId);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const resolved = resolveEntityMatrix(resolveState, entityId);
|
|
153
|
+
const previousSnapshot = matrices.get(entityId);
|
|
154
|
+
if (!previousSnapshot) {
|
|
155
|
+
matrices.set(entityId, new Float32Array(resolved));
|
|
156
|
+
changed.add(entityId);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!mat4EqualsApprox(previousSnapshot, resolved)) {
|
|
161
|
+
previousSnapshot.set(resolved);
|
|
162
|
+
changed.add(entityId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
world.constraintDirtyEntities.clear();
|
|
167
|
+
};
|
|
@@ -1,268 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
TransformComponent,
|
|
11
|
-
} from '../ecs';
|
|
12
|
-
import { getEntityTransformMatrix, toQuat, toVec2, toVec3, toVec4 } from './utils';
|
|
13
|
-
|
|
14
|
-
export const CoreCommandBuilderSystem: System = (world, context) => {
|
|
15
|
-
const intentsToRemove: number[] = [];
|
|
16
|
-
|
|
17
|
-
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
18
|
-
const intent = world.pendingIntents[i];
|
|
19
|
-
if (!intent) continue;
|
|
20
|
-
|
|
21
|
-
if (intent.type === 'attach-model') {
|
|
22
|
-
const modelId = world.nextCoreId++;
|
|
23
|
-
const transform = getEntityTransformMatrix(world, intent.entityId);
|
|
24
|
-
const castShadow = intent.props.castShadow ?? true;
|
|
25
|
-
const receiveShadow = intent.props.receiveShadow ?? true;
|
|
26
|
-
const castOutline = intent.props.castOutline ?? false;
|
|
27
|
-
const outlineColor = intent.props.outlineColor ?? ([0, 0, 0, 0] as const);
|
|
28
|
-
|
|
29
|
-
enqueueCommand(context.worldId, 'cmd-model-create', {
|
|
30
|
-
windowId: context.worldId,
|
|
31
|
-
modelId,
|
|
32
|
-
geometryId: intent.props.geometryId,
|
|
33
|
-
materialId: intent.props.materialId,
|
|
34
|
-
transform: Array.from(transform),
|
|
35
|
-
layerMask: 0x7fffffff,
|
|
36
|
-
castShadow,
|
|
37
|
-
receiveShadow,
|
|
38
|
-
castOutline,
|
|
39
|
-
outlineColor: [...outlineColor] as [number, number, number, number],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let store = world.components.get(intent.entityId);
|
|
43
|
-
if (!store) {
|
|
44
|
-
store = new Map();
|
|
45
|
-
world.components.set(intent.entityId, store);
|
|
46
|
-
}
|
|
47
|
-
store.set('Model', {
|
|
48
|
-
type: 'Model',
|
|
49
|
-
id: modelId,
|
|
50
|
-
geometryId: intent.props.geometryId,
|
|
51
|
-
materialId: intent.props.materialId,
|
|
52
|
-
castShadow: intent.props.castShadow ?? true,
|
|
53
|
-
receiveShadow: intent.props.receiveShadow ?? true,
|
|
54
|
-
castOutline,
|
|
55
|
-
outlineColor: [...outlineColor] as [number, number, number, number],
|
|
56
|
-
skipUpdate: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
intentsToRemove.push(i);
|
|
60
|
-
} else if (intent.type === 'attach-camera') {
|
|
61
|
-
const cameraId = world.nextCoreId++;
|
|
62
|
-
const transform = getEntityTransformMatrix(world, intent.entityId);
|
|
63
|
-
|
|
64
|
-
enqueueCommand(context.worldId, 'cmd-camera-create', {
|
|
65
|
-
cameraId,
|
|
66
|
-
label: `Cam ${cameraId}`,
|
|
67
|
-
kind: intent.props.kind ?? ('perspective' as CameraKind),
|
|
68
|
-
flags: 0,
|
|
69
|
-
nearFar: [intent.props.near ?? 0.1, intent.props.far ?? 1000],
|
|
70
|
-
order: intent.props.order ?? 0,
|
|
71
|
-
transform: Array.from(transform),
|
|
72
|
-
layerMask: 0x7fffffff,
|
|
73
|
-
orthoScale: 1.0,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
let store = world.components.get(intent.entityId);
|
|
77
|
-
if (!store) {
|
|
78
|
-
store = new Map();
|
|
79
|
-
world.components.set(intent.entityId, store);
|
|
80
|
-
}
|
|
81
|
-
store.set('Camera', {
|
|
82
|
-
type: 'Camera',
|
|
83
|
-
id: cameraId,
|
|
84
|
-
kind: intent.props.kind ?? ('perspective' as CameraKind),
|
|
85
|
-
near: intent.props.near ?? 0.1,
|
|
86
|
-
far: intent.props.far ?? 1000,
|
|
87
|
-
order: intent.props.order ?? 0,
|
|
88
|
-
orthoScale: 1.0,
|
|
89
|
-
skipUpdate: true,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
intentsToRemove.push(i);
|
|
93
|
-
} else if (intent.type === 'attach-light') {
|
|
94
|
-
const lightId = world.nextCoreId++;
|
|
95
|
-
const transform = getEntityTransformMatrix(world, intent.entityId);
|
|
96
|
-
|
|
97
|
-
const pos = vec3.create();
|
|
98
|
-
mat4.getTranslation(pos, transform);
|
|
99
|
-
const direction = intent.props.direction
|
|
100
|
-
? toVec3(intent.props.direction)
|
|
101
|
-
: ([0, 0, -1] as [number, number, number]);
|
|
102
|
-
const color = intent.props.color
|
|
103
|
-
? toVec3(intent.props.color)
|
|
104
|
-
: ([1, 1, 1] as [number, number, number]);
|
|
105
|
-
|
|
106
|
-
const lightCmd: {
|
|
107
|
-
windowId: number;
|
|
108
|
-
lightId: number;
|
|
109
|
-
kind: LightKind;
|
|
110
|
-
color: [number, number, number, number];
|
|
111
|
-
intensity: number;
|
|
112
|
-
range: number;
|
|
113
|
-
castShadow: boolean;
|
|
114
|
-
position: [number, number, number, number];
|
|
115
|
-
layerMask: number;
|
|
116
|
-
direction?: [number, number, number, number];
|
|
117
|
-
spotInnerOuter?: [number, number];
|
|
118
|
-
} = {
|
|
119
|
-
windowId: context.worldId,
|
|
120
|
-
lightId,
|
|
121
|
-
kind: intent.props.kind ?? ('directional' as LightKind),
|
|
122
|
-
color: [...color, 1] as [number, number, number, number],
|
|
123
|
-
intensity: intent.props.intensity ?? 1.0,
|
|
124
|
-
range: intent.props.range ?? 10.0,
|
|
125
|
-
castShadow: intent.props.castShadow ?? true,
|
|
126
|
-
position: [pos[0], pos[1], pos[2], 1],
|
|
127
|
-
layerMask: 0x7fffffff,
|
|
128
|
-
};
|
|
129
|
-
if (direction) {
|
|
130
|
-
const [dirX, dirY, dirZ] = direction;
|
|
131
|
-
lightCmd.direction = [dirX, dirY, dirZ, 0];
|
|
132
|
-
}
|
|
133
|
-
if (intent.props.spotInnerOuter) {
|
|
134
|
-
lightCmd.spotInnerOuter = toVec2(intent.props.spotInnerOuter);
|
|
135
|
-
}
|
|
136
|
-
enqueueCommand(context.worldId, 'cmd-light-create', lightCmd);
|
|
137
|
-
|
|
138
|
-
let store = world.components.get(intent.entityId);
|
|
139
|
-
if (!store) {
|
|
140
|
-
store = new Map();
|
|
141
|
-
world.components.set(intent.entityId, store);
|
|
142
|
-
}
|
|
143
|
-
store.set('Light', {
|
|
144
|
-
type: 'Light',
|
|
145
|
-
id: lightId,
|
|
146
|
-
kind: intent.props.kind ?? ('directional' as LightKind),
|
|
147
|
-
color,
|
|
148
|
-
intensity: intent.props.intensity ?? 1.0,
|
|
149
|
-
range: intent.props.range ?? 10.0,
|
|
150
|
-
castShadow: intent.props.castShadow ?? true,
|
|
151
|
-
direction,
|
|
152
|
-
spotInnerOuter: intent.props.spotInnerOuter
|
|
153
|
-
? toVec2(intent.props.spotInnerOuter)
|
|
154
|
-
: [0.2, 0.6],
|
|
155
|
-
skipUpdate: true,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
intentsToRemove.push(i);
|
|
159
|
-
} else if (intent.type === 'update-transform') {
|
|
160
|
-
const store = world.components.get(intent.entityId);
|
|
161
|
-
if (store) {
|
|
162
|
-
const transform = store.get('Transform') as
|
|
163
|
-
| TransformComponent
|
|
164
|
-
| undefined;
|
|
165
|
-
if (transform) {
|
|
166
|
-
const nextProps = { ...intent.props };
|
|
167
|
-
if (nextProps.position) {
|
|
168
|
-
nextProps.position = toVec3(nextProps.position);
|
|
169
|
-
}
|
|
170
|
-
if (nextProps.rotation) {
|
|
171
|
-
nextProps.rotation = toQuat(nextProps.rotation);
|
|
172
|
-
}
|
|
173
|
-
if (nextProps.scale) {
|
|
174
|
-
nextProps.scale = toVec3(nextProps.scale);
|
|
175
|
-
}
|
|
176
|
-
Object.assign(transform, nextProps);
|
|
177
|
-
const matrix = getEntityTransformMatrix(world, intent.entityId);
|
|
178
|
-
const matrixArray = Array.from(matrix);
|
|
179
|
-
|
|
180
|
-
const model = store.get('Model') as ModelComponent | undefined;
|
|
181
|
-
if (model) {
|
|
182
|
-
if (model.skipUpdate) {
|
|
183
|
-
model.skipUpdate = false;
|
|
184
|
-
} else {
|
|
185
|
-
enqueueCommand(context.worldId, 'cmd-model-update', {
|
|
186
|
-
windowId: context.worldId,
|
|
187
|
-
modelId: model.id,
|
|
188
|
-
transform: matrixArray,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const camera = store.get('Camera') as CameraComponent | undefined;
|
|
193
|
-
if (camera) {
|
|
194
|
-
if (camera.skipUpdate) {
|
|
195
|
-
camera.skipUpdate = false;
|
|
196
|
-
} else {
|
|
197
|
-
enqueueCommand(context.worldId, 'cmd-camera-update', {
|
|
198
|
-
cameraId: camera.id,
|
|
199
|
-
transform: matrixArray,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const light = store.get('Light') as LightComponent | undefined;
|
|
204
|
-
if (light) {
|
|
205
|
-
if (light.skipUpdate) {
|
|
206
|
-
light.skipUpdate = false;
|
|
207
|
-
} else {
|
|
208
|
-
const pos = vec3.create();
|
|
209
|
-
mat4.getTranslation(pos, matrix);
|
|
210
|
-
enqueueCommand(context.worldId, 'cmd-light-update', {
|
|
211
|
-
windowId: context.worldId,
|
|
212
|
-
lightId: light.id,
|
|
213
|
-
position: [pos[0], pos[1], pos[2], 1],
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
intentsToRemove.push(i);
|
|
220
|
-
} else if (intent.type === 'detach-component') {
|
|
221
|
-
const store = world.components.get(intent.entityId);
|
|
222
|
-
if (store) {
|
|
223
|
-
const comp = store.get(intent.componentType) as Component | undefined;
|
|
224
|
-
if (comp && 'id' in comp) {
|
|
225
|
-
if (intent.componentType === 'Model') {
|
|
226
|
-
const modelComp = comp as ModelComponent;
|
|
227
|
-
enqueueCommand(context.worldId, 'cmd-model-dispose', {
|
|
228
|
-
windowId: context.worldId,
|
|
229
|
-
modelId: modelComp.id,
|
|
230
|
-
});
|
|
231
|
-
} else if (intent.componentType === 'Camera') {
|
|
232
|
-
const cameraComp = comp as CameraComponent;
|
|
233
|
-
enqueueCommand(context.worldId, 'cmd-camera-dispose', {
|
|
234
|
-
cameraId: cameraComp.id,
|
|
235
|
-
});
|
|
236
|
-
} else if (intent.componentType === 'Light') {
|
|
237
|
-
const lightComp = comp as LightComponent;
|
|
238
|
-
enqueueCommand(context.worldId, 'cmd-light-dispose', {
|
|
239
|
-
windowId: context.worldId,
|
|
240
|
-
lightId: lightComp.id,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
store.delete(intent.componentType);
|
|
245
|
-
}
|
|
246
|
-
intentsToRemove.push(i);
|
|
247
|
-
} else if (intent.type === 'gizmo-draw-line') {
|
|
248
|
-
enqueueCommand(context.worldId, 'cmd-gizmo-draw-line', {
|
|
249
|
-
start: toVec3(intent.start),
|
|
250
|
-
end: toVec3(intent.end),
|
|
251
|
-
color: toVec4(intent.color),
|
|
252
|
-
});
|
|
253
|
-
intentsToRemove.push(i);
|
|
254
|
-
} else if (intent.type === 'gizmo-draw-aabb') {
|
|
255
|
-
enqueueCommand(context.worldId, 'cmd-gizmo-draw-aabb', {
|
|
256
|
-
min: toVec3(intent.min),
|
|
257
|
-
max: toVec3(intent.max),
|
|
258
|
-
color: toVec4(intent.color),
|
|
259
|
-
});
|
|
260
|
-
intentsToRemove.push(i);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
265
|
-
const idx = intentsToRemove[i];
|
|
266
|
-
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatible module shim.
|
|
3
|
+
*
|
|
4
|
+
* The canonical implementation now lives in `scene-sync.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
CoreCommandBuilderSystem,
|
|
8
|
+
SceneSyncSystem,
|
|
9
|
+
} from './scene-sync';
|
|
@@ -2,16 +2,35 @@ import type { EngineCmd } from '../../types/cmds';
|
|
|
2
2
|
import { enqueueCommand } from '../bridge/dispatch';
|
|
3
3
|
import type { System } from '../ecs';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Handles diagnostic intents that query core-side resource lists.
|
|
7
|
+
*
|
|
8
|
+
* This system is intentionally narrow: it transforms
|
|
9
|
+
* `request-resource-list` intents into typed `cmd-*-list` commands.
|
|
10
|
+
*/
|
|
5
11
|
export const DiagnosticsSystem: System = (world, context) => {
|
|
6
12
|
const intentsToRemove: number[] = [];
|
|
7
13
|
|
|
8
14
|
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
9
15
|
const intent = world.pendingIntents[i];
|
|
10
16
|
if (intent?.type === 'request-resource-list') {
|
|
17
|
+
let windowId = world.primaryWindowId;
|
|
18
|
+
if (windowId === undefined) {
|
|
19
|
+
for (const boundWindowId of world.targetWindowBindings.values()) {
|
|
20
|
+
windowId = boundWindowId;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (windowId === undefined) {
|
|
25
|
+
windowId = world.realmCreateArgs.hostWindowId;
|
|
26
|
+
}
|
|
27
|
+
if (windowId === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
11
30
|
const type = intent.resourceType;
|
|
12
31
|
const cmdType = `cmd-${type}-list` as EngineCmd['type'];
|
|
13
32
|
enqueueCommand(context.worldId, cmdType, {
|
|
14
|
-
windowId
|
|
33
|
+
windowId,
|
|
15
34
|
});
|
|
16
35
|
intentsToRemove.push(i);
|
|
17
36
|
}
|
|
@@ -21,22 +40,4 @@ export const DiagnosticsSystem: System = (world, context) => {
|
|
|
21
40
|
const idx = intentsToRemove[i];
|
|
22
41
|
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
23
42
|
}
|
|
24
|
-
|
|
25
|
-
// Simple diagnostic log every 600 frames
|
|
26
|
-
if (world.entities.size > 0 && world.nextCoreId > 2) {
|
|
27
|
-
// We only log if there is something interesting
|
|
28
|
-
const globalCounters = globalThis as unknown as Record<string, number>;
|
|
29
|
-
const frame = globalCounters.vulframFrameCount || 0;
|
|
30
|
-
globalCounters.vulframFrameCount = frame + 1;
|
|
31
|
-
|
|
32
|
-
if (frame % 600 === 0) {
|
|
33
|
-
console.debug(
|
|
34
|
-
`[Diagnostics] World ${context.worldId} | Entities: ${
|
|
35
|
-
world.entities.size
|
|
36
|
-
} | Intents: ${world.pendingIntents.length} | Core objects: ${
|
|
37
|
-
world.nextCoreId - 2
|
|
38
|
-
}`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
43
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { InputMirrorSystem } from './input-mirror';
|
|
2
2
|
export { CommandIntentSystem } from './command-intent';
|
|
3
|
-
export {
|
|
3
|
+
export { ConstraintSolveSystem } from './constraint-solve';
|
|
4
|
+
export { SceneSyncSystem, CoreCommandBuilderSystem } from './scene-sync';
|
|
4
5
|
export { ResourceUploadSystem } from './resource-upload';
|
|
6
|
+
export { UiBridgeSystem } from './ui-bridge';
|
|
5
7
|
export { ResponseDecodeSystem } from './response-decode';
|
|
6
8
|
export { WorldLifecycleSystem } from './world-lifecycle';
|
|
7
9
|
export { DiagnosticsSystem } from './diagnostics';
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import type { KeyboardEvent } from '../../types/events/keyboard';
|
|
2
2
|
import type { PointerEvent } from '../../types/events/pointer';
|
|
3
|
+
import type { GamepadEvent } from '../../types/events/gamepad';
|
|
4
|
+
import type { SystemEvent } from '../../types/events/system';
|
|
5
|
+
import type { UiEvent } from '../../types/events/ui';
|
|
3
6
|
import type { WindowEvent } from '../../types/events/window';
|
|
4
7
|
import type {
|
|
8
|
+
GamepadStateComponent,
|
|
5
9
|
InputStateComponent,
|
|
10
|
+
SystemEventStateComponent,
|
|
6
11
|
System,
|
|
12
|
+
UiEventStateComponent,
|
|
7
13
|
WindowStateComponent,
|
|
8
14
|
} from '../ecs';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
17
|
+
* Mirrors inbound core events into world-level ECS state components.
|
|
18
|
+
*
|
|
19
|
+
* This system owns runtime input snapshots for keyboard, pointer, window,
|
|
20
|
+
* gamepad, system, and UI event streams.
|
|
12
21
|
*/
|
|
13
22
|
export const InputMirrorSystem: System = (world) => {
|
|
14
23
|
// Ensure InputState component exists for the world
|
|
15
24
|
let inputState: InputStateComponent | undefined;
|
|
16
25
|
let windowState: WindowStateComponent | undefined;
|
|
26
|
+
let gamepadState: GamepadStateComponent | undefined;
|
|
27
|
+
let systemEventState: SystemEventStateComponent | undefined;
|
|
28
|
+
let uiEventState: UiEventStateComponent | undefined;
|
|
17
29
|
|
|
18
30
|
// Find or create InputState (attached to a special entity ID 0)
|
|
19
31
|
const WORLD_ENTITY_ID = 0;
|
|
@@ -57,6 +69,38 @@ export const InputMirrorSystem: System = (world) => {
|
|
|
57
69
|
worldStore.set('WindowState', windowState);
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
gamepadState = worldStore.get('GamepadState') as GamepadStateComponent;
|
|
73
|
+
if (!gamepadState) {
|
|
74
|
+
gamepadState = {
|
|
75
|
+
type: 'GamepadState',
|
|
76
|
+
connected: new Map(),
|
|
77
|
+
buttons: new Map(),
|
|
78
|
+
axes: new Map(),
|
|
79
|
+
eventsThisFrame: [],
|
|
80
|
+
};
|
|
81
|
+
worldStore.set('GamepadState', gamepadState);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
systemEventState = worldStore.get(
|
|
85
|
+
'SystemEventState',
|
|
86
|
+
) as SystemEventStateComponent;
|
|
87
|
+
if (!systemEventState) {
|
|
88
|
+
systemEventState = {
|
|
89
|
+
type: 'SystemEventState',
|
|
90
|
+
eventsThisFrame: [],
|
|
91
|
+
};
|
|
92
|
+
worldStore.set('SystemEventState', systemEventState);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
uiEventState = worldStore.get('UiEventState') as UiEventStateComponent;
|
|
96
|
+
if (!uiEventState) {
|
|
97
|
+
uiEventState = {
|
|
98
|
+
type: 'UiEventState',
|
|
99
|
+
eventsThisFrame: [],
|
|
100
|
+
};
|
|
101
|
+
worldStore.set('UiEventState', uiEventState);
|
|
102
|
+
}
|
|
103
|
+
|
|
60
104
|
// Clear "just pressed/released" from previous frame
|
|
61
105
|
inputState.keysJustPressed.clear();
|
|
62
106
|
inputState.keysJustReleased.clear();
|
|
@@ -68,10 +112,13 @@ export const InputMirrorSystem: System = (world) => {
|
|
|
68
112
|
windowState.movedThisFrame = false;
|
|
69
113
|
windowState.focusChangedThisFrame = false;
|
|
70
114
|
windowState.closeRequested = false;
|
|
115
|
+
gamepadState.eventsThisFrame.length = 0;
|
|
116
|
+
systemEventState.eventsThisFrame.length = 0;
|
|
117
|
+
uiEventState.eventsThisFrame.length = 0;
|
|
71
118
|
|
|
72
119
|
// Process inbound events
|
|
73
|
-
|
|
74
|
-
const event = world.inboundEvents
|
|
120
|
+
for (let i = 0; i < world.inboundEvents.length; i++) {
|
|
121
|
+
const event = world.inboundEvents[i];
|
|
75
122
|
if (!event) continue;
|
|
76
123
|
|
|
77
124
|
// Keyboard events
|
|
@@ -148,5 +195,56 @@ export const InputMirrorSystem: System = (world) => {
|
|
|
148
195
|
windowState.scaleFactor = winEvent.data.scaleFactor;
|
|
149
196
|
}
|
|
150
197
|
}
|
|
198
|
+
// Gamepad events
|
|
199
|
+
else if (event.type === 'gamepad') {
|
|
200
|
+
const gpEvent = event.content as GamepadEvent;
|
|
201
|
+
gamepadState.eventsThisFrame.push(gpEvent);
|
|
202
|
+
if (gpEvent.event === 'on-connect') {
|
|
203
|
+
gamepadState.connected.set(gpEvent.data.gamepadId, {
|
|
204
|
+
name: gpEvent.data.name,
|
|
205
|
+
});
|
|
206
|
+
} else if (gpEvent.event === 'on-disconnect') {
|
|
207
|
+
gamepadState.connected.delete(gpEvent.data.gamepadId);
|
|
208
|
+
gamepadState.buttons.delete(gpEvent.data.gamepadId);
|
|
209
|
+
gamepadState.axes.delete(gpEvent.data.gamepadId);
|
|
210
|
+
} else if (gpEvent.event === 'on-button') {
|
|
211
|
+
const id = gpEvent.data.gamepadId;
|
|
212
|
+
let buttons = gamepadState.buttons.get(id);
|
|
213
|
+
if (!buttons) {
|
|
214
|
+
buttons = new Map();
|
|
215
|
+
gamepadState.buttons.set(id, buttons);
|
|
216
|
+
}
|
|
217
|
+
buttons.set(gpEvent.data.button, {
|
|
218
|
+
pressed: gpEvent.data.state === 'pressed',
|
|
219
|
+
value: gpEvent.data.value,
|
|
220
|
+
});
|
|
221
|
+
} else if (gpEvent.event === 'on-axis') {
|
|
222
|
+
const id = gpEvent.data.gamepadId;
|
|
223
|
+
let axes = gamepadState.axes.get(id);
|
|
224
|
+
if (!axes) {
|
|
225
|
+
axes = new Map();
|
|
226
|
+
gamepadState.axes.set(id, axes);
|
|
227
|
+
}
|
|
228
|
+
axes.set(gpEvent.data.axis, gpEvent.data.value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// System events
|
|
232
|
+
else if (event.type === 'system') {
|
|
233
|
+
const sysEvent = event.content as SystemEvent;
|
|
234
|
+
systemEventState.eventsThisFrame.push(sysEvent);
|
|
235
|
+
if (sysEvent.event === 'error') {
|
|
236
|
+
systemEventState.lastError = {
|
|
237
|
+
scope: sysEvent.data.scope,
|
|
238
|
+
message: sysEvent.data.message,
|
|
239
|
+
commandId: sysEvent.data.commandId,
|
|
240
|
+
commandType: sysEvent.data.commandType,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// UI events
|
|
245
|
+
else if (event.type === 'ui') {
|
|
246
|
+
uiEventState.eventsThisFrame.push(event.content as UiEvent);
|
|
247
|
+
}
|
|
151
248
|
}
|
|
249
|
+
world.inboundEvents.length = 0;
|
|
152
250
|
};
|