0.1.0-1-alpha.2-fix.2

This commit is contained in:
2026-09-17 00:25:37 +02:00
parent f8b93050d8
commit 37a00b3c2b
35 changed files with 620 additions and 181 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-reflex-poc-tauri/Cargo.toml
# version: 1
# version: 3
[package]
name = "game-reflex-poc-tauri"
@@ -9,24 +9,24 @@ license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
build = "build.rs"
[lib]
crate-type = ["cdylib", "rlib"]
name = "game_reflex_poc_tauri_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[dependencies]
engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-reflex-poc = { path = "../../games/game-reflex-poc" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tauri.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
[[bin]]
name = "game-reflex-poc-tauri"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
tauri.workspace = true
tauri-plugin-tracing.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -1,14 +1,9 @@
// file: crates/apps/game-reflex-poc-tauri/build.rs
// version: 2
// version: 3
//! Tauri build script skipped for the WebAssembly library target.
//! Native Tauri build script for the Reflex Desktop WebView runner.
fn main() {
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH");
match target_arch {
std::result::Result::Ok(value) if value == "wasm32" => return,
_ => {},
}
tauri_build::build();
return;
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for Reflex Tauri POC",
"windows": ["main"],
"permissions": ["core:default", "tracing:default"]
}

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Reflex POC Tauri</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main id="app">
<canvas id="game" aria-label="Reflex POC"></canvas>
<output id="score">Score : 0</output>
<output id="runtime">Initialisation…</output>
</main>
<script type="module" src="/ts/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,45 @@
/* file: crates/apps/game-reflex-poc-tauri/frontend/style.css */
/* version: 1 */
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: rgb(18 18 24);
font-family: sans-serif;
}
#app {
position: relative;
width: 100%;
height: 100%;
}
#game {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
#score,
#runtime {
position: absolute;
left: 12px;
padding: 6px 8px;
border-radius: 4px;
background: rgb(0 0 0 / 55%);
color: white;
pointer-events: none;
}
#score {
top: 12px;
}
#runtime {
bottom: 12px;
font-size: 12px;
}

View File

@@ -0,0 +1,12 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
export async function getRuntimeLabel(): Promise<string> {
return await invoke<string>("get_runtime_label");
}
export async function notifyFrontendReady(): Promise<void> {
await invoke("frontend_ready");
}

View File

@@ -0,0 +1,67 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts
// version: 1
import init, { ReflexWasmGame } from "../wasm/game_reflex_poc_wasm.js";
import { tracingDebug, tracingTrace } from "./logging";
const FRAME_MS = 16;
function color(red: number, green: number, blue: number): string {
return `rgb(${red} ${green} ${blue})`;
}
function resizeCanvas(canvas: HTMLCanvasElement): void {
const ratio = window.devicePixelRatio || 1;
const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function render(game: ReflexWasmGame, canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, score: HTMLOutputElement): void {
resizeCanvas(canvas);
context.fillStyle = color(game.background_red(), game.background_green(), game.background_blue());
context.fillRect(0, 0, canvas.width, canvas.height);
const count = game.rectangle_count();
for (let index = 0; index < count; index += 1) {
context.fillStyle = color(game.rectangle_red(index), game.rectangle_green(index), game.rectangle_blue(index));
context.fillRect(
game.rectangle_x(index) * canvas.width,
game.rectangle_y(index) * canvas.height,
game.rectangle_width(index) * canvas.width,
game.rectangle_height(index) * canvas.height,
);
}
score.value = `Score : ${game.score()}`;
}
export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise<void> {
await init();
await tracingDebug("Reflex WASM module initialized");
const context = canvas.getContext("2d");
if (context === null) {
throw new Error("2D canvas context is unavailable");
}
const game = new ReflexWasmGame();
canvas.addEventListener("pointerdown", event => {
const bounds = canvas.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width;
const y = (event.clientY - bounds.top) / bounds.height;
game.pointer_down(x, y);
render(game, canvas, context, score);
void tracingTrace(`pointerdown x=${x.toFixed(4)} y=${y.toFixed(4)} score=${game.score()}`);
});
let previous = performance.now();
function frame(now: number): void {
while (now - previous >= FRAME_MS) {
game.tick();
previous += FRAME_MS;
}
render(game, canvas, context, score);
window.requestAnimationFrame(frame);
}
render(game, canvas, context, score);
window.requestAnimationFrame(frame);
}

View File

@@ -0,0 +1,38 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts
// version: 1
import { attachConsole, debug, error, info, trace, warn } from "@fltsci/tauri-plugin-tracing";
let detachRustConsole: (() => void) | null = null;
export async function initializeTracing(): Promise<void> {
detachRustConsole = await attachConsole();
await info("Reflex Tauri frontend tracing initialized", "games::tauri::reflex::frontend");
}
export async function tracingTrace(message: string): Promise<void> {
await trace(message, "games::tauri::reflex::frontend");
}
export async function tracingDebug(message: string): Promise<void> {
await debug(message, "games::tauri::reflex::frontend");
}
export async function tracingInfo(message: string): Promise<void> {
await info(message, "games::tauri::reflex::frontend");
}
export async function tracingWarn(message: string): Promise<void> {
await warn(message, "games::tauri::reflex::frontend");
}
export async function tracingError(message: string): Promise<void> {
await error(message, "games::tauri::reflex::frontend");
}
export function detachTracingConsole(): void {
if (detachRustConsole !== null) {
detachRustConsole();
detachRustConsole = null;
}
}

View File

@@ -0,0 +1,25 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts
// version: 1
import { getRuntimeLabel, notifyFrontendReady } from "./bridge";
import { startGame } from "./game";
import { initializeTracing, tracingError, tracingInfo } from "./logging";
async function main(): Promise<void> {
await initializeTracing();
const canvas = document.querySelector<HTMLCanvasElement>("#game");
const score = document.querySelector<HTMLOutputElement>("#score");
const runtime = document.querySelector<HTMLOutputElement>("#runtime");
if (canvas === null || score === null || runtime === null) {
throw new Error("Reflex Tauri frontend DOM is incomplete");
}
runtime.value = await getRuntimeLabel();
await startGame(canvas, score);
await notifyFrontendReady();
await tracingInfo("Reflex Tauri frontend started");
}
void main().catch(async caughtError => {
const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError);
await tracingError(`Reflex Tauri frontend startup failed: ${message}`);
});

View File

@@ -0,0 +1,20 @@
{
"name": "game-reflex-poc-tauri",
"private": true,
"version": "0.1.0-1-alpha.2.fix.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
"@tauri-apps/api": "^2.11"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11",
"@types/node": "^26.1",
"typescript": "^7.0",
"vite": "^8.2"
}
}

View File

@@ -1,15 +1,19 @@
// file: crates/apps/game-reflex-poc-tauri/src/lib.rs
// version: 1
// version: 2
//! Tauri desktop application facade for the Reflex WebAssembly POC.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! WebAssembly adapter for the Reflex POC when hosted by the Tauri variant.
mod runtime;
mod tauri;
#[cfg(target_arch = "wasm32")]
mod wasm_runtime;
/// Runs the Reflex Tauri desktop application.
pub use self::tauri::run;
#[cfg(target_arch = "wasm32")]
/// Re-export of the Reflex WebAssembly runtime adapter.
pub use self::wasm_runtime::ReflexWasmGame;
/// Records that the Vite/TypeScript frontend reached its ready state.
pub(crate) use self::runtime::record_frontend_ready;
/// Returns the runtime label exposed by the Tauri bridge.
pub(crate) use self::runtime::runtime_label;

View File

@@ -1,25 +1,20 @@
// file: crates/apps/game-reflex-poc-tauri/src/main.rs
// version: 1
// version: 2
//! Binary entry point for the Reflex Tauri desktop application.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
//! Native Tauri host for the local Reflex WebAssembly POC.
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let result = tauri::Builder::default().run(tauri::generate_context!());
match result {
std::result::Result::Ok(()) => {},
fn main() -> std::process::ExitCode {
let result = game_reflex_poc_tauri_lib::run();
return match result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
eprintln!("Reflex POC Tauri runner failed: {error}");
eprintln!("Reflex Tauri application error: {error}");
std::process::ExitCode::FAILURE
},
}
return;
}
#[cfg(target_arch = "wasm32")]
fn main() {
return;
};
}

View File

@@ -0,0 +1,18 @@
// file: crates/apps/game-reflex-poc-tauri/src/runtime.rs
// version: 1
/// Stable label for the current Tauri/WASM runtime combination.
pub(crate) fn runtime_label() -> &'static str {
return "Desktop / TauriWebView / Wasm / KeyboardMouse";
}
/// Records that the frontend completed its startup sequence.
pub(crate) fn record_frontend_ready() {
tracing::info!(
target: "games::tauri::reflex",
action = "frontend_ready",
runtime = crate::runtime_label(),
"Reflex Tauri frontend ready"
);
return;
}

View File

@@ -0,0 +1,39 @@
// file: crates/apps/game-reflex-poc-tauri/src/tauri.rs
// version: 1
#[tauri::command]
fn get_runtime_label() -> String {
return crate::runtime_label().to_string();
}
#[tauri::command]
fn frontend_ready() {
crate::record_frontend_ready();
return;
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tracing_plugin);
}
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![get_runtime_label, frontend_ready]);
}
/// Runs the Reflex Tauri desktop application.
pub fn run() -> std::result::Result<(), String> {
tracing::info!(
target: "games::tauri::reflex",
action = "runtime_start",
"Starting Reflex Tauri runtime"
);
let builder = tauri::Builder::default();
let builder = configure_plugins(builder);
let builder = configure_commands(builder);
return match builder.run(tauri::generate_context!()) {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}

View File

@@ -1,14 +1,24 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Reflex POC Tauri",
"version": "0.1.0-1-alpha.2.fix.1",
"version": "0.1.0-1-alpha.2.fix.2",
"identifier": "com.sasedev.games.reflex.tauri",
"build": {
"frontendDist": "../../../Tauri/game-reflex-poc/frontend"
"beforeDevCommand": {
"script": "python3 ../../../scripts/build_reflex_tauri_wasm.py && npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1432",
"beforeBuildCommand": {
"script": "python3 ../../../scripts/build_reflex_tauri_wasm.py && npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/sasedev-games/game-reflex-poc-tauri/dist"
},
"app": {
"windows": [
{
"label": "main",
"title": "Reflex POC Tauri",
"width": 405,
"height": 720,
@@ -16,10 +26,11 @@
}
],
"security": {
"csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost"
"csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://localhost:1432 ws://localhost:1433"
}
},
"bundle": {
"active": false
"active": false,
"icon": ["icons/icon.png"]
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["frontend/**/*.ts", "vite.config.ts"]
}

View File

@@ -0,0 +1,36 @@
// file: crates/apps/game-reflex-poc-tauri/vite.config.ts
// version: 1
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";
import { defineConfig, normalizePath } from "vite";
const appRoot = fileURLToPath(new URL(".", import.meta.url));
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/sasedev-games/game-reflex-poc-tauri/dist"));
const devHost = process.env.TAURI_DEV_HOST;
export default defineConfig({
clearScreen: false,
root: frontendRoot,
publicDir: false,
build: {
outDir: frontendDist,
emptyOutDir: true,
minify: true,
sourcemap: false,
},
server: {
port: 1432,
strictPort: true,
host: devHost || false,
ws: {
protocol: "ws",
host: devHost || "localhost",
port: 1433,
},
watch: {
ignored: ["**/src/**"],
},
},
});

View File

@@ -0,0 +1,22 @@
# file: crates/apps/game-reflex-poc-wasm/Cargo.toml
# version: 1
[package]
name = "game-reflex-poc-wasm"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
engine-v1-common = { path = "../../engines/engine-v1-common" }
game-reflex-poc = { path = "../../games/game-reflex-poc" }
wasm-bindgen.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,13 @@
// file: crates/apps/game-reflex-poc-wasm/src/lib.rs
// version: 1
//! WebAssembly adapter facade for the Reflex POC.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod runtime;
/// Re-export of the Reflex WebAssembly runtime adapter.
pub use self::runtime::ReflexWasmGame;

View File

@@ -1,7 +1,7 @@
// file: crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs
// file: crates/apps/game-reflex-poc-wasm/src/runtime.rs
// version: 1
/// WebAssembly-owned Reflex game state consumed by the local Tauri WebView.
/// WebAssembly-owned Reflex game state consumed by the Tauri WebView.
#[wasm_bindgen::prelude::wasm_bindgen]
pub struct ReflexWasmGame {
runner: engine_v1_common::FixedStepRunner,
@@ -10,7 +10,7 @@ pub struct ReflexWasmGame {
#[wasm_bindgen::prelude::wasm_bindgen]
impl ReflexWasmGame {
/// Creates a fresh Reflex POC session using the same gameplay crate as native Desktop.
/// Creates a fresh Reflex POC session using the shared gameplay crate.
#[wasm_bindgen::prelude::wasm_bindgen(constructor)]
pub fn new() -> Self {
return Self {
@@ -65,7 +65,7 @@ impl ReflexWasmGame {
return count;
}
/// Returns one rectangle's normalized left coordinate or `-1.0` if the index is absent.
/// Returns one rectangle's normalized left coordinate or `-1.0` if absent.
pub fn rectangle_x(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().x(),
@@ -73,7 +73,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's normalized top coordinate or `-1.0` if the index is absent.
/// Returns one rectangle's normalized top coordinate or `-1.0` if absent.
pub fn rectangle_y(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().y(),
@@ -81,7 +81,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's normalized width or `0.0` if the index is absent.
/// Returns one rectangle's normalized width or `0.0` if absent.
pub fn rectangle_width(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().width(),
@@ -89,7 +89,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's normalized height or `0.0` if the index is absent.
/// Returns one rectangle's normalized height or `0.0` if absent.
pub fn rectangle_height(&self, index: u32) -> f32 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.rect().height(),
@@ -97,7 +97,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's red channel or zero if the index is absent.
/// Returns one rectangle's red channel or zero if absent.
pub fn rectangle_red(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().red(),
@@ -105,7 +105,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's green channel or zero if the index is absent.
/// Returns one rectangle's green channel or zero if absent.
pub fn rectangle_green(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().green(),
@@ -113,7 +113,7 @@ impl ReflexWasmGame {
};
}
/// Returns one rectangle's blue channel or zero if the index is absent.
/// Returns one rectangle's blue channel or zero if absent.
pub fn rectangle_blue(&self, index: u32) -> u8 {
return match self.rectangle(index) {
Some(rectangle) => rectangle.color().blue(),