@vulfram/engine 0.5.6-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/package.json +17 -0
- package/src/engine/api.ts +282 -0
- package/src/engine/bridge/dispatch.ts +115 -0
- package/src/engine/bridge/guards.ts +28 -0
- package/src/engine/bridge/protocol.ts +130 -0
- package/src/engine/ecs/index.ts +516 -0
- package/src/engine/errors.ts +10 -0
- package/src/engine/state.ts +135 -0
- package/src/engine/systems/command-intent.ts +74 -0
- package/src/engine/systems/core-command-builder.ts +265 -0
- package/src/engine/systems/diagnostics.ts +42 -0
- package/src/engine/systems/index.ts +7 -0
- package/src/engine/systems/input-mirror.ts +152 -0
- package/src/engine/systems/resource-upload.ts +143 -0
- package/src/engine/systems/response-decode.ts +28 -0
- package/src/engine/systems/utils.ts +147 -0
- package/src/engine/systems/world-lifecycle.ts +164 -0
- package/src/engine/world/entities.ts +516 -0
- package/src/index.ts +9 -0
- package/src/types/cmds/camera.ts +76 -0
- package/src/types/cmds/environment.ts +45 -0
- package/src/types/cmds/geometry.ts +144 -0
- package/src/types/cmds/gizmo.ts +18 -0
- package/src/types/cmds/index.ts +231 -0
- package/src/types/cmds/light.ts +69 -0
- package/src/types/cmds/material.ts +98 -0
- package/src/types/cmds/model.ts +59 -0
- package/src/types/cmds/shadow.ts +22 -0
- package/src/types/cmds/system.ts +15 -0
- package/src/types/cmds/texture.ts +63 -0
- package/src/types/cmds/window.ts +263 -0
- package/src/types/events/gamepad.ts +34 -0
- package/src/types/events/index.ts +19 -0
- package/src/types/events/keyboard.ts +225 -0
- package/src/types/events/pointer.ts +105 -0
- package/src/types/events/system.ts +13 -0
- package/src/types/events/window.ts +96 -0
- package/src/types/index.ts +3 -0
- package/src/types/kinds.ts +111 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { System } from '../ecs';
|
|
2
|
+
import { engineState } from '../state';
|
|
3
|
+
|
|
4
|
+
export const ResponseDecodeSystem: System = (world, context) => {
|
|
5
|
+
while (world.inboundResponses.length > 0) {
|
|
6
|
+
const res = world.inboundResponses.shift()!;
|
|
7
|
+
const content = res.content as { success?: boolean; message?: string };
|
|
8
|
+
|
|
9
|
+
if (engineState.flags.debugEnabled) {
|
|
10
|
+
const debugKey = '__vulframDebugRespAllCount';
|
|
11
|
+
const count =
|
|
12
|
+
(globalThis as unknown as Record<string, number>)[debugKey] || 0;
|
|
13
|
+
if (count < 10) {
|
|
14
|
+
console.debug('[Debug] Response', res.type, JSON.stringify(content));
|
|
15
|
+
(globalThis as unknown as Record<string, number>)[debugKey] = count + 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (content && typeof content.success === 'boolean' && !content.success) {
|
|
20
|
+
console.error(
|
|
21
|
+
`[World ${context.worldId}] Command ${res.type} (ID: ${res.id}) failed: ${content.message}`,
|
|
22
|
+
);
|
|
23
|
+
} else {
|
|
24
|
+
// DEBUG: Print all successes too for now
|
|
25
|
+
// console.debug(`[World ${context.worldId}] Command ${res.type} (ID: ${res.id}) succeeded.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mat4, quat, vec3 } from 'gl-matrix';
|
|
2
|
+
import type { MaterialOptions, PbrOptions, StandardOptions } from '../../types/cmds/material';
|
|
3
|
+
import type {
|
|
4
|
+
CubeOptions,
|
|
5
|
+
PlaneOptions,
|
|
6
|
+
PrimitiveOptions,
|
|
7
|
+
PyramidOptions,
|
|
8
|
+
} from '../../types/cmds/geometry';
|
|
9
|
+
import type { TransformComponent } from '../ecs';
|
|
10
|
+
import type { WorldState } from '../state';
|
|
11
|
+
|
|
12
|
+
export function toTuple(value: ArrayLike<number>, length: number): number[] {
|
|
13
|
+
const result = new Array<number>(length);
|
|
14
|
+
for (let i = 0; i < length; i++) {
|
|
15
|
+
result[i] = Number(value[i] ?? 0);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toVec2(value: ArrayLike<number>): [number, number] {
|
|
21
|
+
const result = toTuple(value, 2);
|
|
22
|
+
return [result[0] ?? 0, result[1] ?? 0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toVec3(value: ArrayLike<number>): [number, number, number] {
|
|
26
|
+
const result = toTuple(value, 3);
|
|
27
|
+
return [result[0] ?? 0, result[1] ?? 0, result[2] ?? 0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toVec4(value: ArrayLike<number>): [number, number, number, number] {
|
|
31
|
+
const result = toTuple(value, 4);
|
|
32
|
+
return [
|
|
33
|
+
result[0] ?? 0,
|
|
34
|
+
result[1] ?? 0,
|
|
35
|
+
result[2] ?? 0,
|
|
36
|
+
result[3] ?? 0,
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toQuat(value: ArrayLike<number>): [number, number, number, number] {
|
|
41
|
+
return toVec4(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeStandardOptions(options: StandardOptions): StandardOptions {
|
|
45
|
+
const normalized: StandardOptions = {
|
|
46
|
+
...options,
|
|
47
|
+
baseColor: toVec4(options.baseColor),
|
|
48
|
+
surfaceType: options.surfaceType,
|
|
49
|
+
};
|
|
50
|
+
if (options.specColor !== undefined) {
|
|
51
|
+
normalized.specColor = options.specColor ? toVec4(options.specColor) : null;
|
|
52
|
+
}
|
|
53
|
+
if (options.toonParams !== undefined) {
|
|
54
|
+
normalized.toonParams = options.toonParams ? toVec4(options.toonParams) : null;
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizePbrOptions(options: PbrOptions): PbrOptions {
|
|
60
|
+
return {
|
|
61
|
+
...options,
|
|
62
|
+
baseColor: toVec4(options.baseColor),
|
|
63
|
+
emissiveColor: toVec4(options.emissiveColor),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeMaterialOptions(
|
|
68
|
+
options: MaterialOptions | undefined,
|
|
69
|
+
): MaterialOptions | undefined {
|
|
70
|
+
if (!options) return options;
|
|
71
|
+
if (options.type === 'standard') {
|
|
72
|
+
return {
|
|
73
|
+
type: 'standard',
|
|
74
|
+
content: normalizeStandardOptions(options.content),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
type: 'pbr',
|
|
79
|
+
content: normalizePbrOptions(options.content),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizePrimitiveOptions(options: PrimitiveOptions): PrimitiveOptions {
|
|
84
|
+
if (options.type === 'cube') {
|
|
85
|
+
const content = options.content as CubeOptions;
|
|
86
|
+
return {
|
|
87
|
+
type: 'cube',
|
|
88
|
+
content: {
|
|
89
|
+
...content,
|
|
90
|
+
size: content.size ? toVec3(content.size) : content.size,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (options.type === 'plane') {
|
|
95
|
+
const content = options.content as PlaneOptions;
|
|
96
|
+
return {
|
|
97
|
+
type: 'plane',
|
|
98
|
+
content: {
|
|
99
|
+
...content,
|
|
100
|
+
size: content.size ? toVec3(content.size) : content.size,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (options.type === 'pyramid') {
|
|
105
|
+
const content = options.content as PyramidOptions;
|
|
106
|
+
return {
|
|
107
|
+
type: 'pyramid',
|
|
108
|
+
content: {
|
|
109
|
+
...content,
|
|
110
|
+
size: content.size ? toVec3(content.size) : content.size,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return options;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getEntityTransformMatrix(
|
|
118
|
+
world: WorldState,
|
|
119
|
+
entityId: number,
|
|
120
|
+
): mat4 {
|
|
121
|
+
const store = world.components.get(entityId);
|
|
122
|
+
if (!store) return mat4.create();
|
|
123
|
+
|
|
124
|
+
const transform = store.get('Transform') as TransformComponent | undefined;
|
|
125
|
+
|
|
126
|
+
const m = mat4.create();
|
|
127
|
+
if (transform) {
|
|
128
|
+
const rotation = quat.fromValues(
|
|
129
|
+
transform.rotation[0],
|
|
130
|
+
transform.rotation[1],
|
|
131
|
+
transform.rotation[2],
|
|
132
|
+
transform.rotation[3],
|
|
133
|
+
);
|
|
134
|
+
const position = vec3.fromValues(
|
|
135
|
+
transform.position[0],
|
|
136
|
+
transform.position[1],
|
|
137
|
+
transform.position[2],
|
|
138
|
+
);
|
|
139
|
+
const scale = vec3.fromValues(
|
|
140
|
+
transform.scale[0],
|
|
141
|
+
transform.scale[1],
|
|
142
|
+
transform.scale[2],
|
|
143
|
+
);
|
|
144
|
+
mat4.fromRotationTranslationScale(m, rotation, position, scale);
|
|
145
|
+
}
|
|
146
|
+
return m;
|
|
147
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { EnvironmentConfig } from '../../types/cmds/environment';
|
|
2
|
+
import type { ShadowConfig } from '../../types/cmds/shadow';
|
|
3
|
+
import type { WindowState } from '../../types/kinds';
|
|
4
|
+
import { enqueueCommand } from '../bridge/dispatch';
|
|
5
|
+
import type { System } from '../ecs';
|
|
6
|
+
import { toVec2, toVec3 } from './utils';
|
|
7
|
+
|
|
8
|
+
export const WorldLifecycleSystem: System = (world, context) => {
|
|
9
|
+
const intentsToRemove: number[] = [];
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
12
|
+
const intent = world.pendingIntents[i];
|
|
13
|
+
if (!intent) continue;
|
|
14
|
+
|
|
15
|
+
if (intent.type === 'create-window') {
|
|
16
|
+
const cmd = {
|
|
17
|
+
windowId: context.worldId,
|
|
18
|
+
title: intent.props.title,
|
|
19
|
+
size: toVec2(intent.props.size),
|
|
20
|
+
position: toVec2(intent.props.position),
|
|
21
|
+
borderless: intent.props.borderless ?? false,
|
|
22
|
+
resizable: intent.props.resizable ?? true,
|
|
23
|
+
transparent: intent.props.transparent ?? false,
|
|
24
|
+
initialState: intent.props.initialState ?? ('windowed' as WindowState),
|
|
25
|
+
} as const;
|
|
26
|
+
const payload: typeof cmd & { canvasId?: string } = { ...cmd };
|
|
27
|
+
if (intent.props.canvasId !== undefined) {
|
|
28
|
+
payload.canvasId = intent.props.canvasId;
|
|
29
|
+
}
|
|
30
|
+
enqueueCommand(context.worldId, 'cmd-window-create', payload);
|
|
31
|
+
intentsToRemove.push(i);
|
|
32
|
+
} else if (intent.type === 'close-window') {
|
|
33
|
+
enqueueCommand(context.worldId, 'cmd-window-close', {
|
|
34
|
+
windowId: context.worldId,
|
|
35
|
+
});
|
|
36
|
+
intentsToRemove.push(i);
|
|
37
|
+
} else if (intent.type === 'update-window') {
|
|
38
|
+
const p = intent.props;
|
|
39
|
+
if (p.title !== undefined) {
|
|
40
|
+
enqueueCommand(context.worldId, 'cmd-window-set-title', {
|
|
41
|
+
windowId: context.worldId,
|
|
42
|
+
title: p.title,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (p.position !== undefined) {
|
|
46
|
+
enqueueCommand(context.worldId, 'cmd-window-set-position', {
|
|
47
|
+
windowId: context.worldId,
|
|
48
|
+
position: p.position,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (p.size !== undefined) {
|
|
52
|
+
enqueueCommand(context.worldId, 'cmd-window-set-size', {
|
|
53
|
+
windowId: context.worldId,
|
|
54
|
+
size: p.size,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (p.state !== undefined) {
|
|
58
|
+
enqueueCommand(context.worldId, 'cmd-window-set-state', {
|
|
59
|
+
windowId: context.worldId,
|
|
60
|
+
state: p.state,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (p.resizable !== undefined) {
|
|
64
|
+
enqueueCommand(context.worldId, 'cmd-window-set-resizable', {
|
|
65
|
+
windowId: context.worldId,
|
|
66
|
+
resizable: p.resizable,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (p.decorations !== undefined) {
|
|
70
|
+
enqueueCommand(context.worldId, 'cmd-window-set-decorations', {
|
|
71
|
+
windowId: context.worldId,
|
|
72
|
+
decorations: p.decorations,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (p.cursorVisible !== undefined) {
|
|
76
|
+
enqueueCommand(context.worldId, 'cmd-window-set-cursor-visible', {
|
|
77
|
+
windowId: context.worldId,
|
|
78
|
+
visible: p.cursorVisible,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (p.cursorGrab !== undefined) {
|
|
82
|
+
enqueueCommand(context.worldId, 'cmd-window-set-cursor-grab', {
|
|
83
|
+
windowId: context.worldId,
|
|
84
|
+
mode: p.cursorGrab,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (p.icon !== undefined) {
|
|
88
|
+
enqueueCommand(context.worldId, 'cmd-window-set-icon', {
|
|
89
|
+
windowId: context.worldId,
|
|
90
|
+
icon: p.icon,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (p.cursorIcon !== undefined) {
|
|
94
|
+
enqueueCommand(context.worldId, 'cmd-window-set-cursor-icon', {
|
|
95
|
+
windowId: context.worldId,
|
|
96
|
+
icon: p.cursorIcon,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
intentsToRemove.push(i);
|
|
100
|
+
} else if (intent.type === 'request-attention') {
|
|
101
|
+
enqueueCommand(context.worldId, 'cmd-window-request-attention', {
|
|
102
|
+
windowId: context.worldId,
|
|
103
|
+
attentionType: intent.attentionType,
|
|
104
|
+
});
|
|
105
|
+
intentsToRemove.push(i);
|
|
106
|
+
} else if (intent.type === 'focus-window') {
|
|
107
|
+
enqueueCommand(context.worldId, 'cmd-window-focus', {
|
|
108
|
+
windowId: context.worldId,
|
|
109
|
+
});
|
|
110
|
+
intentsToRemove.push(i);
|
|
111
|
+
} else if (intent.type === 'configure-environment') {
|
|
112
|
+
const config = intent.config as EnvironmentConfig;
|
|
113
|
+
const payload: EnvironmentConfig = {
|
|
114
|
+
msaa: {
|
|
115
|
+
enabled: config.msaa.enabled,
|
|
116
|
+
sampleCount: config.msaa.sampleCount,
|
|
117
|
+
},
|
|
118
|
+
skybox: {
|
|
119
|
+
mode: config.skybox.mode,
|
|
120
|
+
intensity: config.skybox.intensity,
|
|
121
|
+
rotation: config.skybox.rotation,
|
|
122
|
+
tint: toVec3(config.skybox.tint),
|
|
123
|
+
cubemapTextureId: config.skybox.cubemapTextureId ?? null,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
enqueueCommand(context.worldId, 'cmd-environment-update', {
|
|
127
|
+
windowId: context.worldId,
|
|
128
|
+
config: payload,
|
|
129
|
+
});
|
|
130
|
+
intentsToRemove.push(i);
|
|
131
|
+
} else if (intent.type === 'configure-shadows') {
|
|
132
|
+
const c = intent.config || {};
|
|
133
|
+
const config: ShadowConfig = {
|
|
134
|
+
tileResolution: c.tileResolution ?? 1024,
|
|
135
|
+
atlasTilesW: c.atlasTilesW ?? 8,
|
|
136
|
+
atlasTilesH: c.atlasTilesH ?? 8,
|
|
137
|
+
atlasLayers: c.atlasLayers ?? 2,
|
|
138
|
+
virtualGridSize: c.virtualGridSize ?? 1,
|
|
139
|
+
smoothing: c.smoothing ?? 2,
|
|
140
|
+
normalBias: c.normalBias ?? 0.01,
|
|
141
|
+
};
|
|
142
|
+
enqueueCommand(context.worldId, 'cmd-shadow-configure', {
|
|
143
|
+
windowId: context.worldId,
|
|
144
|
+
config,
|
|
145
|
+
});
|
|
146
|
+
intentsToRemove.push(i);
|
|
147
|
+
} else if (intent.type === 'send-notification') {
|
|
148
|
+
enqueueCommand(context.worldId, 'cmd-notification-send', {
|
|
149
|
+
id: `notif-${Date.now()}`,
|
|
150
|
+
level: intent.level,
|
|
151
|
+
title: intent.title,
|
|
152
|
+
body: intent.message,
|
|
153
|
+
timeout: 5000,
|
|
154
|
+
});
|
|
155
|
+
intentsToRemove.push(i);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
160
|
+
const idx = intentsToRemove[i];
|
|
161
|
+
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
};
|