feat! js_interop improvements

This commit is contained in:
Nick Fisher
2025-05-07 17:06:38 +08:00
parent 63e2dcd0ca
commit 2f16908992
159 changed files with 12989 additions and 8377 deletions

View File

@@ -11,14 +11,8 @@ import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_in
class ThermionFlutterPlugin {
ThermionFlutterPlugin._();
static Future<ThermionViewer> createViewer(
{ThermionFlutterOptions options =
const ThermionFlutterOptions()}) async {
final viewer =
await ThermionFlutterPlatform.instance.createViewer(options: options);
return viewer;
static Future<ThermionViewer> createViewer() {
return ThermionFlutterPlatform.instance.createViewer();
}
}

View File

@@ -19,7 +19,6 @@ Future kDefaultResizeCallback(Size size, View view, double pixelRatio) async {
}
class ThermionWidget extends StatefulWidget {
///
/// The viewer whose content will be rendered into this widget.
///
@@ -43,7 +42,7 @@ class ThermionWidget extends StatefulWidget {
///
/// If true, add an overlay showing the FPS on top of the rendered content.
///
///
final bool showFpsCounter;
///
@@ -67,12 +66,9 @@ class ThermionWidget extends StatefulWidget {
class _ThermionWidgetState extends State<ThermionWidget> {
@override
Widget build(BuildContext context) {
// Web doesn't support imported textures yet
if (kIsWeb) {
throw Exception();
// return ThermionWidgetWeb(
// viewer: widget.viewer,
// options: ThermionFlutterPlugin.options as ThermionFlutterWebOptions?);
return ThermionWidgetWeb(
viewer: widget.viewer, options: const ThermionFlutterWebOptions(importCanvasAsWidget: true));
}
return ThermionTextureWidget(

View File

@@ -1,37 +1,121 @@
import 'dart:js_util';
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:ui_web' as ui_web;
import 'package:logging/logging.dart';
import 'package:thermion_flutter/thermion_flutter.dart';
import 'package:thermion_flutter_web/thermion_flutter_web.dart';
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
import 'package:web/web.dart';
import 'package:web/web.dart' as web;
import 'package:flutter/widgets.dart';
class ThermionWidgetWeb extends StatelessWidget {
import 'resize_observer.dart';
class ThermionWidgetWeb extends StatefulWidget {
final ThermionFlutterWebOptions options;
final ThermionViewer viewer;
const ThermionWidgetWeb(
{super.key, this.options = const ThermionFlutterWebOptions.empty(), required this.viewer});
{super.key,
this.options = const ThermionFlutterWebOptions(),
required this.viewer});
@override
State<StatefulWidget> createState() => _ThermionWidgetWebState();
}
class _ThermionWidgetWebState extends State<ThermionWidgetWeb> {
void initState() {
super.initState();
_requestFrame();
}
DateTime lastRender = DateTime.now();
void _requestFrame() async {
Pointer? stackPtr;
WidgetsBinding.instance.scheduleFrameCallback((d) async {
if (stackPtr != null) {
stackRestore(stackPtr!);
stackPtr = null;
}
var elapsed = DateTime.now().microsecondsSinceEpoch -
lastRender.microsecondsSinceEpoch;
// if (elapsed > 1667) {
lastRender = DateTime.now();
if (widget.viewer.rendering) {
await FilamentApp.instance!.requestFrame();
}
// }
stackPtr = stackSave();
_requestFrame();
});
}
@override
Widget build(BuildContext context) {
// if (_texture == null || _resizing) {
// return widget.initial ?? Container(color: Colors.red);
// }
// return ResizeObserver(
// onResized: _resizeTexture,
// child: ThermionWidgetWeb(
// options: widget.options as ThermionFlutterWebOptions?));
if (options?.importCanvasAsWidget == true) {
return _ImageCopyingWidget();
if (widget.options.importCanvasAsWidget) {
return _ImageCopyingWidget(viewer: widget.viewer);
// return _PlatformView(
// viewer: widget.viewer,
// );
}
return Container(color: const Color(0x00000000));
}
}
class _PlatformView extends StatefulWidget {
final ThermionViewer viewer;
const _PlatformView({super.key, required this.viewer});
@override
State<StatefulWidget> createState() => _PlatformViewState();
}
class _PlatformViewState extends State<_PlatformView> {
void initState() {
super.initState();
ui_web.platformViewRegistry.registerViewFactory(
'imported-canvas',
(int viewId, {Object? params}) {
var canvas = web.document.getElementById("thermion_canvas");
WidgetsBinding.instance.addPostFrameCallback((_) {
var renderBox = this.context.findRenderObject() as RenderBox?;
_resize(Size(0, 0), renderBox!.size);
});
return canvas! as Object;
},
);
}
void _resize(Size oldSize, Size newSize) {
var width = newSize.width.toInt();
var height = newSize.height.toInt();
ThermionFlutterWebPlugin.instance
.resizeCanvas(newSize.width, newSize.height);
widget.viewer.setViewport(width, height);
}
@override
Widget build(BuildContext context) {
return ResizeObserver(
onResized: _resize,
child: HtmlElementView(
viewType: 'imported-canvas',
onPlatformViewCreated: (i) {},
creationParams: <String, Object?>{
'key': 'someValue',
},
));
}
}
class _ImageCopyingWidget extends StatefulWidget {
final ThermionViewer viewer;
const _ImageCopyingWidget({super.key, required this.viewer});
@override
State<StatefulWidget> createState() {
return _ImageCopyingWidgetState();
@@ -39,34 +123,82 @@ class _ImageCopyingWidget extends StatefulWidget {
}
class _ImageCopyingWidgetState extends State<_ImageCopyingWidget> {
final _logger = Logger("_ImageCopyingWidgetState");
late final _logger = Logger(this.runtimeType.toString());
late web.HTMLCanvasElement canvas;
ui.Image? _img;
double width = 0;
double height = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
capture();
canvas =
web.document.getElementById("thermion_canvas") as web.HTMLCanvasElement;
WidgetsBinding.instance.addPostFrameCallback((t) {
_refresh(Duration.zero);
});
}
Future capture() async {
void _refresh(Duration _) async {
try {
final ImageBitmap newSource = await promiseToFuture<ImageBitmap>(
window.createImageBitmap(
document.getElementById("canvas") as HTMLCanvasElement));
_img = await ui_web.createImageFromImageBitmap(newSource);
setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) {
capture();
});
final rb = this.context.findRenderObject() as RenderBox?;
if (_resizing || rb == null || rb.size.isEmpty) {
setState(() {});
return;
}
if (canvas.width != rb.size.width || canvas.height != rb.size.height) {
ThermionFlutterWebPlugin.instance
.resizeCanvas(rb.size.width, rb.size.height);
await widget.viewer
.setViewport(rb.size.width.ceil(), rb.size.height.ceil())
.timeout(Duration(seconds: 1));
}
width = canvas.width * web.window.devicePixelRatio;
height = canvas.height * web.window.devicePixelRatio;
_img = await ui_web.createImageFromTextureSource(canvas,
width: width.ceil(), height: height.ceil(), transferOwnership: true);
_request++;
} catch (err) {
_logger.severe(err);
} finally {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
WidgetsBinding.instance.scheduleFrameCallback(_refresh);
}
}
int _request = 0;
bool _resizing = false;
Timer? _resizeTimer;
void _resize(Size oldSize, Size newSize) {
_resizeTimer?.cancel();
_resizing = true;
_resizeTimer = Timer(Duration(milliseconds: 100), () {
_resizing = false;
});
}
@override
Widget build(BuildContext context) {
return RawImage(image: _img!);
if (_img == null) {
return Container();
}
return ResizeObserver(
onResized: _resize,
child: RawImage(
key: Key(_request.toString()),
width: width,
height: height,
image: _img!,
filterQuality: FilterQuality.high,
isAntiAlias: false,
));
}
}

View File

@@ -113,6 +113,7 @@ class _ViewerWidgetState extends State<ViewerWidget> {
}
void didUpdateWidget(ViewerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.manipulatorType != widget.manipulatorType) {
_setViewportWidget();
setState(() {});

View File

@@ -1,11 +1,10 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:thermion_dart/thermion_dart.dart';
import 'package:thermion_dart/src/viewer/src/ffi/src/thermion_viewer_ffi.dart';
import 'package:thermion_dart/src/viewer/src/ffi/src/ffi_filament_app.dart';
import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
import 'package:logging/logging.dart';
@@ -43,23 +42,22 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
return asset.buffer.asUint8List(asset.offsetInBytes);
}
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
Future<ThermionViewer> createViewer() async {
var driverPlatform = await channel.invokeMethod("getDriverPlatform");
var platformPtr = driverPlatform == null
? nullptr
: Pointer<Void>.fromAddress(driverPlatform);
: VoidPointerClass.fromAddress(driverPlatform);
var sharedContext = await channel.invokeMethod("getSharedContext");
var sharedContextPtr = sharedContext == null
? nullptr
: Pointer<Void>.fromAddress(sharedContext);
: VoidPointerClass.fromAddress(sharedContext);
late Backend backend;
if (options?.backend != null) {
switch (options!.backend) {
if (options.backend != null) {
switch (options.backend) {
case Backend.VULKAN:
if (!Platform.isWindows) {
throw Exception("Vulkan only supported on Windows");
@@ -93,12 +91,12 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
resourceLoader: loadAsset,
platform: platformPtr,
sharedContext: sharedContextPtr,
uberArchivePath: options?.uberarchivePath);
uberArchivePath: options.uberarchivePath);
if (FilamentApp.instance == null) {
await FFIFilamentApp.create(config: config);
FilamentApp.instance!.onDestroy(() async {
if(Platform.isWindows) {
if (Platform.isWindows) {
await channel.invokeMethod("destroyContext");
}
_swapChain = null;
@@ -160,8 +158,9 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
_swapChain = await FilamentApp.instance!
.createHeadlessSwapChain(descriptor.width, descriptor.height);
_logger.info("Created headless swapchain ${descriptor.width}x${descriptor.height}");
_logger.info(
"Created headless swapchain ${descriptor.width}x${descriptor.height}");
await FilamentApp.instance!.register(_swapChain!, view);
} else if (Platform.isAndroid) {
@@ -214,7 +213,7 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
PlatformTextureDescriptor texture,
View view,
int width,
int height) async {
int height) async {
var newTexture = await createTextureAndBindToView(view, width, height);
if (newTexture == null) {
throw Exception();

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:thermion_dart/thermion_dart.dart';
import 'package:thermion_dart/src/filament/filament.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'thermion_flutter_texture.dart';
@@ -9,7 +8,8 @@ class ThermionFlutterOptions {
final String? uberarchivePath;
final Backend? backend;
const ThermionFlutterOptions({this.uberarchivePath = null, this.backend = null});
const ThermionFlutterOptions(
{this.uberarchivePath = null, this.backend = null});
}
abstract class ThermionFlutterPlatform extends PlatformInterface {
@@ -20,6 +20,29 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
static late final ThermionFlutterPlatform _instance;
static ThermionFlutterPlatform get instance => _instance;
///
///
///
ThermionFlutterOptions? _options;
ThermionFlutterOptions get options {
_options ??= const ThermionFlutterOptions();
return _options!;
}
///
///
///
void setOptions(covariant ThermionFlutterOptions options) {
if (_options != null) {
throw Exception(
"Options can only be set once for the entire app lifecycle.");
}
_options = options;
}
///
///
///
static set instance(ThermionFlutterPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
@@ -28,8 +51,9 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
///
///
///
Future<ThermionViewer> createViewer(
{covariant ThermionFlutterOptions? options});
Future<ThermionViewer> createViewer() {
throw UnimplementedError();
}
///
/// Creates a raw rendering surface.
@@ -38,12 +62,16 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
/// call this yourself. May not be supported on all platforms.
///
Future<PlatformTextureDescriptor> createTextureDescriptor(
int width, int height);
int width, int height) {
throw UnimplementedError();
}
///
/// Destroys a raw rendering surface.
///
Future destroyTextureDescriptor(PlatformTextureDescriptor descriptor);
Future destroyTextureDescriptor(PlatformTextureDescriptor descriptor) {
throw UnimplementedError();
}
///
/// Create a rendering surface and binds to the given [View]
@@ -52,17 +80,23 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
/// call this yourself. May not be supported on all platforms.
///
Future<PlatformTextureDescriptor?> createTextureAndBindToView(
View view, int width, int height);
View view, int width, int height) {
throw UnimplementedError();
}
///
///
///
///
Future<PlatformTextureDescriptor?> resizeTexture(
PlatformTextureDescriptor texture, View view, int width, int height);
PlatformTextureDescriptor texture, View view, int width, int height) {
throw UnimplementedError();
}
///
///
///
Future markTextureFrameAvailable(PlatformTextureDescriptor texture);
Future markTextureFrameAvailable(PlatformTextureDescriptor texture) {
throw UnimplementedError();
}
}

View File

@@ -1,84 +1,123 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:logging/logging.dart';
import 'package:flutter/services.dart';
import 'package:thermion_dart/thermion_dart.dart';
import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
import 'package:web/web.dart';
class ThermionFlutterWebPlugin extends ThermionFlutterPlatform {
ThermionViewerWasm? _viewer;
late final _logger = Logger(this.runtimeType.toString());
static void registerWith(Registrar registrar) {
ThermionFlutterPlatform.instance = ThermionFlutterWebPlugin();
}
@override
Future<PlatformTextureDescriptor?> createTexture(double width, double height,
double offsetLeft, double offsetTop, double pixelRatio) async {
await _viewer!.destroySwapChain();
await _viewer!.createSwapChain(width.ceil(), height.ceil());
final canvas = document.getElementById("canvas") as HTMLCanvasElement;
canvas.width = (width * pixelRatio).ceil();
canvas.height = (height * pixelRatio).ceil();
(canvas as HTMLElement).style.position = "fixed";
(canvas as HTMLElement).style.zIndex = "-1";
(canvas as HTMLElement).style.left =
(offsetLeft * pixelRatio).ceil().toString();
(canvas as HTMLElement).style.top =
(offsetTop * pixelRatio).ceil().toString();
_viewer!
.setViewportAndCameraProjection(width.ceil(), height.ceil(), 1.0);
return PlatformTextureDescriptor(null, null, 0, 0, null);
ThermionFlutterWebOptions? _options;
void setOptions(ThermionFlutterWebOptions options) {
_options = options;
}
@override
Future destroyTexture(PlatformTextureDescriptor texture) async {
// noop
ThermionFlutterWebOptions get options {
_options ??= const ThermionFlutterWebOptions();
return _options!;
}
@override
Future<PlatformTextureDescriptor?> resizeTexture(PlatformTextureDescriptor texture,
int width, int height, int offsetLeft, int offsetTop, double pixelRatio) async {
final canvas = document.getElementById("canvas") as HTMLCanvasElement;
canvas.width = width;
canvas.height = height;
(canvas as HTMLElement).style.position = "fixed";
(canvas as HTMLElement).style.zIndex = "-1";
(canvas as HTMLElement).style.left =
(offsetLeft * pixelRatio).ceil().toString();
(canvas as HTMLElement).style.top =
(offsetTop * pixelRatio).ceil().toString();
_viewer!.setViewportAndCameraProjection(width, height, 1.0);
return PlatformTextureDescriptor(null, null, 0, 0, null);
}
static ThermionFlutterWebPlugin get instance =>
ThermionFlutterPlatform.instance as ThermionFlutterWebPlugin;
Future<ThermionViewer> createViewerWithOptions(
ThermionFlutterWebOptions options) async {
_viewer = ThermionViewerWasm(assetPathPrefix: "/assets/");
final canvas = options.createCanvas
? document.createElement("canvas") as HTMLCanvasElement?
: document.getElementById("canvas") as HTMLCanvasElement?;
if (canvas == null) {
throw Exception("Could not locate or create canvas");
static Future<Uint8List> loadAsset(String path) async {
if (path.startsWith("file://")) {
throw UnsupportedError("file:// URIs not supported on web");
}
canvas.id = "canvas";
document.body!.appendChild(canvas);
canvas.style.display = 'none';
final pixelRatio = window.devicePixelRatio;
await _viewer!
.initialize(1, 1, pixelRatio, uberArchivePath: options.uberarchivePath);
return _viewer!;
if (path.startsWith("asset://")) {
path = path.replaceAll("asset://", "");
}
var asset = await rootBundle.load(path);
return asset.buffer.asUint8List(asset.offsetInBytes);
}
@override
Future<ThermionViewer> createViewer({String? uberarchivePath}) {
throw Exception("Use createViewerWithOptions instead");
Future<ThermionViewer> createViewer() async {
HTMLCanvasElement? canvas;
if (FilamentApp.instance == null) {
// first, try and initialize bindings to see if the user has included thermion_dart.js manually in index.html
try {
NativeLibrary.initBindings("thermion_dart");
} catch (err) {
_logger.info(
"Failed to find thermion_dart in window context, appending manually");
// if not, manually add the script to the DOM
var scriptElement =
document.createElement("script") as HTMLScriptElement;
scriptElement.src = "./thermion_dart.js";
document.head!.appendChild(scriptElement);
final completer = Completer<JSObject?>();
scriptElement.addEventListener(
"load",
() {
final constructor = globalContext
.getProperty("thermion_dart".toJS) as JSFunction?;
if (constructor == null) {
_logger.severe("Failed to find JS library constructor");
completer.complete(null);
} else {
final lib = constructor.callAsFunction() as JSPromise;
lib.toDart.then((resolved) {
completer.complete(resolved as JSObject);
});
}
}.toJS);
final lib = await completer.future;
globalContext.setProperty("thermion_dart".toJS, lib);
NativeLibrary.initBindings("thermion_dart");
}
canvas = options.createCanvas == true
? document.createElement("canvas") as HTMLCanvasElement?
: document.getElementById("thermion_canvas") as HTMLCanvasElement?;
if (canvas == null) {
throw Exception("Could not locate or create canvas");
}
canvas.id = "thermion_canvas";
// canvas.style.display = "none";
document.body!.appendChild(canvas);
(canvas as HTMLElement).style.position = "fixed";
(canvas as HTMLElement).style.zIndex = "-1";
final config = FFIFilamentConfig(
backend: Backend.OPENGL,
resourceLoader: loadAsset,
platform: nullptr,
sharedContext: nullptr,
uberArchivePath: options.uberarchivePath);
await FFIFilamentApp.create(config: config);
}
final viewer = ThermionViewerFFI(loadAssetFromUri: loadAsset);
await viewer.initialized;
await viewer.setViewport(canvas!.width, canvas.height);
var swapChain = await FilamentApp.instance!
.createHeadlessSwapChain(canvas.width, canvas.height);
await FilamentApp.instance!.register(swapChain, viewer.view);
return viewer;
}
///
///
///
void resizeCanvas(double width, double height) async {
_logger.info("Resizing canvas to ${width}x${height}");
Thermion_resizeCanvas((window.devicePixelRatio * width).ceil(),
(window.devicePixelRatio * height).ceil());
}
}

View File

@@ -5,7 +5,7 @@ class ThermionFlutterWebOptions extends ThermionFlutterOptions {
final bool createCanvas;
final bool importCanvasAsWidget;
ThermionFlutterWebOptions(
const ThermionFlutterWebOptions(
{this.importCanvasAsWidget = false,
this.createCanvas = true,
String? uberarchivePath})

View File

@@ -24,6 +24,12 @@ dependencies:
thermion_flutter_platform_interface: ^0.2.1-dev.20.0
flutter_web_plugins:
sdk: flutter
dependency_overrides:
thermion_flutter_platform_interface:
path: ../thermion_flutter_platform_interface
thermion_dart:
path: ../../thermion_dart
dev_dependencies:
flutter_test: