@bornengine/engine 0.4.16 → 0.5.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 +24 -14
- package/native/android/Cargo.lock +577 -28
- package/native/android/Cargo.toml +2 -0
- package/native/android/src/lib.rs +39 -3
- package/native/ios/Cargo.lock +573 -24
- package/native/ios/Cargo.toml +2 -0
- package/native/ios/src/lib.rs +72 -9
- package/native/ios/src/text_input.rs +48 -0
- package/native/linux/Cargo.lock +623 -35
- package/native/linux/Cargo.toml +3 -0
- package/native/macos/Cargo.lock +630 -35
- package/native/macos/Cargo.toml +3 -0
- package/native/shared/Cargo.lock +430 -27
- package/native/shared/Cargo.toml +5 -0
- package/native/shared/build.rs +41 -1
- package/native/shared/src/engine.rs +54 -2
- package/native/shared/src/ffi.rs +54 -1
- package/native/shared/src/ffi_core/mod.rs +2 -0
- package/native/shared/src/ffi_core/ui.rs +182 -0
- package/native/shared/src/input.rs +317 -12
- package/native/shared/src/lib.rs +1 -0
- package/native/shared/src/renderer/mod.rs +47 -201
- package/native/shared/src/renderer/texture_store.rs +5 -0
- package/native/shared/src/renderer/ui_pass.rs +742 -0
- package/native/shared/src/textures.rs +2 -0
- package/native/shared/src/ui/commands.rs +213 -0
- package/native/shared/src/ui/egui.rs +1282 -0
- package/native/shared/src/ui/imgui.rs +1056 -0
- package/native/shared/src/ui/input.rs +173 -0
- package/native/shared/src/ui/mod.rs +252 -0
- package/native/shared/src/ui/responses.rs +61 -0
- package/native/tvos/Cargo.lock +576 -27
- package/native/tvos/Cargo.toml +2 -0
- package/native/visionos/Cargo.lock +571 -22
- package/native/visionos/Cargo.toml +2 -0
- package/native/watchos/src/ffi_stubs.rs +27 -0
- package/native/web/Cargo.lock +346 -11
- package/native/web/Cargo.toml +6 -0
- package/native/web/bloom_glue.js +84 -11
- package/native/web/build.sh +150 -34
- package/native/web/src/lib.rs +2 -0
- package/native/web/src/ui_ffi.rs +148 -0
- package/native/windows/Cargo.lock +638 -38
- package/native/windows/Cargo.toml +3 -0
- package/native/windows/src/lib.rs +50 -1
- package/native/windows/src/windows_text.rs +69 -0
- package/package.json +91 -2
- package/src/debug-ui/index.ts +4 -0
- package/src/debug-ui/types.ts +1 -0
- package/src/game/adapters/audio-source-component.ts +67 -0
- package/src/game/adapters/rigid-body-component.ts +69 -0
- package/src/game/adapters/scene-node-component.ts +49 -0
- package/src/game/game-component.ts +89 -0
- package/src/game/game-object.ts +338 -0
- package/src/game/game-scene.ts +303 -0
- package/src/game/index.ts +7 -0
- package/src/game/transform.ts +315 -0
- package/src/index.ts +27 -0
- package/src/ui/api.ts +216 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/opcodes.ts +66 -0
- package/src/ui/types.ts +82 -0
package/native/web/bloom_glue.js
CHANGED
|
@@ -52,6 +52,8 @@ let manifestPaths = null; // Set<string> when a manifest was loaded
|
|
|
52
52
|
// browser only grants pointer lock inside a user gesture, and revokes it on
|
|
53
53
|
// ESC without delivering the keydown. Both mismatches are reconciled here.
|
|
54
54
|
let wantPointerLock = false;
|
|
55
|
+
let uiTextInput = null;
|
|
56
|
+
let uiTextInputFocused = false;
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Idempotent engine bootstrap. Safe to call multiple times; only the first
|
|
@@ -204,6 +206,14 @@ function buildFfiImports() {
|
|
|
204
206
|
imports.bloom_measure_text_ex = (font, text, size, spacing) =>
|
|
205
207
|
bloom.bloom_measure_text_ex_str(font, String(text), size, spacing);
|
|
206
208
|
|
|
209
|
+
// Route whole-string UI commands through wasm-bindgen string parameters.
|
|
210
|
+
imports.bloom_ui_command = (backend, opcode, id, a, b, c, d, text) =>
|
|
211
|
+
bloom.bloom_ui_command_str(backend, opcode, id, a, b, c, d, String(text));
|
|
212
|
+
imports.bloom_ui_scratch_command = (backend, opcode, id, count, text) =>
|
|
213
|
+
bloom.bloom_ui_scratch_command_str(backend, opcode, id, count, String(text));
|
|
214
|
+
imports.bloom_ui_inject_text = (text) =>
|
|
215
|
+
bloom.bloom_ui_inject_text_str(String(text));
|
|
216
|
+
|
|
207
217
|
// --- Materials & post-FX: shader source strings route to _str variants ---
|
|
208
218
|
const materialVariants = [
|
|
209
219
|
'bloom_compile_material', 'bloom_compile_material_refractive',
|
|
@@ -384,6 +394,7 @@ function startRafLoop() {
|
|
|
384
394
|
document.getElementById('loading')?.remove();
|
|
385
395
|
}
|
|
386
396
|
flushInput();
|
|
397
|
+
syncUiKeyboardRequest();
|
|
387
398
|
bloom.bloom_begin_drawing();
|
|
388
399
|
if (gameCallback !== null && typeof globalThis.callWasmClosure === 'function') {
|
|
389
400
|
const dt = bloom.bloom_get_delta_time();
|
|
@@ -520,6 +531,44 @@ function setupDomBridge() {
|
|
|
520
531
|
const canvas = document.getElementById('bloom-canvas');
|
|
521
532
|
if (!canvas) return;
|
|
522
533
|
|
|
534
|
+
// Browsers deliver text and IME composition through DOM editing events,
|
|
535
|
+
// not keyboard key codes. Keep a real focusable control so mobile browsers
|
|
536
|
+
// can show their soft keyboard, while visually leaving the game canvas
|
|
537
|
+
// unchanged.
|
|
538
|
+
uiTextInput = document.createElement('textarea');
|
|
539
|
+
uiTextInput.setAttribute('aria-label', 'Game text input');
|
|
540
|
+
uiTextInput.autocomplete = 'off';
|
|
541
|
+
uiTextInput.autocapitalize = 'off';
|
|
542
|
+
uiTextInput.spellcheck = false;
|
|
543
|
+
Object.assign(uiTextInput.style, {
|
|
544
|
+
position: 'fixed', left: '0', bottom: '0', width: '1px', height: '1px',
|
|
545
|
+
padding: '0', border: '0', opacity: '0', resize: 'none', overflow: 'hidden',
|
|
546
|
+
zIndex: '-1', caretColor: 'transparent',
|
|
547
|
+
});
|
|
548
|
+
document.body.appendChild(uiTextInput);
|
|
549
|
+
let composing = false;
|
|
550
|
+
uiTextInput.addEventListener('compositionstart', () => { composing = true; });
|
|
551
|
+
uiTextInput.addEventListener('compositionend', (e) => {
|
|
552
|
+
composing = false;
|
|
553
|
+
if (e.data) bloom.bloom_ui_inject_text(String(e.data));
|
|
554
|
+
uiTextInput.value = '';
|
|
555
|
+
});
|
|
556
|
+
uiTextInput.addEventListener('input', (e) => {
|
|
557
|
+
if (composing || e.isComposing || e.inputType === 'insertFromComposition') {
|
|
558
|
+
uiTextInput.value = '';
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (e.inputType?.startsWith('insert') && e.data) {
|
|
562
|
+
bloom.bloom_ui_inject_text(String(e.data));
|
|
563
|
+
}
|
|
564
|
+
uiTextInput.value = '';
|
|
565
|
+
});
|
|
566
|
+
uiTextInput.addEventListener('paste', (e) => {
|
|
567
|
+
const text = e.clipboardData?.getData('text/plain') ?? '';
|
|
568
|
+
if (text) bloom.bloom_ui_inject_text(text);
|
|
569
|
+
e.preventDefault();
|
|
570
|
+
});
|
|
571
|
+
|
|
523
572
|
// HiDPI: keep the canvas backing store + renderer surface in sync with the
|
|
524
573
|
// CSS box and devicePixelRatio. Clamp dpr to 3 to bound GPU cost.
|
|
525
574
|
function syncCanvasSize() {
|
|
@@ -570,10 +619,10 @@ function setupDomBridge() {
|
|
|
570
619
|
const code = keyMap[e.code];
|
|
571
620
|
if (code !== undefined) {
|
|
572
621
|
inputQueue.push({ kind: 'key', k: 'k' + code, code, up: false });
|
|
573
|
-
if (e.code !== 'F12' && e.code !== 'F5') e.preventDefault();
|
|
622
|
+
if (!uiTextInputFocused && e.code !== 'F12' && e.code !== 'F5') e.preventDefault();
|
|
574
623
|
}
|
|
575
624
|
if (typeof bloom.bloom_inject_char === 'function'
|
|
576
|
-
&& e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
625
|
+
&& !uiTextInputFocused && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
577
626
|
bloom.bloom_inject_char(e.key.codePointAt(0));
|
|
578
627
|
}
|
|
579
628
|
});
|
|
@@ -634,14 +683,21 @@ function setupDomBridge() {
|
|
|
634
683
|
// Touch. Browser touch identifiers are arbitrary; map them onto stable
|
|
635
684
|
// slots 0..N-1 the way the engine's sparse touch API expects. Release goes
|
|
636
685
|
// through the deferred path so a same-frame tap stays visible.
|
|
637
|
-
const touchSlots = new Map(); // identifier → slot
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
686
|
+
const touchSlots = new Map(); // browser identifier → stable engine slot
|
|
687
|
+
const touchCapacity = Math.max(0, Math.min(10,
|
|
688
|
+
(typeof bloom.bloom_get_max_touch_points === 'function'
|
|
689
|
+
? bloom.bloom_get_max_touch_points() : 10) | 0));
|
|
690
|
+
function slotForStart(id) {
|
|
691
|
+
const existing = touchSlots.get(id);
|
|
692
|
+
if (existing !== undefined) return existing;
|
|
641
693
|
const used = new Set(touchSlots.values());
|
|
642
|
-
for (
|
|
643
|
-
|
|
644
|
-
|
|
694
|
+
for (let slot = 0; slot < touchCapacity; slot++) {
|
|
695
|
+
if (!used.has(slot)) {
|
|
696
|
+
touchSlots.set(id, slot);
|
|
697
|
+
return slot;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return undefined;
|
|
645
701
|
}
|
|
646
702
|
function touchXY(t) {
|
|
647
703
|
const rect = canvas.getBoundingClientRect();
|
|
@@ -650,15 +706,19 @@ function setupDomBridge() {
|
|
|
650
706
|
canvas.addEventListener('touchstart', (e) => {
|
|
651
707
|
resumeAudio();
|
|
652
708
|
for (const t of e.changedTouches) {
|
|
709
|
+
const slot = slotForStart(t.identifier);
|
|
710
|
+
if (slot === undefined) continue;
|
|
653
711
|
const [x, y] = touchXY(t);
|
|
654
|
-
bloom.bloom_inject_touch(
|
|
712
|
+
bloom.bloom_inject_touch(slot, x, y, 1);
|
|
655
713
|
}
|
|
656
714
|
e.preventDefault();
|
|
657
715
|
}, { passive: false });
|
|
658
716
|
canvas.addEventListener('touchmove', (e) => {
|
|
659
717
|
for (const t of e.changedTouches) {
|
|
718
|
+
const slot = touchSlots.get(t.identifier);
|
|
719
|
+
if (slot === undefined) continue;
|
|
660
720
|
const [x, y] = touchXY(t);
|
|
661
|
-
bloom.bloom_inject_touch(
|
|
721
|
+
bloom.bloom_inject_touch(slot, x, y, 1);
|
|
662
722
|
}
|
|
663
723
|
e.preventDefault();
|
|
664
724
|
}, { passive: false });
|
|
@@ -678,6 +738,19 @@ function setupDomBridge() {
|
|
|
678
738
|
document.addEventListener('pointerdown', resumeAudio);
|
|
679
739
|
}
|
|
680
740
|
|
|
741
|
+
function syncUiKeyboardRequest() {
|
|
742
|
+
if (!uiTextInput || typeof bloom.bloom_ui_take_keyboard_request !== 'function') return;
|
|
743
|
+
const request = bloom.bloom_ui_take_keyboard_request(0);
|
|
744
|
+
if (request > 0.5 && !uiTextInputFocused) {
|
|
745
|
+
uiTextInputFocused = true;
|
|
746
|
+
uiTextInput.focus({ preventScroll: true });
|
|
747
|
+
} else if (request >= 0 && request <= 0.5 && uiTextInputFocused) {
|
|
748
|
+
uiTextInputFocused = false;
|
|
749
|
+
uiTextInput.blur();
|
|
750
|
+
uiTextInput.value = '';
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
681
754
|
// --- Auto-boot on import ---
|
|
682
755
|
// The page creates `window.__bloomReady` (a pending promise) in a classic
|
|
683
756
|
// inline <script> during parse; the gated `bootPerryWasm(...)` call awaits it.
|
package/native/web/build.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Build Bloom Engine for Web
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
# ./native/web/build.sh [game.ts] [--output dist/]
|
|
5
|
+
# ./native/web/build.sh [--dev | --release] [game.ts] [--output dist/]
|
|
6
6
|
#
|
|
7
7
|
# Steps:
|
|
8
8
|
# 1. Build bloom_web.wasm via wasm-pack
|
|
@@ -12,54 +12,167 @@
|
|
|
12
12
|
# Prerequisites:
|
|
13
13
|
# - wasm-pack: cargo install wasm-pack
|
|
14
14
|
# - perry: ../../../perry/perry/target/release/perry (or in PATH)
|
|
15
|
-
# - wasm-opt (optional): for binary size optimization
|
|
16
15
|
|
|
17
|
-
set -
|
|
16
|
+
set -euo pipefail
|
|
18
17
|
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
-
ENGINE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
19
|
+
ENGINE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
|
|
21
20
|
WEB_CRATE="$SCRIPT_DIR"
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
BUILD_PROFILE="--release"
|
|
22
|
+
PROFILE_SET=0
|
|
23
|
+
OUTPUT_DIR=""
|
|
24
|
+
GAME_FILE=""
|
|
25
|
+
GAME_FILE_INPUT=""
|
|
26
|
+
GAME_DIR=""
|
|
27
|
+
|
|
28
|
+
usage() {
|
|
29
|
+
cat <<'USAGE'
|
|
30
|
+
Usage: ./native/web/build.sh [--dev | --release] [game.ts] [--output dist/]
|
|
31
|
+
|
|
32
|
+
Profiles:
|
|
33
|
+
--dev Faster iterative build; skips wasm-opt.
|
|
34
|
+
--release Optimized build (default); runs wasm-opt once with -Oz.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--output DIR Write the assembled site to DIR (default: dist/web).
|
|
38
|
+
-h, --help Show this help.
|
|
39
|
+
USAGE
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
usage_error() {
|
|
43
|
+
echo "ERROR: $1" >&2
|
|
44
|
+
usage >&2
|
|
45
|
+
exit 2
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
while (($#)); do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--dev|--release)
|
|
51
|
+
if ((PROFILE_SET)) && [[ "$BUILD_PROFILE" != "$1" ]]; then
|
|
52
|
+
usage_error "--dev and --release cannot be used together"
|
|
53
|
+
fi
|
|
54
|
+
BUILD_PROFILE="$1"
|
|
55
|
+
PROFILE_SET=1
|
|
56
|
+
shift
|
|
57
|
+
;;
|
|
58
|
+
--output)
|
|
59
|
+
(($# >= 2)) || usage_error "--output requires a directory"
|
|
60
|
+
[[ -n "$2" ]] || usage_error "--output requires a directory"
|
|
61
|
+
OUTPUT_DIR="$2"
|
|
62
|
+
shift 2
|
|
63
|
+
;;
|
|
64
|
+
--output=*)
|
|
65
|
+
OUTPUT_DIR="${1#*=}"
|
|
66
|
+
[[ -n "$OUTPUT_DIR" ]] || usage_error "--output requires a directory"
|
|
67
|
+
shift
|
|
68
|
+
;;
|
|
69
|
+
-h|--help)
|
|
70
|
+
usage
|
|
71
|
+
exit 0
|
|
72
|
+
;;
|
|
73
|
+
-* )
|
|
74
|
+
usage_error "unknown option: $1"
|
|
75
|
+
;;
|
|
76
|
+
*)
|
|
77
|
+
[[ -z "$GAME_FILE" ]] || usage_error "only one game file may be provided"
|
|
78
|
+
GAME_FILE="$1"
|
|
79
|
+
shift
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
OUTPUT_DIR="${OUTPUT_DIR:-$ENGINE_DIR/dist/web}"
|
|
85
|
+
if [[ "$OUTPUT_DIR" != /* ]]; then
|
|
86
|
+
OUTPUT_DIR="$PWD/$OUTPUT_DIR"
|
|
87
|
+
fi
|
|
88
|
+
OUTPUT_DIR="$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$OUTPUT_DIR")"
|
|
24
89
|
|
|
25
|
-
# Resolve
|
|
26
|
-
#
|
|
27
|
-
# relative path like `examples/pong/main.ts` would otherwise no longer resolve.
|
|
90
|
+
# Resolve game paths before validating output: cleanup must never remove game
|
|
91
|
+
# source files or the assets directory that will be copied into the build.
|
|
28
92
|
if [ -n "$GAME_FILE" ]; then
|
|
29
93
|
if [ -f "$GAME_FILE" ]; then
|
|
30
|
-
|
|
94
|
+
GAME_FILE_INPUT="$(python3 -c 'import os, sys; path = sys.argv[1]; print(os.path.join(os.path.realpath(os.path.dirname(path) or "."), os.path.basename(path)))' "$GAME_FILE")"
|
|
95
|
+
GAME_FILE="$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$GAME_FILE_INPUT")"
|
|
96
|
+
GAME_DIR="$(dirname "$GAME_FILE")"
|
|
31
97
|
else
|
|
32
98
|
echo "ERROR: game file not found: $GAME_FILE"
|
|
33
99
|
exit 1
|
|
34
100
|
fi
|
|
35
101
|
fi
|
|
36
102
|
|
|
103
|
+
if [[ "$OUTPUT_DIR" == "/" || "$OUTPUT_DIR" == "$WEB_CRATE" || "$OUTPUT_DIR" == "$WEB_CRATE/"* || "$WEB_CRATE" == "$OUTPUT_DIR/"* ]]; then
|
|
104
|
+
usage_error "--output must not overlap the native/web source tree"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Resolve game-output cleanup paths against the source file and its assets
|
|
108
|
+
# directory. This permits common output folders such as <game>/dist/web while
|
|
109
|
+
# rejecting destinations whose owned artifacts would erase game inputs.
|
|
110
|
+
if [ -n "$GAME_FILE" ]; then
|
|
111
|
+
if ! python3 - "$OUTPUT_DIR" "$GAME_FILE_INPUT" "$GAME_FILE" "$GAME_DIR" <<'PY'
|
|
112
|
+
import os
|
|
113
|
+
import sys
|
|
114
|
+
|
|
115
|
+
output_dir, game_file_input, game_file_target, game_dir = sys.argv[1:]
|
|
116
|
+
output_dir = os.path.realpath(output_dir)
|
|
117
|
+
game_file_input = os.path.abspath(game_file_input)
|
|
118
|
+
game_file_target = os.path.realpath(game_file_target)
|
|
119
|
+
game_assets_target = os.path.join(os.path.realpath(game_dir), "assets")
|
|
120
|
+
game_assets_input = os.path.join(os.path.dirname(game_file_input), "assets")
|
|
121
|
+
cleaned_paths = [
|
|
122
|
+
os.path.join(output_dir, "pkg"),
|
|
123
|
+
os.path.join(output_dir, "assets"),
|
|
124
|
+
os.path.join(output_dir, "index.html"),
|
|
125
|
+
os.path.join(output_dir, "bloom_glue.js"),
|
|
126
|
+
os.path.join(output_dir, "jolt_bridge.js"),
|
|
127
|
+
]
|
|
128
|
+
source_paths = [game_file_input, game_file_target]
|
|
129
|
+
for game_assets in (game_assets_input, game_assets_target):
|
|
130
|
+
if os.path.lexists(game_assets):
|
|
131
|
+
source_paths.extend((os.path.abspath(game_assets), os.path.realpath(game_assets)))
|
|
132
|
+
|
|
133
|
+
cleanup_aliases = []
|
|
134
|
+
for cleaned in cleaned_paths:
|
|
135
|
+
cleaned = os.path.abspath(cleaned)
|
|
136
|
+
cleanup_aliases.extend((cleaned, os.path.realpath(cleaned)))
|
|
137
|
+
|
|
138
|
+
def overlaps(left, right):
|
|
139
|
+
try:
|
|
140
|
+
common = os.path.commonpath((left, right))
|
|
141
|
+
if common == left or common == right:
|
|
142
|
+
return True
|
|
143
|
+
return os.path.lexists(left) and os.path.lexists(right) and os.path.samefile(left, right)
|
|
144
|
+
except (OSError, ValueError):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
sys.exit(any(overlaps(cleaned, source) for cleaned in cleanup_aliases for source in source_paths))
|
|
148
|
+
PY
|
|
149
|
+
then
|
|
150
|
+
usage_error "--output overlaps with game source inputs"
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
PERRY_TMP=""
|
|
155
|
+
cleanup() {
|
|
156
|
+
if [[ -n "$PERRY_TMP" ]]; then
|
|
157
|
+
rm -rf "$PERRY_TMP"
|
|
158
|
+
fi
|
|
159
|
+
}
|
|
160
|
+
trap cleanup EXIT
|
|
161
|
+
|
|
37
162
|
echo "=== Bloom Web Build ==="
|
|
163
|
+
echo " Profile: $BUILD_PROFILE"
|
|
38
164
|
echo ""
|
|
39
165
|
|
|
40
166
|
# 1. Build Bloom WASM via wasm-pack
|
|
41
|
-
echo "[1/
|
|
167
|
+
echo "[1/3] Building bloom_web.wasm..."
|
|
42
168
|
cd "$WEB_CRATE"
|
|
43
|
-
wasm-pack build --target web --out-dir pkg --no-typescript 2>&1 | tail -3
|
|
169
|
+
wasm-pack build --target web --out-dir pkg --no-typescript "$BUILD_PROFILE" 2>&1 | tail -3
|
|
44
170
|
echo " Output: $WEB_CRATE/pkg/"
|
|
45
171
|
|
|
46
|
-
# 2.
|
|
47
|
-
if command -v wasm-opt &> /dev/null; then
|
|
48
|
-
echo "[2/4] Optimizing WASM with wasm-opt..."
|
|
49
|
-
WASM_FILE="$WEB_CRATE/pkg/bloom_web_bg.wasm"
|
|
50
|
-
ORIG_SIZE=$(wc -c < "$WASM_FILE")
|
|
51
|
-
wasm-opt -Oz "$WASM_FILE" -o "$WASM_FILE.opt"
|
|
52
|
-
mv "$WASM_FILE.opt" "$WASM_FILE"
|
|
53
|
-
OPT_SIZE=$(wc -c < "$WASM_FILE")
|
|
54
|
-
echo " Optimized: $((ORIG_SIZE / 1024))KB → $((OPT_SIZE / 1024))KB"
|
|
55
|
-
else
|
|
56
|
-
echo "[2/4] Skipping wasm-opt (not installed). Install with: cargo install wasm-opt"
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# 3. Compile game (if provided)
|
|
172
|
+
# 2. Compile game (if provided)
|
|
60
173
|
PERRY_HTML=""
|
|
61
174
|
if [ -n "$GAME_FILE" ] && [ -f "$GAME_FILE" ]; then
|
|
62
|
-
echo "[3
|
|
175
|
+
echo "[2/3] Compiling game: $GAME_FILE"
|
|
63
176
|
|
|
64
177
|
# Find perry compiler
|
|
65
178
|
PERRY=""
|
|
@@ -78,16 +191,21 @@ if [ -n "$GAME_FILE" ] && [ -f "$GAME_FILE" ]; then
|
|
|
78
191
|
# Use a temp dir (portable across GNU/BSD mktemp) so cleanup is a single rm.
|
|
79
192
|
PERRY_TMP="$(mktemp -d)"
|
|
80
193
|
PERRY_HTML="$PERRY_TMP/game.html"
|
|
81
|
-
$PERRY "$GAME_FILE" --target wasm -o "$PERRY_HTML"
|
|
194
|
+
"$PERRY" "$GAME_FILE" --target wasm -o "$PERRY_HTML"
|
|
82
195
|
echo " Game compiled to WASM ($PERRY_HTML)"
|
|
83
196
|
else
|
|
84
|
-
echo "[3
|
|
197
|
+
echo "[2/3] No game file specified, skipping game compilation"
|
|
85
198
|
fi
|
|
86
199
|
|
|
87
|
-
#
|
|
88
|
-
echo "[
|
|
200
|
+
# 3. Assemble output directory
|
|
201
|
+
echo "[3/3] Assembling output..."
|
|
89
202
|
mkdir -p "$OUTPUT_DIR"
|
|
90
203
|
|
|
204
|
+
# Replace artifacts owned by this build so repeated builds never nest pkg or
|
|
205
|
+
# assets directories, or leave stale files from the previous game/profile.
|
|
206
|
+
rm -rf "$OUTPUT_DIR/pkg" "$OUTPUT_DIR/assets"
|
|
207
|
+
rm -f "$OUTPUT_DIR/index.html" "$OUTPUT_DIR/bloom_glue.js" "$OUTPUT_DIR/jolt_bridge.js"
|
|
208
|
+
|
|
91
209
|
# Copy Bloom WASM package
|
|
92
210
|
cp -r "$WEB_CRATE/pkg" "$OUTPUT_DIR/pkg"
|
|
93
211
|
|
|
@@ -99,7 +217,6 @@ if [ -n "$PERRY_HTML" ]; then
|
|
|
99
217
|
# Game build: splice the Bloom bootstrap into Perry's HTML and gate the game's
|
|
100
218
|
# bootPerryWasm() call on engine readiness → dist/web/index.html.
|
|
101
219
|
python3 "$WEB_CRATE/splice_game.py" "$PERRY_HTML" "$OUTPUT_DIR/index.html"
|
|
102
|
-
rm -rf "$PERRY_TMP"
|
|
103
220
|
echo " Spliced game + engine into index.html"
|
|
104
221
|
else
|
|
105
222
|
# No game: ship the engine-only standalone page.
|
|
@@ -109,7 +226,6 @@ fi
|
|
|
109
226
|
|
|
110
227
|
# Copy game assets (if game directory has an assets/ folder)
|
|
111
228
|
if [ -n "$GAME_FILE" ]; then
|
|
112
|
-
GAME_DIR="$(dirname "$(realpath "$GAME_FILE")")"
|
|
113
229
|
if [ -d "$GAME_DIR/assets" ]; then
|
|
114
230
|
cp -r "$GAME_DIR/assets" "$OUTPUT_DIR/assets"
|
|
115
231
|
echo " Copied assets/ directory"
|
package/native/web/src/lib.rs
CHANGED
|
@@ -313,7 +313,9 @@ pub fn bloom_get_time() -> f64 {
|
|
|
313
313
|
// in input_ffi.rs (2000-line file policy).
|
|
314
314
|
mod input_ffi;
|
|
315
315
|
mod material_ffi;
|
|
316
|
+
mod ui_ffi;
|
|
316
317
|
pub use input_ffi::*;
|
|
318
|
+
pub use ui_ffi::*;
|
|
317
319
|
|
|
318
320
|
// EN-063 — web FFI parity for full-3D games: mesh/instance/texture-array
|
|
319
321
|
// scratch builders, staged-model bytes, env-HDR bytes, profiler text,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
use super::engine;
|
|
2
|
+
use bloom_shared::ui::{UiBackend, UiCommand, UiOpcode};
|
|
3
|
+
use wasm_bindgen::prelude::*;
|
|
4
|
+
|
|
5
|
+
#[wasm_bindgen]
|
|
6
|
+
pub fn bloom_ui_command_str(
|
|
7
|
+
backend: f64,
|
|
8
|
+
opcode: f64,
|
|
9
|
+
id: f64,
|
|
10
|
+
a: f64,
|
|
11
|
+
b: f64,
|
|
12
|
+
c: f64,
|
|
13
|
+
d: f64,
|
|
14
|
+
text: &str,
|
|
15
|
+
) -> f64 {
|
|
16
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
17
|
+
return 0.0;
|
|
18
|
+
};
|
|
19
|
+
let Some(opcode) = UiOpcode::from_abi(opcode) else {
|
|
20
|
+
return 0.0;
|
|
21
|
+
};
|
|
22
|
+
let Some(id) = bloom_shared::ui::id_from_abi(id) else {
|
|
23
|
+
return 0.0;
|
|
24
|
+
};
|
|
25
|
+
if engine()
|
|
26
|
+
.ui
|
|
27
|
+
.queue_command(UiCommand::new(backend, opcode, id, [a, b, c, d], text))
|
|
28
|
+
{
|
|
29
|
+
1.0
|
|
30
|
+
} else {
|
|
31
|
+
0.0
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[wasm_bindgen]
|
|
36
|
+
pub fn bloom_ui_scratch_reset(backend: f64) {
|
|
37
|
+
if let Some(backend) = UiBackend::from_abi(backend) {
|
|
38
|
+
engine().ui.reset_scratch(backend);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[wasm_bindgen]
|
|
43
|
+
pub fn bloom_ui_scratch_push_f64(backend: f64, value: f64) {
|
|
44
|
+
if let Some(backend) = UiBackend::from_abi(backend) {
|
|
45
|
+
engine().ui.push_scratch(backend, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[wasm_bindgen]
|
|
50
|
+
pub fn bloom_ui_scratch_command_str(
|
|
51
|
+
backend: f64,
|
|
52
|
+
opcode: f64,
|
|
53
|
+
id: f64,
|
|
54
|
+
count: f64,
|
|
55
|
+
text: &str,
|
|
56
|
+
) -> f64 {
|
|
57
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
58
|
+
return 0.0;
|
|
59
|
+
};
|
|
60
|
+
let Some(opcode) = UiOpcode::from_abi(opcode) else {
|
|
61
|
+
return 0.0;
|
|
62
|
+
};
|
|
63
|
+
let Some(id) = bloom_shared::ui::id_from_abi(id) else {
|
|
64
|
+
return 0.0;
|
|
65
|
+
};
|
|
66
|
+
let Some(count) = bloom_shared::ui::id_from_abi(count) else {
|
|
67
|
+
return 0.0;
|
|
68
|
+
};
|
|
69
|
+
if engine()
|
|
70
|
+
.ui
|
|
71
|
+
.queue_scratch_command(backend, opcode, id, count as usize, text)
|
|
72
|
+
{
|
|
73
|
+
1.0
|
|
74
|
+
} else {
|
|
75
|
+
0.0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[wasm_bindgen]
|
|
80
|
+
pub fn bloom_ui_inject_text_str(text: &str) {
|
|
81
|
+
engine().ui.inject_text(text.to_owned());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[wasm_bindgen]
|
|
85
|
+
pub fn bloom_ui_response(backend: f64, id: f64, field: f64) -> f64 {
|
|
86
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
87
|
+
return 0.0;
|
|
88
|
+
};
|
|
89
|
+
let Some(id) = bloom_shared::ui::id_from_abi(id) else {
|
|
90
|
+
return 0.0;
|
|
91
|
+
};
|
|
92
|
+
let Some(field) = bloom_shared::ui::id_from_abi(field) else {
|
|
93
|
+
return 0.0;
|
|
94
|
+
};
|
|
95
|
+
let response = engine().ui.response(backend, id);
|
|
96
|
+
match field {
|
|
97
|
+
0 => response.clicked as u8 as f64,
|
|
98
|
+
1 => response.changed as u8 as f64,
|
|
99
|
+
2 => response.hovered as u8 as f64,
|
|
100
|
+
3 => response.focused as u8 as f64,
|
|
101
|
+
4 => response.dragged as u8 as f64,
|
|
102
|
+
5 => response.value,
|
|
103
|
+
6 => engine().ui.has_response(backend, id) as u8 as f64,
|
|
104
|
+
_ => 0.0,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[wasm_bindgen]
|
|
109
|
+
pub fn bloom_ui_response_text(backend: f64, id: f64) -> String {
|
|
110
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
111
|
+
return String::new();
|
|
112
|
+
};
|
|
113
|
+
let Some(id) = bloom_shared::ui::id_from_abi(id) else {
|
|
114
|
+
return String::new();
|
|
115
|
+
};
|
|
116
|
+
engine().ui.response_text(backend, id)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[wasm_bindgen]
|
|
120
|
+
pub fn bloom_ui_is_available(backend: f64) -> f64 {
|
|
121
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
122
|
+
return 0.0;
|
|
123
|
+
};
|
|
124
|
+
engine().ui.is_available(backend) as u8 as f64
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[wasm_bindgen]
|
|
128
|
+
pub fn bloom_ui_wants_input(backend: f64, kind: f64) -> f64 {
|
|
129
|
+
let Some(backend) = UiBackend::from_abi(backend) else {
|
|
130
|
+
return 0.0;
|
|
131
|
+
};
|
|
132
|
+
let Some(kind) = bloom_shared::ui::id_from_abi(kind) else {
|
|
133
|
+
return 0.0;
|
|
134
|
+
};
|
|
135
|
+
engine().ui.wants_input(backend, kind) as u8 as f64
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[wasm_bindgen]
|
|
139
|
+
pub fn bloom_ui_take_keyboard_request(backend: f64) -> f64 {
|
|
140
|
+
let Some(UiBackend::Egui) = UiBackend::from_abi(backend) else {
|
|
141
|
+
return -1.0;
|
|
142
|
+
};
|
|
143
|
+
match engine().ui.take_keyboard_request() {
|
|
144
|
+
Some(true) => 1.0,
|
|
145
|
+
Some(false) => 0.0,
|
|
146
|
+
None => -1.0,
|
|
147
|
+
}
|
|
148
|
+
}
|