@galfus/engine 0.0.0-stage → 0.31.0-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 +190 -2
- package/package.json +59 -4
- package/src/anchors/cache.ts +101 -0
- package/src/anchors/component.ts +123 -0
- package/src/anchors/hierarchy.ts +192 -0
- package/src/anchors/index.ts +8 -0
- package/src/anchors/solver.ts +247 -0
- package/src/anchors/strategy.ts +142 -0
- package/src/animation/blending.ts +163 -0
- package/src/animation/clip.ts +132 -0
- package/src/animation/coordinator.ts +568 -0
- package/src/animation/index.ts +7 -0
- package/src/animation/native-targets.ts +37 -0
- package/src/animation/player.ts +287 -0
- package/src/audio/index.ts +4 -0
- package/src/audio/manager.ts +371 -0
- package/src/collision/components.ts +211 -0
- package/src/collision/index.ts +16 -0
- package/src/collision/query2d.ts +167 -0
- package/src/collision/query3d.ts +170 -0
- package/src/ecs/component.ts +130 -0
- package/src/ecs/entity.ts +75 -0
- package/src/ecs/index.ts +9 -0
- package/src/ecs/intent.ts +162 -0
- package/src/ecs/metadata.ts +44 -0
- package/src/ecs/system.ts +224 -0
- package/src/ecs/world.ts +291 -0
- package/src/index.ts +19 -0
- package/src/interaction/focus.ts +327 -0
- package/src/interaction/index.ts +9 -0
- package/src/interaction/listener.ts +58 -0
- package/src/interaction/snapshot2d.ts +210 -0
- package/src/interaction/snapshot3d.ts +214 -0
- package/src/interaction/system.ts +544 -0
- package/src/interaction/types.ts +27 -0
- package/src/math/common.ts +67 -0
- package/src/math/index.ts +17 -0
- package/src/math/mat2d.ts +320 -0
- package/src/math/mat4.ts +576 -0
- package/src/math/quat.ts +438 -0
- package/src/math/vec2.ts +161 -0
- package/src/math/vec3.ts +242 -0
- package/src/math/vec4.ts +171 -0
- package/src/multiverse/coordinator.ts +540 -0
- package/src/multiverse/index.ts +13 -0
- package/src/multiverse/presentation-graph.ts +306 -0
- package/src/multiverse/presentation-links.ts +134 -0
- package/src/multiverse/realm.ts +49 -0
- package/src/platform/index.ts +2 -0
- package/src/platform/platform.ts +149 -0
- package/src/platform/window.ts +181 -0
- package/src/realm2d/components.ts +254 -0
- package/src/realm2d/index.ts +12 -0
- package/src/realm2d/presets/index.ts +36 -0
- package/src/realm2d/realm.ts +522 -0
- package/src/realm2d/sync.ts +284 -0
- package/src/realm2d/utils/index.ts +32 -0
- package/src/realm3d/components.ts +341 -0
- package/src/realm3d/geometry.ts +224 -0
- package/src/realm3d/index.ts +12 -0
- package/src/realm3d/presets/index.ts +67 -0
- package/src/realm3d/realm.ts +588 -0
- package/src/realm3d/resource-ids.ts +62 -0
- package/src/realm3d/skinning.ts +128 -0
- package/src/realm3d/sync.ts +442 -0
- package/src/realm3d/utils/index.ts +26 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/manager.ts +92 -0
- package/src/runtime.ts +588 -0
- package/src/spatial/dirty_registry.ts +217 -0
- package/src/spatial/flags.ts +14 -0
- package/src/spatial/index.ts +6 -0
- package/src/spatial/metrics.ts +124 -0
- package/src/text/editor.ts +224 -0
- package/src/text/encoding.ts +38 -0
- package/src/text/index.ts +13 -0
- package/src/text/input.ts +160 -0
- package/src/text/keyboard.ts +50 -0
- package/src/text/svg-decorations.ts +147 -0
- package/src/types.ts +85 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { KeyboardEvent } from '@galfus/transport-types';
|
|
2
|
+
import type { Focusable } from '../interaction/focus';
|
|
3
|
+
import type { Realm2D } from '../realm2d/realm';
|
|
4
|
+
import { TextEditor, type TextSelection } from './editor';
|
|
5
|
+
import { applyTextKeyboardEvent } from './keyboard';
|
|
6
|
+
|
|
7
|
+
export interface TextInputOptions {
|
|
8
|
+
readonly svgId: number;
|
|
9
|
+
readonly elementId: string;
|
|
10
|
+
/** SVG element used for hit testing; defaults to elementId. */
|
|
11
|
+
readonly hitAreaId?: string;
|
|
12
|
+
readonly value?: string;
|
|
13
|
+
readonly tabIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TextInputEventType = 'focus' | 'blur' | 'change' | 'selection' | 'composition';
|
|
17
|
+
export interface TextInputEvent {
|
|
18
|
+
readonly type: TextInputEventType;
|
|
19
|
+
readonly windowId?: number;
|
|
20
|
+
readonly value: string;
|
|
21
|
+
readonly selection: TextSelection;
|
|
22
|
+
}
|
|
23
|
+
export type TextInputCallback = (event: TextInputEvent) => void;
|
|
24
|
+
|
|
25
|
+
/** Editable Unicode state bound to one SVG text element and one focus target. */
|
|
26
|
+
export class TextInput {
|
|
27
|
+
private readonly editor: TextEditor;
|
|
28
|
+
private readonly callbacks = new Map<TextInputEventType, Set<TextInputCallback>>();
|
|
29
|
+
private disposed = false;
|
|
30
|
+
private disposing = false;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly realm: Realm2D,
|
|
34
|
+
private readonly options: TextInputOptions,
|
|
35
|
+
readonly focusable: Focusable,
|
|
36
|
+
private readonly setIme: (windowId: number, enabled: boolean) => void,
|
|
37
|
+
private readonly remove: (input: TextInput) => void
|
|
38
|
+
) {
|
|
39
|
+
this.editor = new TextEditor(options.value ?? '');
|
|
40
|
+
this.render();
|
|
41
|
+
focusable.on('focus', ({ windowId }) => {
|
|
42
|
+
this.setIme(windowId, true);
|
|
43
|
+
this.emit('focus', windowId);
|
|
44
|
+
});
|
|
45
|
+
focusable.on('blur', ({ windowId }) => {
|
|
46
|
+
if (this.editor.composition) {
|
|
47
|
+
this.editor.cancelComposition();
|
|
48
|
+
if (!this.realm.isDisposed && !this.focusable.listener.isDisposed) this.render();
|
|
49
|
+
}
|
|
50
|
+
this.setIme(windowId, false);
|
|
51
|
+
this.emit('blur', windowId);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get value(): string {
|
|
56
|
+
return this.editor.value;
|
|
57
|
+
}
|
|
58
|
+
get svgId(): number {
|
|
59
|
+
return this.options.svgId;
|
|
60
|
+
}
|
|
61
|
+
get elementId(): string {
|
|
62
|
+
return this.options.elementId;
|
|
63
|
+
}
|
|
64
|
+
get displayValue(): string {
|
|
65
|
+
return this.editor.displayValue;
|
|
66
|
+
}
|
|
67
|
+
get selection(): TextSelection {
|
|
68
|
+
return this.editor.selection;
|
|
69
|
+
}
|
|
70
|
+
get selectedText(): string {
|
|
71
|
+
return this.editor.selectedText;
|
|
72
|
+
}
|
|
73
|
+
get composition() {
|
|
74
|
+
return this.editor.composition;
|
|
75
|
+
}
|
|
76
|
+
get isDisposed(): boolean {
|
|
77
|
+
return this.disposed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
requestFocus(windowId?: number): boolean {
|
|
81
|
+
this.assertLive();
|
|
82
|
+
return this.focusable.requestFocus(windowId);
|
|
83
|
+
}
|
|
84
|
+
blur(windowId?: number): void {
|
|
85
|
+
this.assertLive();
|
|
86
|
+
this.focusable.blur(windowId);
|
|
87
|
+
}
|
|
88
|
+
setValue(value: string): void {
|
|
89
|
+
this.mutate(() => this.editor.setValue(value));
|
|
90
|
+
}
|
|
91
|
+
setSelection(anchor: number, focus = anchor): void {
|
|
92
|
+
this.mutate(() => this.editor.setSelection(anchor, focus));
|
|
93
|
+
}
|
|
94
|
+
selectAll(): void {
|
|
95
|
+
this.mutate(() => this.editor.selectAll());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
on(type: TextInputEventType, callback: TextInputCallback): () => void {
|
|
99
|
+
this.assertLive();
|
|
100
|
+
let callbacks = this.callbacks.get(type);
|
|
101
|
+
if (!callbacks) this.callbacks.set(type, (callbacks = new Set()));
|
|
102
|
+
callbacks.add(callback);
|
|
103
|
+
return () => callbacks.delete(callback);
|
|
104
|
+
}
|
|
105
|
+
off(type: TextInputEventType, callback: TextInputCallback): void {
|
|
106
|
+
this.callbacks.get(type)?.delete(callback);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal */
|
|
110
|
+
handleKeyboard(event: KeyboardEvent): void {
|
|
111
|
+
if (!this.disposed)
|
|
112
|
+
this.mutate(() => applyTextKeyboardEvent(this.editor, event), event.windowId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dispose(): void {
|
|
116
|
+
if (this.disposed || this.disposing) return;
|
|
117
|
+
this.disposing = true;
|
|
118
|
+
this.focusable.dispose();
|
|
119
|
+
this.disposed = true;
|
|
120
|
+
this.callbacks.clear();
|
|
121
|
+
this.remove(this);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private mutate(action: () => void, windowId?: number): void {
|
|
125
|
+
this.assertLive();
|
|
126
|
+
const value = this.editor.value;
|
|
127
|
+
const display = this.editor.displayValue;
|
|
128
|
+
const { anchor, focus } = this.editor.selection;
|
|
129
|
+
const composition = this.editor.composition;
|
|
130
|
+
action();
|
|
131
|
+
if (display !== this.editor.displayValue) this.render();
|
|
132
|
+
if (value !== this.editor.value) this.emit('change', windowId);
|
|
133
|
+
if (this.disposed) return;
|
|
134
|
+
const selection = this.editor.selection;
|
|
135
|
+
if (anchor !== selection.anchor || focus !== selection.focus) this.emit('selection', windowId);
|
|
136
|
+
if (!this.disposed && composition !== this.editor.composition)
|
|
137
|
+
this.emit('composition', windowId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private render(): void {
|
|
141
|
+
this.realm.setSvgText(
|
|
142
|
+
{ svgId: this.options.svgId },
|
|
143
|
+
this.options.elementId,
|
|
144
|
+
this.editor.displayValue
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private emit(type: TextInputEventType, windowId?: number): void {
|
|
149
|
+
const callbacks = this.callbacks.get(type);
|
|
150
|
+
if (!callbacks) return;
|
|
151
|
+
const event = { type, windowId, value: this.value, selection: this.selection };
|
|
152
|
+
for (const callback of callbacks) {
|
|
153
|
+
callback(event);
|
|
154
|
+
if (this.disposed) break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
private assertLive(): void {
|
|
158
|
+
if (this.disposed) throw new Error('Text input is disposed.');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { KeyboardEvent } from '@galfus/transport-types';
|
|
2
|
+
import { TextEditor } from './editor';
|
|
3
|
+
|
|
4
|
+
/** Maps platform keyboard and composition events to a single editor. */
|
|
5
|
+
export function applyTextKeyboardEvent(editor: TextEditor, event: KeyboardEvent): void {
|
|
6
|
+
switch (event.type) {
|
|
7
|
+
case 'imePreedit':
|
|
8
|
+
if (event.text) editor.updateComposition(event.text, event.selection);
|
|
9
|
+
else editor.cancelComposition();
|
|
10
|
+
return;
|
|
11
|
+
case 'imeCommit':
|
|
12
|
+
case 'imeDisabled':
|
|
13
|
+
editor.cancelComposition();
|
|
14
|
+
return;
|
|
15
|
+
case 'textInput':
|
|
16
|
+
editor.insert(event.text);
|
|
17
|
+
return;
|
|
18
|
+
case 'input':
|
|
19
|
+
break;
|
|
20
|
+
default:
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (event.state !== 'pressed' || editor.composition) return;
|
|
24
|
+
const { shift, ctrl, alt, superKey } = event.modifiers;
|
|
25
|
+
const isControl = (ctrl || superKey) && !alt;
|
|
26
|
+
if (isControl && event.logicalKey.toLowerCase() === 'a') {
|
|
27
|
+
editor.selectAll();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
switch (event.logicalKey) {
|
|
31
|
+
case 'ArrowLeft':
|
|
32
|
+
editor.move('left', shift, isControl);
|
|
33
|
+
return;
|
|
34
|
+
case 'ArrowRight':
|
|
35
|
+
editor.move('right', shift, isControl);
|
|
36
|
+
return;
|
|
37
|
+
case 'Home':
|
|
38
|
+
if (!isControl) editor.moveToBoundary('start', shift);
|
|
39
|
+
return;
|
|
40
|
+
case 'End':
|
|
41
|
+
if (!isControl) editor.moveToBoundary('end', shift);
|
|
42
|
+
return;
|
|
43
|
+
case 'Backspace':
|
|
44
|
+
editor.backspace(isControl);
|
|
45
|
+
return;
|
|
46
|
+
case 'Delete':
|
|
47
|
+
editor.deleteForward(isControl);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** A rectangle in SVG viewport coordinates. */
|
|
2
|
+
export interface TextDecorationRect {
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
readonly width: number;
|
|
6
|
+
readonly height: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SvgTextDecorationOptions {
|
|
10
|
+
/** An existing SVG rect reserved for the caret. */
|
|
11
|
+
readonly cursorId: string;
|
|
12
|
+
/** Existing SVG rects reserved for selection spans. */
|
|
13
|
+
readonly selectionIds?: readonly string[];
|
|
14
|
+
/** Alternatively, one existing SVG path can hold any number of selection spans. */
|
|
15
|
+
readonly selectionPathId?: string;
|
|
16
|
+
readonly cursorColor?: string;
|
|
17
|
+
readonly selectionColor?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SvgAttributeWriter {
|
|
21
|
+
setSvgAttribute(svg: { svgId: number }, elementId: string, name: string, value: string): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Draws host-calculated caret and selection geometry in an existing SVG. */
|
|
25
|
+
export class SvgTextDecorations {
|
|
26
|
+
private readonly cache = new Map<string, Map<string, string>>();
|
|
27
|
+
private readonly selectionIds: readonly string[];
|
|
28
|
+
private readonly cursorId: string;
|
|
29
|
+
private readonly selectionPathId?: string;
|
|
30
|
+
private readonly svg: { svgId: number };
|
|
31
|
+
private cursorColor: string;
|
|
32
|
+
private selectionColor: string;
|
|
33
|
+
private disposed = false;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly realm: SvgAttributeWriter,
|
|
37
|
+
svgId: number,
|
|
38
|
+
options: SvgTextDecorationOptions
|
|
39
|
+
) {
|
|
40
|
+
const selectionIds = options.selectionIds ?? [];
|
|
41
|
+
if (!options.cursorId || selectionIds.some((id) => !id) || options.selectionPathId === '')
|
|
42
|
+
throw new Error('Text decoration IDs must not be empty.');
|
|
43
|
+
if (selectionIds.length && options.selectionPathId)
|
|
44
|
+
throw new Error('Use selection rectangles or a selection path, not both.');
|
|
45
|
+
const ids = [
|
|
46
|
+
options.cursorId,
|
|
47
|
+
...selectionIds,
|
|
48
|
+
...(options.selectionPathId ? [options.selectionPathId] : [])
|
|
49
|
+
];
|
|
50
|
+
if (new Set(ids).size !== ids.length) throw new Error('Text decoration IDs must be distinct.');
|
|
51
|
+
this.selectionIds = [...selectionIds];
|
|
52
|
+
this.selectionPathId = options.selectionPathId;
|
|
53
|
+
this.cursorId = options.cursorId;
|
|
54
|
+
this.svg = { svgId };
|
|
55
|
+
this.cursorColor = SvgTextDecorations.color(options.cursorColor ?? '#ffffff');
|
|
56
|
+
this.selectionColor = SvgTextDecorations.color(options.selectionColor ?? '#438cff80');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setColors(colors: { cursorColor?: string; selectionColor?: string }): void {
|
|
60
|
+
this.assertLive();
|
|
61
|
+
const cursorColor =
|
|
62
|
+
colors.cursorColor === undefined
|
|
63
|
+
? this.cursorColor
|
|
64
|
+
: SvgTextDecorations.color(colors.cursorColor);
|
|
65
|
+
const selectionColor =
|
|
66
|
+
colors.selectionColor === undefined
|
|
67
|
+
? this.selectionColor
|
|
68
|
+
: SvgTextDecorations.color(colors.selectionColor);
|
|
69
|
+
this.cursorColor = cursorColor;
|
|
70
|
+
this.selectionColor = selectionColor;
|
|
71
|
+
if (this.cache.get(this.cursorId)?.get('display') === 'inline')
|
|
72
|
+
this.attribute(this.cursorId, 'fill', cursorColor);
|
|
73
|
+
for (const id of this.selectionIds)
|
|
74
|
+
if (this.cache.get(id)?.get('display') === 'inline')
|
|
75
|
+
this.attribute(id, 'fill', selectionColor);
|
|
76
|
+
if (this.selectionPathId && this.cache.get(this.selectionPathId)?.get('display') === 'inline')
|
|
77
|
+
this.attribute(this.selectionPathId, 'fill', selectionColor);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Updates geometry and colors; absent rectangles are hidden. */
|
|
81
|
+
update(cursor: TextDecorationRect | undefined, selections: readonly TextDecorationRect[]): void {
|
|
82
|
+
this.assertLive();
|
|
83
|
+
if (!this.selectionPathId && selections.length > this.selectionIds.length)
|
|
84
|
+
throw new RangeError('Not enough SVG selection rectangles for the text layout.');
|
|
85
|
+
if (cursor) SvgTextDecorations.validateRect(cursor);
|
|
86
|
+
for (const rect of selections) SvgTextDecorations.validateRect(rect);
|
|
87
|
+
this.rect(this.cursorId, cursor, this.cursorColor);
|
|
88
|
+
if (this.selectionPathId) {
|
|
89
|
+
if (selections.length) {
|
|
90
|
+
const path = selections
|
|
91
|
+
.map(({ x, y, width, height }) => `M${x} ${y}h${width}v${height}h${-width}z`)
|
|
92
|
+
.join('');
|
|
93
|
+
this.attribute(this.selectionPathId, 'd', path);
|
|
94
|
+
this.attribute(this.selectionPathId, 'fill', this.selectionColor);
|
|
95
|
+
this.attribute(this.selectionPathId, 'display', 'inline');
|
|
96
|
+
} else this.attribute(this.selectionPathId, 'display', 'none');
|
|
97
|
+
}
|
|
98
|
+
for (let index = 0; index < this.selectionIds.length; index++)
|
|
99
|
+
this.rect(this.selectionIds[index]!, selections[index], this.selectionColor);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dispose(): void {
|
|
103
|
+
if (this.disposed) return;
|
|
104
|
+
this.update(undefined, []);
|
|
105
|
+
this.disposed = true;
|
|
106
|
+
this.cache.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private rect(id: string, rect: TextDecorationRect | undefined, color: string): void {
|
|
110
|
+
if (!rect) {
|
|
111
|
+
this.attribute(id, 'display', 'none');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.attribute(id, 'x', String(rect.x));
|
|
115
|
+
this.attribute(id, 'y', String(rect.y));
|
|
116
|
+
this.attribute(id, 'width', String(rect.width));
|
|
117
|
+
this.attribute(id, 'height', String(rect.height));
|
|
118
|
+
this.attribute(id, 'fill', color);
|
|
119
|
+
this.attribute(id, 'display', 'inline');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private attribute(id: string, name: string, value: string): void {
|
|
123
|
+
let attributes = this.cache.get(id);
|
|
124
|
+
if (attributes?.get(name) === value) return;
|
|
125
|
+
this.realm.setSvgAttribute(this.svg, id, name, value);
|
|
126
|
+
if (!attributes) this.cache.set(id, (attributes = new Map()));
|
|
127
|
+
attributes.set(name, value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private assertLive(): void {
|
|
131
|
+
if (this.disposed) throw new Error('SVG text decorations are disposed.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static color(value: string): string {
|
|
135
|
+
if (!value.trim()) throw new Error('Text decoration color must not be empty.');
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static validateRect(rect: TextDecorationRect): void {
|
|
140
|
+
if (
|
|
141
|
+
![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) ||
|
|
142
|
+
rect.width < 0 ||
|
|
143
|
+
rect.height < 0
|
|
144
|
+
)
|
|
145
|
+
throw new RangeError('Text decoration rectangle must be finite and non-negative in size.');
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strongly typed nominal (branded) logical IDs for Galfus Engine subsystems.
|
|
3
|
+
*/
|
|
4
|
+
import { assertValidLogicalId } from '@galfus/transport-types';
|
|
5
|
+
|
|
6
|
+
declare const __brand: unique symbol;
|
|
7
|
+
export type Brand<T, B> = T & { readonly [__brand]: B };
|
|
8
|
+
|
|
9
|
+
export type WindowId = Brand<number, 'WindowId'>;
|
|
10
|
+
export type TargetId = Brand<number, 'TargetId'>;
|
|
11
|
+
export type RealmId = Brand<number, 'RealmId'>;
|
|
12
|
+
export type InvocationId = Brand<number, 'InvocationId'>;
|
|
13
|
+
export type TextureId = Brand<number, 'TextureId'>;
|
|
14
|
+
export type GeometryId = Brand<number, 'GeometryId'>;
|
|
15
|
+
export type MaterialId = Brand<number, 'MaterialId'>;
|
|
16
|
+
export type SoundId = Brand<number, 'SoundId'>;
|
|
17
|
+
export type VoiceId = Brand<number, 'VoiceId'>;
|
|
18
|
+
export type TransitionId = Brand<number, 'TransitionId'>;
|
|
19
|
+
export type PathId = Brand<number, 'PathId'>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sequential ID allocator that strictly enforces core-reserved ID boundaries.
|
|
23
|
+
*/
|
|
24
|
+
export class IdAllocator<T extends number = number> {
|
|
25
|
+
private _nextId: number;
|
|
26
|
+
private readonly _used = new Set<number>();
|
|
27
|
+
|
|
28
|
+
constructor(initialId = 1) {
|
|
29
|
+
this._nextId = initialId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
allocate(context = 'LogicalId'): T {
|
|
33
|
+
while (this._used.has(this._nextId)) {
|
|
34
|
+
this._nextId++;
|
|
35
|
+
}
|
|
36
|
+
const id = this._nextId++;
|
|
37
|
+
assertValidLogicalId(id, context);
|
|
38
|
+
this._used.add(id);
|
|
39
|
+
return id as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
markUsed(id: number, context = 'LogicalId'): void {
|
|
43
|
+
assertValidLogicalId(id, context);
|
|
44
|
+
this._used.add(id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reset(startId = 1): void {
|
|
48
|
+
this._nextId = startId;
|
|
49
|
+
this._used.clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Event map describing all event types emitted across the Galfus Engine runtime.
|
|
55
|
+
*/
|
|
56
|
+
import type {
|
|
57
|
+
AnimationEvent,
|
|
58
|
+
FrameTiming,
|
|
59
|
+
GamepadEvent,
|
|
60
|
+
KeyboardEvent,
|
|
61
|
+
PlatformEvent,
|
|
62
|
+
PointerEvent,
|
|
63
|
+
WindowEvent
|
|
64
|
+
} from '@galfus/transport-types';
|
|
65
|
+
|
|
66
|
+
export interface FixedTickTiming {
|
|
67
|
+
readonly fixedDt: number;
|
|
68
|
+
readonly time: number;
|
|
69
|
+
readonly subStep: number;
|
|
70
|
+
readonly frameId: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RuntimeEventMap {
|
|
74
|
+
tick: FrameTiming;
|
|
75
|
+
fixedTick: FixedTickTiming;
|
|
76
|
+
window: WindowEvent;
|
|
77
|
+
pointer: PointerEvent;
|
|
78
|
+
keyboard: KeyboardEvent;
|
|
79
|
+
gamepad: GamepadEvent;
|
|
80
|
+
animation: AnimationEvent;
|
|
81
|
+
platform: PlatformEvent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type RuntimeEventType = keyof RuntimeEventMap;
|
|
85
|
+
export type RuntimeEventListener<T extends RuntimeEventType> = (event: RuntimeEventMap[T]) => void;
|