diff --git a/examples/dart/js_wasm/pubspec.yaml b/examples/dart/js_wasm/pubspec.yaml new file mode 100644 index 00000000..171e4b10 --- /dev/null +++ b/examples/dart/js_wasm/pubspec.yaml @@ -0,0 +1,21 @@ +name: example_web +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.0 + +dependencies: + thermion_dart: + path: ../../../thermion_dart + native_toolchain_c: ^0.9.0 + native_assets_cli: ^0.12.0 + logging: ^1.3.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 + build_runner: ^2.4.13 + build_test: ^2.2.2 + build_web_compilers: ^4.0.11 diff --git a/examples/dart/js_wasm/web/assets b/examples/dart/js_wasm/web/assets new file mode 120000 index 00000000..f86487ff --- /dev/null +++ b/examples/dart/js_wasm/web/assets @@ -0,0 +1 @@ +../../../assets/ \ No newline at end of file diff --git a/examples/dart/js_wasm/web/example.dart b/examples/dart/js_wasm/web/example.dart new file mode 100644 index 00000000..9dcf029a --- /dev/null +++ b/examples/dart/js_wasm/web/example.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:math'; +import 'package:web/web.dart'; +import 'package:logging/logging.dart'; +import 'package:thermion_dart/thermion_dart.dart' hide NativeLibrary; +import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart'; +import 'package:thermion_dart/src/filament/src/implementation/resource_loader.dart'; +import 'web_input_handler.dart'; +import 'package:thermion_dart/src/bindings/src/thermion_dart_js_interop.g.dart'; + +void main(List arguments) async { + Logger.root.onRecord.listen((record) { + print(record); + }); + + NativeLibrary.initBindings("thermion_dart"); + + final canvas = + document.getElementById("thermion_canvas") as HTMLCanvasElement; + try { + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; + } catch (err) { + print(err.toString()); + } + + final config = + FFIFilamentConfig(sharedContext: nullptr.cast(), backend: Backend.OPENGL); + + await FFIFilamentApp.create(config: config); + + var swapChain = await FilamentApp.instance! + .createHeadlessSwapChain(canvas.width, canvas.height); + final viewer = ThermionViewerFFI(loadAssetFromUri: defaultResourceLoader); + await viewer.initialized; + await FilamentApp.instance!.setClearOptions(1.0, 0.0, 0.0, 1.0); + await FilamentApp.instance!.register(swapChain, viewer.view); + await viewer.setViewport(canvas.width, canvas.height); + await viewer.setRendering(true); + final rnd = Random(); + // ignore: prefer_function_declarations_over_variables + bool resizing = false; + // ignore: prefer_function_declarations_over_variables + final resizer = () async { + if (resizing) { + return; + } + try { + resizing = true; + await viewer.setViewport(canvas.clientWidth, canvas.clientHeight); + Thermion_resizeCanvas(canvas.clientWidth, canvas.clientHeight); + } catch (err) { + print(err); + } finally { + resizing = false; + } + }; + // ignore: unused_local_variable, prefer_function_declarations_over_variables + final jsWrapper = () { + var promise = resizer().toJS; + return promise; + }; + window.addEventListener('resize', jsWrapper.toJS); + // // await FilamentApp.instance!.render(); + // // await Future.delayed(Duration(seconds: 1)); + + // // await FilamentApp.instance!.setClearOptions(1.0, 1.0, 0.0, 1.0); + // // await FilamentApp.instance!.render(); + // // await Future.delayed(Duration(seconds: 1)); + + await viewer.loadSkybox("assets/default_env_skybox.ktx"); + await viewer.loadGltf("assets/cube.glb"); + final camera = await viewer.getActiveCamera(); + + var zOffset = 10.0; + + final inputHandler = DelegateInputHandler.flight(viewer); + + final webInputHandler = + WebInputHandler(inputHandler: inputHandler, canvas: canvas); + await camera.lookAt(Vector3(0, 0, zOffset)); + DateTime lastRender = DateTime.now(); + + while (true) { + var stackPtr = stackSave(); + var now = DateTime.now(); + await FilamentApp.instance!.requestFrame(); + now = DateTime.now(); + var timeSinceLast = + now.microsecondsSinceEpoch - lastRender.microsecondsSinceEpoch; + lastRender = now; + if (timeSinceLast < 1667) { + var waitFor = 1667 - timeSinceLast; + await Future.delayed(Duration(microseconds: waitFor)); + } + stackRestore(stackPtr); + // inputHandler.keyDown(PhysicalKey.S); + // await camera.lookAt(Vector3(0,0,zOffset)); + // zOffset +=0.1; + } +} diff --git a/examples/dart/js_wasm/web/index_js.html b/examples/dart/js_wasm/web/index_js.html new file mode 100644 index 00000000..6955f86d --- /dev/null +++ b/examples/dart/js_wasm/web/index_js.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/dart/js_wasm/web/index_wasm.html b/examples/dart/js_wasm/web/index_wasm.html new file mode 100644 index 00000000..154418fb --- /dev/null +++ b/examples/dart/js_wasm/web/index_wasm.html @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/examples/dart/js_wasm/web/main.js b/examples/dart/js_wasm/web/main.js new file mode 100644 index 00000000..440838ed --- /dev/null +++ b/examples/dart/js_wasm/web/main.js @@ -0,0 +1,22 @@ +import { readFile } from 'fs/promises'; + +import { compile } from './example.mjs'; +import thermion_dart from './thermion_dart.js'; + + +async function runDartWasm() { + globalThis['thermion_dart'] = await thermion_dart(); + + const wasmBytes = await readFile('example.wasm'); + const compiledApp = await compile(wasmBytes); + const instantiatedApp = await compiledApp.instantiate({}); + try { + instantiatedApp.invokeMain(); + } catch(err) { + console.error("Failed"); + console.error(err); + } +} + +runDartWasm().catch(console.error); + diff --git a/examples/dart/js_wasm/web/thermion_dart.js b/examples/dart/js_wasm/web/thermion_dart.js new file mode 120000 index 00000000..7046c2e9 --- /dev/null +++ b/examples/dart/js_wasm/web/thermion_dart.js @@ -0,0 +1 @@ +../../../../thermion_dart/native/web/build/build/out/thermion_dart.js \ No newline at end of file diff --git a/examples/dart/js_wasm/web/thermion_dart.wasm b/examples/dart/js_wasm/web/thermion_dart.wasm new file mode 120000 index 00000000..c7a867ee --- /dev/null +++ b/examples/dart/js_wasm/web/thermion_dart.wasm @@ -0,0 +1 @@ +../../../../thermion_dart/native/web/build/build/out/thermion_dart.wasm \ No newline at end of file diff --git a/examples/dart/js_wasm/web/web_input_handler.dart b/examples/dart/js_wasm/web/web_input_handler.dart new file mode 100644 index 00000000..5b5e9649 --- /dev/null +++ b/examples/dart/js_wasm/web/web_input_handler.dart @@ -0,0 +1,233 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; +import 'package:thermion_dart/thermion_dart.dart'; + +class WebInputHandler { + final DelegateInputHandler inputHandler; + final web.HTMLCanvasElement canvas; + late double pixelRatio; + + final Map _touchPositions = {}; + + WebInputHandler({ + required this.inputHandler, + required this.canvas, + }) { + pixelRatio = web.window.devicePixelRatio; + _initializeEventListeners(); + } + + void _initializeEventListeners() { + canvas.addEventListener('mousedown', _onMouseDown.toJS); + canvas.addEventListener('mousemove', _onMouseMove.toJS); + canvas.addEventListener('mouseup', _onMouseUp.toJS); + canvas.addEventListener('wheel', _onMouseWheel.toJS); + web.window.addEventListener('keydown', _onKeyDown.toJS); + web.window.addEventListener('keyup', _onKeyUp.toJS); + + canvas.addEventListener('touchstart', _onTouchStart.toJS); + canvas.addEventListener('touchmove', _onTouchMove.toJS); + canvas.addEventListener('touchend', _onTouchEnd.toJS); + canvas.addEventListener('touchcancel', _onTouchCancel.toJS); + } + + void _onMouseDown(web.MouseEvent event) { + final localPos = _getLocalPositionFromEvent(event); + final isMiddle = event.button == 1; + inputHandler.onPointerDown(localPos, isMiddle); + event.preventDefault(); + } + + void _onMouseMove(web.MouseEvent event) { + final localPos = _getLocalPositionFromEvent(event); + + final delta = Vector2(event.movementX ?? 0, event.movementY ?? 0); + final isMiddle = event.buttons & 4 != 0; + inputHandler.onPointerMove(localPos, delta, isMiddle); + event.preventDefault(); + } + + void _onMouseUp(web.MouseEvent event) { + final isMiddle = event.button == 1; + inputHandler.onPointerUp(isMiddle); + event.preventDefault(); + } + + void _onMouseWheel(web.WheelEvent event) { + final localPos = _getLocalPositionFromEvent(event); + final delta = event.deltaY; + inputHandler.onPointerScroll(localPos, delta); + event.preventDefault(); + } + + void _onKeyDown(web.KeyboardEvent event) { + PhysicalKey? key; + switch (event.code) { + case 'KeyW': + key = PhysicalKey.W; + break; + case 'KeyA': + key = PhysicalKey.A; + break; + case 'KeyS': + key = PhysicalKey.S; + break; + case 'KeyD': + key = PhysicalKey.D; + break; + } + if (key != null) inputHandler.keyDown(key); + event.preventDefault(); + } + + void _onKeyUp(web.KeyboardEvent event) { + PhysicalKey? key; + switch (event.code) { + case 'KeyW': + key = PhysicalKey.W; + break; + case 'KeyA': + key = PhysicalKey.A; + break; + case 'KeyS': + key = PhysicalKey.S; + break; + case 'KeyD': + key = PhysicalKey.D; + break; + } + if (key != null) inputHandler.keyUp(key); + event.preventDefault(); + } + + void _onTouchStart(web.TouchEvent event) { + + for (var touch in event.changedTouches.toList()) { + final pos = _getLocalPositionFromTouch(touch); + _touchPositions[touch.identifier] = pos; + } + + final touchCount = event.touches.toList().length; + if (touchCount == 1) { + final touch = event.touches.toList().first; + final pos = _getLocalPositionFromTouch(touch); + inputHandler.onPointerDown(pos, false); + } + + _handleScaleStart(touchCount, null); + event.preventDefault(); + } + + void _onTouchMove(web.TouchEvent event) { + for (var touch in event.changedTouches.toList()) { + final id = touch.identifier; + final currPos = _getLocalPositionFromTouch(touch); + final prevPos = _touchPositions[id]; + + if (prevPos != null) { + final delta = currPos - prevPos; + inputHandler.onPointerMove(currPos, delta, false); + } + + _touchPositions[id] = currPos; + } + + final touchCount = event.touches.toList().length; + if (touchCount >= 2) { + final touches = event.touches.toList().toList(); + final touch0 = touches[0]; + final touch1 = touches[1]; + final pos0 = _getLocalPositionFromTouch(touch0); + final pos1 = _getLocalPositionFromTouch(touch1); + final prevPos0 = _touchPositions[touch0.identifier]; + final prevPos1 = _touchPositions[touch1.identifier]; + + if (prevPos0 != null && prevPos1 != null) { + final prevDist = (prevPos0 - prevPos1).length; + final currDist = (pos0 - pos1).length; + final scale = currDist / prevDist; + final focalPoint = (pos0 + pos1) * 0.5; + + inputHandler.onScaleUpdate( + focalPoint, + Vector2(0, 0), + 0.0, + 0.0, + scale, + touchCount, + 0.0, + null, + ); + } + } + + event.preventDefault(); + } + + void _onTouchEnd(web.TouchEvent event) { + for (var touch in event.changedTouches.toList()) { + _touchPositions.remove(touch.identifier); + } + + final touchCount = event.touches.toList().length; + inputHandler.onScaleEnd(touchCount, 0.0); + event.preventDefault(); + } + + void _onTouchCancel(web.TouchEvent event) { + for (var touch in event.changedTouches.toList()) { + _touchPositions.remove(touch.identifier); + } + + final touchCount = event.touches.toList().length; + inputHandler.onScaleEnd(touchCount, 0.0); + event.preventDefault(); + } + + void _handleScaleStart(int pointerCount, Duration? sourceTimestamp) { + inputHandler.onScaleStart(Vector2.zero(), pointerCount, sourceTimestamp); + } + + Vector2 _getLocalPositionFromEvent(web.Event event) { + final rect = canvas.getBoundingClientRect(); + double clientX = 0, clientY = 0; + + if (event is web.MouseEvent) { + clientX = event.clientX.toDouble(); + clientY = event.clientY.toDouble(); + } else if (event is web.TouchEvent) { + final touch = event.touches.toList().firstOrNull; + if (touch != null) { + clientX = touch.clientX; + clientY = touch.clientY; + } + } + + return Vector2( + (clientX - rect.left) * pixelRatio, + (clientY - rect.top) * pixelRatio, + ); + } + + Vector2 _getLocalPositionFromTouch(web.Touch touch) { + final rect = canvas.getBoundingClientRect(); + return Vector2( + (touch.clientX - rect.left) * pixelRatio, + (touch.clientY - rect.top) * pixelRatio, + ); + } + + void dispose() { + canvas.removeEventListener('mousedown', _onMouseDown.toJS); + canvas.removeEventListener('mousemove', _onMouseMove.toJS); + canvas.removeEventListener('mouseup', _onMouseUp.toJS); + canvas.removeEventListener('wheel', _onMouseWheel.toJS); + web.window.removeEventListener('keydown', _onKeyDown.toJS); + web.window.removeEventListener('keyup', _onKeyUp.toJS); + canvas.removeEventListener('touchstart', _onTouchStart.toJS); + canvas.removeEventListener('touchmove', _onTouchMove.toJS); + canvas.removeEventListener('touchend', _onTouchEnd.toJS); + canvas.removeEventListener('touchcancel', _onTouchCancel.toJS); + } +} diff --git a/examples/flutter/quickstart/web/index.html b/examples/flutter/quickstart/web/index.html index cbadfa17..115ebc24 100644 --- a/examples/flutter/quickstart/web/index.html +++ b/examples/flutter/quickstart/web/index.html @@ -2,19 +2,6 @@ - @@ -37,12 +24,17 @@ const serviceWorkerVersion = null; - - + + -