refactor: continual refactor to support multiple render targets
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
|
||||
///
|
||||
/// Handles all platform-specific initialization to create a backing rendering
|
||||
@@ -11,118 +9,30 @@ import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dar
|
||||
/// Call [createViewerWithOptions] to create an instance of [ThermionViewer].
|
||||
///
|
||||
class ThermionFlutterPlugin {
|
||||
ThermionFlutterPlugin._();
|
||||
|
||||
static AppLifecycleListener? _appLifecycleListener;
|
||||
ThermionFlutterPlugin._();
|
||||
|
||||
static bool _initializing = false;
|
||||
|
||||
static ThermionViewer? _viewer;
|
||||
|
||||
static bool _wasRenderingOnInactive = false;
|
||||
|
||||
static void _handleStateChange(AppLifecycleState state) async {
|
||||
if (_viewer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _viewer!.initialized;
|
||||
switch (state) {
|
||||
case AppLifecycleState.detached:
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = _viewer!.rendering;
|
||||
}
|
||||
await _viewer!.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = _viewer!.rendering;
|
||||
}
|
||||
await _viewer!.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = _viewer!.rendering;
|
||||
}
|
||||
// on Windows in particular, restoring a window after minimizing stalls the renderer (and the whole application) for a considerable length of time.
|
||||
// disabling rendering on minimize seems to fix the issue (so I wonder if there's some kind of command buffer that's filling up while the window is minimized).
|
||||
await _viewer!.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = _viewer!.rendering;
|
||||
}
|
||||
await _viewer!.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
await _viewer!.setRendering(_wasRenderingOnInactive);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use createViewerWithOptions")
|
||||
static Future<ThermionViewer> createViewer({String? uberArchivePath}) async {
|
||||
static Future<ThermionViewer> createViewer(
|
||||
{ThermionFlutterOptions options =
|
||||
const ThermionFlutterOptions.empty()}) async {
|
||||
|
||||
if (_initializing) {
|
||||
throw Exception("Existing call to createViewer has not completed.");
|
||||
}
|
||||
_initializing = true;
|
||||
|
||||
_viewer = await ThermionFlutterPlatform.instance
|
||||
.createViewer(uberarchivePath: uberArchivePath);
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onStateChange: _handleStateChange,
|
||||
);
|
||||
_viewer!.onDispose(() async {
|
||||
_viewer = null;
|
||||
_appLifecycleListener?.dispose();
|
||||
_appLifecycleListener = null;
|
||||
});
|
||||
_initializing = false;
|
||||
return _viewer!;
|
||||
}
|
||||
|
||||
static Future<ThermionViewer> createViewerWithOptions(
|
||||
{ThermionFlutterOptions options = const ThermionFlutterOptions.empty()}) async {
|
||||
if (_initializing) {
|
||||
throw Exception("Existing call to createViewer has not completed.");
|
||||
}
|
||||
_initializing = true;
|
||||
_viewer =
|
||||
await ThermionFlutterPlatform.instance.createViewerWithOptions(options);
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onStateChange: _handleStateChange,
|
||||
);
|
||||
await ThermionFlutterPlatform.instance.createViewer(options: options);
|
||||
|
||||
_viewer!.onDispose(() async {
|
||||
_viewer = null;
|
||||
_appLifecycleListener?.dispose();
|
||||
_appLifecycleListener = null;
|
||||
});
|
||||
_initializing = false;
|
||||
return _viewer!;
|
||||
}
|
||||
|
||||
static Future<ThermionFlutterTexture?> createTexture(
|
||||
double width,
|
||||
double height,
|
||||
double offsetLeft,
|
||||
double offsetTop,
|
||||
double pixelRatio) async {
|
||||
return ThermionFlutterPlatform.instance
|
||||
.createTexture(width, height, offsetLeft, offsetTop, pixelRatio);
|
||||
}
|
||||
|
||||
static Future destroyTexture(ThermionFlutterTexture texture) async {
|
||||
return ThermionFlutterPlatform.instance.destroyTexture(texture);
|
||||
}
|
||||
|
||||
static Future<ThermionFlutterTexture?> resizeTexture(
|
||||
ThermionFlutterTexture texture,
|
||||
int width,
|
||||
int height,
|
||||
int offsetLeft,
|
||||
int offsetTop,
|
||||
double pixelRatio) async {
|
||||
return ThermionFlutterPlatform.instance.resizeTexture(
|
||||
texture, width, height, offsetLeft, offsetTop, pixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class PixelRatioAware extends StatelessWidget {
|
||||
final Widget Function(BuildContext context, double pixelRatio) builder;
|
||||
|
||||
const PixelRatioAware({Key? key, required this.builder}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return builder(context, MediaQuery.of(context).devicePixelRatio);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/pixel_ratio_aware.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
extension OffsetExtension on Offset {
|
||||
@@ -79,54 +80,64 @@ class _ThermionListenerWidgetState extends State<ThermionListenerWidget> {
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
}
|
||||
|
||||
Widget _desktop() {
|
||||
Widget _desktop(double pixelRatio) {
|
||||
return Listener(
|
||||
onPointerHover: (event) => widget.gestureHandler
|
||||
.onPointerHover(event.localPosition.toVector2(), event.delta.toVector2()),
|
||||
onPointerHover: (event) => widget.gestureHandler.onPointerHover(
|
||||
event.localPosition.toVector2() * pixelRatio,
|
||||
event.delta.toVector2() * pixelRatio),
|
||||
onPointerSignal: (PointerSignalEvent pointerSignal) {
|
||||
if (pointerSignal is PointerScrollEvent) {
|
||||
widget.gestureHandler.onPointerScroll(
|
||||
pointerSignal.localPosition.toVector2(),
|
||||
pointerSignal.scrollDelta.dy);
|
||||
pointerSignal.localPosition.toVector2() * pixelRatio,
|
||||
pointerSignal.scrollDelta.dy * pixelRatio);
|
||||
}
|
||||
},
|
||||
onPointerPanZoomStart: (pzs) {
|
||||
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
|
||||
},
|
||||
onPointerDown: (d) => widget.gestureHandler
|
||||
.onPointerDown(d.localPosition.toVector2(), d.buttons & kMiddleMouseButton != 0),
|
||||
onPointerMove: (d) => widget.gestureHandler
|
||||
.onPointerMove(d.localPosition.toVector2(), d.delta.toVector2(), d.buttons & kMiddleMouseButton != 0),
|
||||
onPointerUp: (d) => widget.gestureHandler.onPointerUp(d.buttons & kMiddleMouseButton != 0),
|
||||
onPointerDown: (d) => widget.gestureHandler.onPointerDown(
|
||||
d.localPosition.toVector2() * pixelRatio,
|
||||
d.buttons & kMiddleMouseButton != 0),
|
||||
onPointerMove: (d) => widget.gestureHandler.onPointerMove(
|
||||
d.localPosition.toVector2() * pixelRatio,
|
||||
d.delta.toVector2() * pixelRatio,
|
||||
d.buttons & kMiddleMouseButton != 0),
|
||||
onPointerUp: (d) => widget.gestureHandler
|
||||
.onPointerUp(d.buttons & kMiddleMouseButton != 0),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mobile() {
|
||||
return _MobileListenerWidget(gestureHandler: widget.gestureHandler);
|
||||
Widget _mobile(double pixelRatio) {
|
||||
return _MobileListenerWidget(
|
||||
gestureHandler: widget.gestureHandler, pixelRatio: pixelRatio);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: widget.gestureHandler.initialized,
|
||||
builder: (_, initialized) {
|
||||
if (initialized.data != true) {
|
||||
return widget.child ?? Container();
|
||||
}
|
||||
return Stack(children: [
|
||||
if (widget.child != null) Positioned.fill(child: widget.child!),
|
||||
if (isDesktop) Positioned.fill(child: _desktop()),
|
||||
if (!isDesktop) Positioned.fill(child: _mobile())
|
||||
]);
|
||||
});
|
||||
return PixelRatioAware(builder: (ctx, pixelRatio) {
|
||||
return FutureBuilder(
|
||||
initialData: 1.0,
|
||||
future: widget.gestureHandler.initialized,
|
||||
builder: (_, initialized) {
|
||||
if (initialized.data != true) {
|
||||
return widget.child ?? Container();
|
||||
}
|
||||
return Stack(children: [
|
||||
if (widget.child != null) Positioned.fill(child: widget.child!),
|
||||
if (isDesktop) Positioned.fill(child: _desktop(pixelRatio)),
|
||||
if (!isDesktop) Positioned.fill(child: _mobile(pixelRatio))
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileListenerWidget extends StatefulWidget {
|
||||
final InputHandler gestureHandler;
|
||||
final double pixelRatio;
|
||||
|
||||
const _MobileListenerWidget({Key? key, required this.gestureHandler})
|
||||
const _MobileListenerWidget({Key? key, required this.gestureHandler, required this.pixelRatio})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -146,7 +157,7 @@ class _MobileListenerWidgetState extends State<_MobileListenerWidget> {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (details) => widget.gestureHandler
|
||||
.onPointerDown(details.localPosition.toVector2(), false),
|
||||
.onPointerDown(details.localPosition.toVector2() * widget.pixelRatio, false),
|
||||
onDoubleTap: () {
|
||||
widget.gestureHandler.setActionForType(InputType.SCALE1,
|
||||
isPan ? InputAction.TRANSLATE : InputAction.ROTATE);
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/shared_types/view.dart' as t;
|
||||
import 'package:thermion_flutter/src/widgets/src/resize_observer.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' hide Colors;
|
||||
|
||||
class ThermionTextureWidget extends StatefulWidget {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
final t.View view;
|
||||
|
||||
final Widget? initial;
|
||||
|
||||
const ThermionTextureWidget(
|
||||
{super.key, required this.viewer, required this.view, this.initial});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ThermionTextureWidgetState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
ThermionFlutterTexture? _texture;
|
||||
RenderTarget? _renderTarget;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_texture?.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await widget.viewer.initialized;
|
||||
|
||||
var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
var size = ((context.findRenderObject()) as RenderBox).size;
|
||||
var width = (size.width * dpr).ceil();
|
||||
var height = (size.height * dpr).ceil();
|
||||
|
||||
_texture =
|
||||
await ThermionFlutterPlatform.instance.createTexture(width, height);
|
||||
|
||||
_renderTarget = await widget.viewer.createRenderTarget(
|
||||
_texture!.width, _texture!.height, _texture!.hardwareId);
|
||||
|
||||
await widget.view.setRenderTarget(_renderTarget!);
|
||||
|
||||
await widget.view.updateViewport(width, height);
|
||||
var camera = await widget.view.getCamera();
|
||||
await camera.setLensProjection(aspect: width / height);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
_requestFrame();
|
||||
|
||||
widget.viewer.onDispose(() async {
|
||||
var texture = _texture;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
if (texture != null) {
|
||||
_renderTarget = await widget.viewer.createRenderTarget(
|
||||
texture.width, texture.height, texture.flutterId);
|
||||
await widget.view.setRenderTarget(null);
|
||||
await _renderTarget!.destroy();
|
||||
texture.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _rendering = false;
|
||||
|
||||
void _requestFrame() {
|
||||
WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
if (!_rendering) {
|
||||
_rendering = true;
|
||||
await widget.viewer.requestFrame();
|
||||
await _texture?.markFrameAvailable();
|
||||
_rendering = false;
|
||||
}
|
||||
_requestFrame();
|
||||
});
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resize(Size newSize) async {
|
||||
|
||||
_resizeTimer?.cancel();
|
||||
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 10), () async {
|
||||
if (_resizing || !mounted) {
|
||||
return;
|
||||
}
|
||||
_resizeTimer!.cancel();
|
||||
_resizing = true;
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSize *= MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
var newWidth = newSize.width.ceil();
|
||||
var newHeight = newSize.height.ceil();
|
||||
|
||||
await _texture?.resize(
|
||||
newWidth,
|
||||
newHeight,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
await widget.view.updateViewport(newWidth, newHeight);
|
||||
var camera = await widget.view.getCamera();
|
||||
await camera.setLensProjection(aspect: newWidth / newHeight);
|
||||
|
||||
setState(() {});
|
||||
_resizing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_texture == null) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
return Stack(children: [
|
||||
Positioned.fill(
|
||||
child: ResizeObserver(
|
||||
onResized: _resize,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(
|
||||
child: Texture(
|
||||
key: ObjectKey("flutter_texture_${_texture!.flutterId}"),
|
||||
textureId: _texture!.flutterId,
|
||||
filterQuality: FilterQuality.none,
|
||||
freeze: false,
|
||||
))
|
||||
]))),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
var img =
|
||||
await widget.viewer.capture(renderTarget: _renderTarget!);
|
||||
print(img);
|
||||
},
|
||||
child: Text("CAPTURE")),
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// class _ThermionWidgetState extends State<ThermionWidget> {
|
||||
|
||||
// ThermionFlutterTexture? _texture;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
// await widget.viewer.initialized;
|
||||
// widget.viewer.onDispose(() async {
|
||||
// _rendering = false;
|
||||
|
||||
// if (_texture != null) {
|
||||
// var texture = _texture;
|
||||
// _texture = null;
|
||||
// if (mounted) {
|
||||
// setState(() {});
|
||||
// }
|
||||
// await ThermionFlutterPlugin.destroyTexture(texture!);
|
||||
// }
|
||||
// });
|
||||
// var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// var size = ((context.findRenderObject()) as RenderBox).size;
|
||||
// _texture = await ThermionFlutterPlugin.createTexture(
|
||||
// size.width, size.height, 0, 0, dpr);
|
||||
|
||||
// if (mounted) {
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
// _requestFrame();
|
||||
// });
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
// bool _rendering = false;
|
||||
|
||||
// void _requestFrame() {
|
||||
// WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
// if (!_rendering) {
|
||||
// _rendering = true;
|
||||
// await widget.viewer.requestFrame();
|
||||
// _rendering = false;
|
||||
// }
|
||||
// _requestFrame();
|
||||
// });
|
||||
// }
|
||||
|
||||
// bool _resizing = false;
|
||||
// Timer? _resizeTimer;
|
||||
|
||||
// Future _resizeTexture(Size newSize) async {
|
||||
// _resizeTimer?.cancel();
|
||||
// _resizeTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||
// if (_resizing || !mounted) {
|
||||
// return;
|
||||
// }
|
||||
// _resizeTimer!.cancel();
|
||||
// _resizing = true;
|
||||
|
||||
// if (!mounted) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// _texture.resize(newSize.width.ceil(), newSize.height.ceil(), 0, 0, dpr);
|
||||
// setState(() {});
|
||||
// _resizing = false;
|
||||
// });
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (_texture == null || _resizing) {
|
||||
// return widget.initial ??
|
||||
// Container(
|
||||
// color:
|
||||
// kIsWeb ? const Color.fromARGB(0, 170, 129, 129) : Colors.red);
|
||||
// }
|
||||
|
||||
// var textureWidget = Texture(
|
||||
// key: ObjectKey("texture_${_texture!.flutterId}"),
|
||||
// textureId: _texture!.flutterId!,
|
||||
// filterQuality: FilterQuality.none,
|
||||
// freeze: false,
|
||||
// );
|
||||
|
||||
// return ResizeObserver(
|
||||
// onResized: _resizeTexture,
|
||||
// child: Stack(children: [
|
||||
// Positioned.fill(
|
||||
// child: Platform.isLinux || Platform.isWindows
|
||||
// ? Transform(
|
||||
// alignment: Alignment.center,
|
||||
// transform: Matrix4.rotationX(
|
||||
// pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere?
|
||||
// child: textureWidget)
|
||||
// : textureWidget)
|
||||
// ]));
|
||||
// }
|
||||
// }
|
||||
@@ -1,18 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/thermion_texture_widget.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/thermion_widget_web.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/transparent_filament_widget.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
|
||||
import 'resize_observer.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/shared_types/view.dart' as t;
|
||||
import 'thermion_widget_windows.dart';
|
||||
|
||||
class ThermionWidget extends StatefulWidget {
|
||||
///
|
||||
/// The viewer.
|
||||
///
|
||||
final ThermionViewer viewer;
|
||||
|
||||
///
|
||||
/// The view.
|
||||
///
|
||||
final t.View? view;
|
||||
|
||||
///
|
||||
/// The options to use when creating this widget.
|
||||
///
|
||||
final ThermionFlutterOptions? options;
|
||||
|
||||
///
|
||||
@@ -22,130 +31,51 @@ class ThermionWidget extends StatefulWidget {
|
||||
final Widget? initial;
|
||||
|
||||
const ThermionWidget(
|
||||
{Key? key, this.initial, required this.viewer, this.options})
|
||||
{Key? key, this.initial, required this.viewer, this.view, this.options})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_ThermionWidgetState createState() => _ThermionWidgetState();
|
||||
State<ThermionWidget> createState() => _ThermionWidgetState();
|
||||
}
|
||||
|
||||
class _ThermionWidgetState extends State<ThermionWidget> {
|
||||
ThermionFlutterTexture? _texture;
|
||||
t.View? view;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await widget.viewer.initialized;
|
||||
widget.viewer.onDispose(() async {
|
||||
if (_texture != null) {
|
||||
var texture = _texture;
|
||||
_texture = null;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
await ThermionFlutterPlugin.destroyTexture(texture!);
|
||||
}
|
||||
});
|
||||
var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
var size = ((context.findRenderObject()) as RenderBox).size;
|
||||
_texture = await ThermionFlutterPlugin.createTexture(
|
||||
size.width, size.height, 0, 0, dpr);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
_requestFrame();
|
||||
});
|
||||
super.initState();
|
||||
initialize();
|
||||
}
|
||||
|
||||
|
||||
bool _rendering = false;
|
||||
|
||||
void _requestFrame() {
|
||||
WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
if (!_rendering) {
|
||||
_rendering = true;
|
||||
await widget.viewer.requestFrame();
|
||||
_rendering = false;
|
||||
Future initialize() async {
|
||||
if (widget.view != null) {
|
||||
view = widget.view;
|
||||
} else {
|
||||
view = await widget.viewer.getViewAt(0);
|
||||
}
|
||||
_requestFrame();
|
||||
});
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resizeTexture(Size newSize) async {
|
||||
_resizeTimer?.cancel();
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||
if (_resizing || !mounted) {
|
||||
return;
|
||||
}
|
||||
_resizeTimer!.cancel();
|
||||
_resizing = true;
|
||||
var oldTexture = _texture;
|
||||
_texture = null;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
_texture = await ThermionFlutterPlugin.resizeTexture(
|
||||
oldTexture!, newSize.width.ceil(), newSize.height.ceil(), 0, 0, dpr);
|
||||
setState(() {});
|
||||
_resizing = false;
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (view == null) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
// Windows & Web don't support imported textures yet
|
||||
if (kIsWeb) {
|
||||
if (_texture == null || _resizing) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
return ResizeObserver(
|
||||
onResized: _resizeTexture,
|
||||
child: ThermionWidgetWeb(
|
||||
options: widget.options as ThermionFlutterWebOptions?));
|
||||
return ThermionWidgetWeb(
|
||||
viewer: widget.viewer,
|
||||
options: widget.options as ThermionFlutterWebOptions);
|
||||
}
|
||||
|
||||
if (_texture?.usesBackingWindow == true) {
|
||||
return ResizeObserver(
|
||||
onResized: _resizeTexture,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(child: CustomPaint(painter: TransparencyPainter()))
|
||||
]));
|
||||
if (Platform.isWindows) {
|
||||
return ThermionWidgetWindows(viewer: widget.viewer);
|
||||
}
|
||||
|
||||
if (_texture == null || _resizing) {
|
||||
return widget.initial ??
|
||||
Container(
|
||||
color:
|
||||
kIsWeb ? const Color.fromARGB(0, 170, 129, 129) : Colors.red);
|
||||
}
|
||||
|
||||
var textureWidget = Texture(
|
||||
key: ObjectKey("texture_${_texture!.flutterTextureId}"),
|
||||
textureId: _texture!.flutterTextureId!,
|
||||
filterQuality: FilterQuality.none,
|
||||
freeze: false,
|
||||
);
|
||||
|
||||
return ResizeObserver(
|
||||
onResized: _resizeTexture,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(
|
||||
child: Platform.isLinux || Platform.isWindows
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationX(
|
||||
pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere?
|
||||
child: textureWidget)
|
||||
: textureWidget)
|
||||
]));
|
||||
return ThermionTextureWidget(
|
||||
key: ObjectKey(view!),
|
||||
initial: widget.initial,
|
||||
viewer: widget.viewer,
|
||||
view: view!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,28 @@ import 'dart:js_util';
|
||||
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_options.dart';
|
||||
import 'package:web/web.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ThermionWidgetWeb extends StatelessWidget {
|
||||
final ThermionFlutterWebOptions options;
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const ThermionWidgetWeb(
|
||||
{super.key, this.options = const ThermionFlutterWebOptions.empty()});
|
||||
{super.key, this.options = const ThermionFlutterWebOptions.empty(), required this.viewer});
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_ffi.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
|
||||
|
||||
class ThermionWidgetWeb extends StatefulWidget {
|
||||
class ThermionWidgetWeb extends StatelessWidget {
|
||||
final ThermionFlutterWebOptions? options;
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const ThermionWidgetWeb({super.key, required this.options, required this.viewer});
|
||||
|
||||
const ThermionWidgetWeb({super.key, required this.options});
|
||||
|
||||
@override
|
||||
// ignore: no_logic_in_create_state
|
||||
State<StatefulWidget> createState() => throw Exception();
|
||||
Widget build(BuildContext context) {
|
||||
throw Exception("STUB");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
|
||||
class ThermionWidgetWindows extends StatelessWidget {
|
||||
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const ThermionWidgetWindows({super.key, required this.viewer});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO: implement build
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -55,14 +55,7 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(resourcesPtr!).takeUnretainedValue()
|
||||
instance.resources.removeValue(forKey:UInt32(rbuf.id))
|
||||
}
|
||||
|
||||
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
if(instance.texture != nil) {
|
||||
instance.registry.textureFrameAvailable(instance.texture!.flutterTextureId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let _messenger = registrar.messenger;
|
||||
@@ -88,12 +81,12 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
resourceLoaderWrapper = make_resource_loader(loadResource, freeResource, Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
result(Int64(Int(bitPattern: resourceLoaderWrapper!)))
|
||||
case "markTextureFrameAvailable":
|
||||
let flutterTextureId = call.arguments as! Int64
|
||||
registry.textureFrameAvailable(flutterTextureId)
|
||||
result(nil)
|
||||
case "getRenderCallback":
|
||||
if(renderCallbackHolder.isEmpty) {
|
||||
renderCallbackHolder.append(unsafeBitCast(markTextureFrameAvailable, to:Int64.self))
|
||||
renderCallbackHolder.append(unsafeBitCast(Unmanaged.passUnretained(self), to:UInt64.self))
|
||||
}
|
||||
result(renderCallbackHolder)
|
||||
result(nil)
|
||||
case "getDriverPlatform":
|
||||
result(nil)
|
||||
case "getSharedContext":
|
||||
|
||||
@@ -6,11 +6,11 @@ public class ThermionFlutterTexture : NSObject, FlutterTexture {
|
||||
|
||||
var flutterTextureId: Int64 = -1
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionTexture
|
||||
var texture: ThermionTextureSwift
|
||||
|
||||
init(registry:FlutterTextureRegistry, width:Int64, height:Int64) {
|
||||
self.registry = registry
|
||||
self.texture = ThermionTexture(width:width, height: height)
|
||||
self.texture = ThermionTextureSwift(width:width, height: height)
|
||||
super.init()
|
||||
self.flutterTextureId = registry.register(self)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "ResourceBuffer.hpp"
|
||||
|
||||
using namespace filament;
|
||||
using namespace thermion_filament;
|
||||
using namespace thermion;
|
||||
using namespace std;
|
||||
|
||||
int _i = 0;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterAndroid
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
|
||||
ThermionViewerFFI? _viewer;
|
||||
|
||||
ThermionFlutterAndroid._() {}
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterAndroid._();
|
||||
}
|
||||
|
||||
final _textures = <ThermionFlutterTexture>{};
|
||||
|
||||
bool _creatingTexture = false;
|
||||
bool _destroyingTexture = false;
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Create a rendering surface.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself.
|
||||
///
|
||||
/// The name here is slightly misleading because we only create
|
||||
/// a texture render target on macOS and iOS; on Android, we render into
|
||||
/// a native window derived from a Surface, and on Windows we render into
|
||||
/// a HWND.
|
||||
///
|
||||
/// Currently, this only supports a single "texture" (aka rendering surface)
|
||||
/// at any given time. If a [ThermionWidget] is disposed, it will call
|
||||
/// [destroyTexture]; if it is resized, it will call [resizeTexture].
|
||||
///
|
||||
/// In future, we probably want to be able to create multiple distinct
|
||||
/// textures/render targets. This would make it possible to have multiple
|
||||
/// Flutter Texture widgets, each with its own Filament View attached.
|
||||
/// The current design doesn't accommodate this (for example, it seems we can
|
||||
/// only create a single native window from a Surface at any one time).
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
throw Exception("TODO");
|
||||
// note that when [ThermionWidget] is disposed, we don't destroy the
|
||||
// texture; instead, we keep it around in case a subsequent call requests
|
||||
// a texture of the same size.
|
||||
|
||||
// if (_textures.length > 1) {
|
||||
// throw Exception("Multiple textures not yet supported");
|
||||
// } else if (_textures.length == 1) {
|
||||
// if (_textures.first.height == physicalHeight &&
|
||||
// _textures.first.width == physicalWidth) {
|
||||
// return _textures.first;
|
||||
// } else {
|
||||
// await _viewer!.setRendering(false);
|
||||
// await _swapChain?.destroy();
|
||||
// await destroyTexture(_textures.first);
|
||||
// _textures.clear();
|
||||
// }
|
||||
// }
|
||||
|
||||
// _creatingTexture = true;
|
||||
|
||||
// var result = await _channel.invokeMethod("createTexture",
|
||||
// [physicalWidth, physicalHeight, offsetLeft, offsetLeft]);
|
||||
|
||||
// if (result == null || (result[0] == -1)) {
|
||||
// throw Exception("Failed to create texture");
|
||||
// }
|
||||
// final flutterTextureId = result[0] as int?;
|
||||
// final hardwareTextureId = result[1] as int?;
|
||||
// final surfaceAddress = result[2] as int?;
|
||||
|
||||
// _logger.info(
|
||||
// "Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
// final texture = ThermionFlutterTexture(flutterTextureId, hardwareTextureId,
|
||||
// physicalWidth, physicalHeight, surfaceAddress);
|
||||
|
||||
// await _viewer?.createSwapChain(physicalWidth, physicalHeight,
|
||||
// surface: texture.surfaceAddress == null
|
||||
// ? nullptr
|
||||
// : Pointer<Void>.fromAddress(texture.surfaceAddress!));
|
||||
|
||||
// if (texture.hardwareTextureId != null) {
|
||||
// if (_renderTarget != null) {
|
||||
// await _renderTarget!.destroy();
|
||||
// }
|
||||
// // ignore: unused_local_variable
|
||||
// _renderTarget = await _viewer?.createRenderTarget(
|
||||
// physicalWidth, physicalHeight, texture.hardwareTextureId!);
|
||||
// }
|
||||
|
||||
// await _viewer?.updateViewportAndCameraProjection(
|
||||
// physicalWidth.toDouble(), physicalHeight.toDouble());
|
||||
// _creatingTexture = false;
|
||||
// _textures.add(texture);
|
||||
// return texture;
|
||||
}
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future resizeWindow(
|
||||
int width,
|
||||
int height,
|
||||
int offsetLeft,
|
||||
int offsetTop,
|
||||
) async {
|
||||
throw Exception("Not supported on iOS");
|
||||
}
|
||||
}
|
||||
@@ -1,256 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses a Flutter platform
|
||||
/// channel to create a rendering context, resource loaders, and
|
||||
/// render target(s).
|
||||
///
|
||||
class ThermionFlutterFFI extends ThermionFlutterPlatform {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
|
||||
ThermionViewerFFI? _viewer;
|
||||
|
||||
ThermionFlutterFFI._() {}
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterFFI._();
|
||||
}
|
||||
|
||||
final _textures = <ThermionFlutterTexture>{};
|
||||
|
||||
Future<ThermionViewer> createViewerWithOptions(
|
||||
ThermionFlutterOptions options) async {
|
||||
return createViewer(uberarchivePath: options.uberarchivePath);
|
||||
}
|
||||
|
||||
Future<ThermionViewer> createViewer({String? uberarchivePath}) async {
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await _channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
|
||||
var renderCallback =
|
||||
Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
|
||||
renderCallbackResult[0]);
|
||||
var renderCallbackOwner =
|
||||
Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
||||
|
||||
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
|
||||
var sharedContext = await _channel.invokeMethod("getSharedContext");
|
||||
|
||||
var sharedContextPtr = sharedContext == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(sharedContext);
|
||||
|
||||
_viewer = ThermionViewerFFI(
|
||||
resourceLoader: resourceLoader,
|
||||
renderCallback: renderCallback,
|
||||
renderCallbackOwner: renderCallbackOwner,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: uberarchivePath);
|
||||
await _viewer!.initialized;
|
||||
return _viewer!;
|
||||
}
|
||||
|
||||
bool _creatingTexture = false;
|
||||
bool _destroyingTexture = false;
|
||||
|
||||
Future _waitForTextureCreationToComplete() async {
|
||||
var iter = 0;
|
||||
|
||||
while (_creatingTexture || _destroyingTexture) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
iter++;
|
||||
if (iter > 10) {
|
||||
throw Exception(
|
||||
"Previous call to createTexture failed to complete within 500ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Create a backing surface for rendering.
|
||||
/// This is called by [ThermionWidget]; don't call this yourself.
|
||||
///
|
||||
/// The name here is slightly misleading because we only create
|
||||
/// a texture render target on macOS and iOS; on Android, we render into
|
||||
/// a native window derived from a Surface, and on Windows we render into
|
||||
/// a HWND.
|
||||
///
|
||||
/// Currently, this only supports a single "texture" (aka rendering surface)
|
||||
/// at any given time. If a [ThermionWidget] is disposed, it will call
|
||||
/// [destroyTexture]; if it is resized, it will call [resizeTexture].
|
||||
///
|
||||
/// In future, we probably want to be able to create multiple distinct
|
||||
/// textures/render targets. This would make it possible to have multiple
|
||||
/// Flutter Texture widgets, each with its own Filament View attached.
|
||||
/// The current design doesn't accommodate this (for example, it seems we can
|
||||
/// only create a single native window from a Surface at any one time).
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(double width, double height,
|
||||
double offsetLeft, double offsetRight, double pixelRatio) async {
|
||||
final physicalWidth = (width * pixelRatio).ceil();
|
||||
final physicalHeight = (height * pixelRatio).ceil();
|
||||
// when a ThermionWidget is inserted, disposed then immediately reinserted
|
||||
// into the widget hierarchy (e.g. rebuilding due to setState(() {}) being called in an ancestor widget)
|
||||
// the first call to createTexture may not have completed before the second.
|
||||
// add a loop here to wait (max 500ms) for the first call to complete
|
||||
await _waitForTextureCreationToComplete();
|
||||
|
||||
// note that when [ThermionWidget] is disposed, we don't destroy the
|
||||
// texture; instead, we keep it around in case a subsequent call requests
|
||||
// a texture of the same size.
|
||||
|
||||
if (_textures.length > 1) {
|
||||
throw Exception("Multiple textures not yet supported");
|
||||
} else if (_textures.length == 1) {
|
||||
if (_textures.first.height == physicalHeight &&
|
||||
_textures.first.width == physicalWidth) {
|
||||
return _textures.first;
|
||||
} else {
|
||||
await _viewer!.setRendering(false);
|
||||
await _swapChain?.destroy();
|
||||
await destroyTexture(_textures.first);
|
||||
_textures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
_creatingTexture = true;
|
||||
|
||||
var result = await _channel.invokeMethod("createTexture",
|
||||
[physicalWidth, physicalHeight, offsetLeft, offsetLeft]);
|
||||
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
final flutterTextureId = result[0] as int?;
|
||||
final hardwareTextureId = result[1] as int?;
|
||||
final surfaceAddress = result[2] as int?;
|
||||
|
||||
_logger.info(
|
||||
"Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
_viewer?.viewportDimensions =
|
||||
(physicalWidth.toDouble(), physicalHeight.toDouble());
|
||||
|
||||
final texture = ThermionFlutterTexture(flutterTextureId, hardwareTextureId,
|
||||
physicalWidth, physicalHeight, surfaceAddress);
|
||||
|
||||
await _viewer?.createSwapChain(physicalWidth, physicalHeight,
|
||||
surface: texture.surfaceAddress == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(texture.surfaceAddress!));
|
||||
|
||||
if (texture.hardwareTextureId != null) {
|
||||
if (_renderTarget != null) {
|
||||
await _renderTarget!.destroy();
|
||||
}
|
||||
// ignore: unused_local_variable
|
||||
_renderTarget = await _viewer?.createRenderTarget(
|
||||
physicalWidth,
|
||||
physicalHeight,
|
||||
texture.hardwareTextureId!);
|
||||
}
|
||||
|
||||
await _viewer?.updateViewportAndCameraProjection(
|
||||
physicalWidth.toDouble(), physicalHeight.toDouble());
|
||||
_creatingTexture = false;
|
||||
_textures.add(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
///
|
||||
/// Destroy a texture and clean up the texture cache (if applicable).
|
||||
///
|
||||
Future destroyTexture(ThermionFlutterTexture texture) async {
|
||||
if (_creatingTexture || _destroyingTexture) {
|
||||
throw Exception(
|
||||
"Cannot destroy texture while concurrent call to createTexture/destroyTexture has not completed");
|
||||
}
|
||||
_destroyingTexture = true;
|
||||
_textures.remove(texture);
|
||||
await _channel.invokeMethod("destroyTexture", texture.flutterTextureId);
|
||||
_destroyingTexture = false;
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> resizeTexture(
|
||||
ThermionFlutterTexture texture,
|
||||
int width,
|
||||
int height,
|
||||
int offsetLeft,
|
||||
int offsetTop,
|
||||
double pixelRatio) async {
|
||||
if (_resizing) {
|
||||
throw Exception("Resize underway");
|
||||
}
|
||||
|
||||
width = (width * pixelRatio).ceil();
|
||||
height = (height * pixelRatio).ceil();
|
||||
|
||||
if ((width - _viewer!.viewportDimensions.$1).abs() < 0.001 ||
|
||||
(height - _viewer!.viewportDimensions.$2).abs() < 0.001) {
|
||||
return texture;
|
||||
}
|
||||
_resizing = true;
|
||||
bool wasRendering = _viewer!.rendering;
|
||||
await _viewer!.setRendering(false);
|
||||
await _swapChain?.destroy();
|
||||
await destroyTexture(texture);
|
||||
|
||||
var result = await _channel
|
||||
.invokeMethod("createTexture", [width, height, offsetLeft, offsetLeft]);
|
||||
|
||||
if (result == null || result[0] == -1) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
_viewer!.viewportDimensions = (width.toDouble(), height.toDouble());
|
||||
var newTexture =
|
||||
ThermionFlutterTexture(result[0], result[1], width, height, result[2]);
|
||||
|
||||
await _viewer!.createSwapChain(width, height,
|
||||
surface: newTexture.surfaceAddress == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(newTexture.surfaceAddress!));
|
||||
|
||||
if (newTexture.hardwareTextureId != null) {
|
||||
// ignore: unused_local_variable
|
||||
var renderTarget = await _viewer!.createRenderTarget(
|
||||
width, height, newTexture.hardwareTextureId!);
|
||||
}
|
||||
await _viewer!
|
||||
.updateViewportAndCameraProjection(width.toDouble(), height.toDouble());
|
||||
|
||||
_viewer!.viewportDimensions = (width.toDouble(), height.toDouble());
|
||||
if (wasRendering) {
|
||||
await _viewer!.setRendering(true);
|
||||
}
|
||||
_textures.add(newTexture);
|
||||
_resizing = false;
|
||||
return newTexture;
|
||||
}
|
||||
}
|
||||
export 'thermion_flutter_android.dart';
|
||||
export 'thermion_flutter_macos.dart';
|
||||
export 'thermion_flutter_windows.dart';
|
||||
export 'thermion_flutter_ios.dart';
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterIOS
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
|
||||
ThermionViewerFFI? _viewer;
|
||||
|
||||
ThermionFlutterIOS._() {}
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterIOS._();
|
||||
}
|
||||
|
||||
final _textures = <ThermionFlutterTexture>{};
|
||||
|
||||
bool _creatingTexture = false;
|
||||
bool _destroyingTexture = false;
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Create a rendering surface.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself.
|
||||
///
|
||||
/// The name here is slightly misleading because we only create
|
||||
/// a texture render target on macOS and iOS; on Android, we render into
|
||||
/// a native window derived from a Surface, and on Windows we render into
|
||||
/// a HWND.
|
||||
///
|
||||
/// Currently, this only supports a single "texture" (aka rendering surface)
|
||||
/// at any given time. If a [ThermionWidget] is disposed, it will call
|
||||
/// [destroyTexture]; if it is resized, it will call [resizeTexture].
|
||||
///
|
||||
/// In future, we probably want to be able to create multiple distinct
|
||||
/// textures/render targets. This would make it possible to have multiple
|
||||
/// Flutter Texture widgets, each with its own Filament View attached.
|
||||
/// The current design doesn't accommodate this (for example, it seems we can
|
||||
/// only create a single native window from a Surface at any one time).
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
throw Exception("TODO");
|
||||
// note that when [ThermionWidget] is disposed, we don't destroy the
|
||||
// texture; instead, we keep it around in case a subsequent call requests
|
||||
// a texture of the same size.
|
||||
|
||||
// if (_textures.length > 1) {
|
||||
// throw Exception("Multiple textures not yet supported");
|
||||
// } else if (_textures.length == 1) {
|
||||
// if (_textures.first.height == physicalHeight &&
|
||||
// _textures.first.width == physicalWidth) {
|
||||
// return _textures.first;
|
||||
// } else {
|
||||
// await _viewer!.setRendering(false);
|
||||
// await _swapChain?.destroy();
|
||||
// await destroyTexture(_textures.first);
|
||||
// _textures.clear();
|
||||
// }
|
||||
// }
|
||||
|
||||
// _creatingTexture = true;
|
||||
|
||||
// var result = await _channel.invokeMethod("createTexture",
|
||||
// [physicalWidth, physicalHeight, offsetLeft, offsetLeft]);
|
||||
|
||||
// if (result == null || (result[0] == -1)) {
|
||||
// throw Exception("Failed to create texture");
|
||||
// }
|
||||
// final flutterTextureId = result[0] as int?;
|
||||
// final hardwareTextureId = result[1] as int?;
|
||||
// final surfaceAddress = result[2] as int?;
|
||||
|
||||
// _logger.info(
|
||||
// "Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
// final texture = ThermionFlutterTexture(flutterTextureId, hardwareTextureId,
|
||||
// physicalWidth, physicalHeight, surfaceAddress);
|
||||
|
||||
// await _viewer?.createSwapChain(physicalWidth, physicalHeight,
|
||||
// surface: texture.surfaceAddress == null
|
||||
// ? nullptr
|
||||
// : Pointer<Void>.fromAddress(texture.surfaceAddress!));
|
||||
|
||||
// if (texture.hardwareTextureId != null) {
|
||||
// if (_renderTarget != null) {
|
||||
// await _renderTarget!.destroy();
|
||||
// }
|
||||
// // ignore: unused_local_variable
|
||||
// _renderTarget = await _viewer?.createRenderTarget(
|
||||
// physicalWidth, physicalHeight, texture.hardwareTextureId!);
|
||||
// }
|
||||
|
||||
// await _viewer?.updateViewportAndCameraProjection(
|
||||
// physicalWidth.toDouble(), physicalHeight.toDouble());
|
||||
// _creatingTexture = false;
|
||||
// _textures.add(texture);
|
||||
// return texture;
|
||||
}
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future resizeWindow(
|
||||
int width,
|
||||
int height,
|
||||
int offsetLeft,
|
||||
int offsetTop,
|
||||
) async {
|
||||
throw Exception("Not supported on iOS");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
|
||||
SwapChain? _swapChain;
|
||||
|
||||
ThermionFlutterMacOS._() {}
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterMacOS._();
|
||||
}
|
||||
|
||||
// On desktop platforms, textures are always created
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
if (_swapChain == null) {
|
||||
// this is the headless swap chain
|
||||
// since we will be using render targets, the actual dimensions don't matter
|
||||
_swapChain = await viewer!.createSwapChain(width, height);
|
||||
}
|
||||
// Get screen width and height
|
||||
int screenWidth = width; //1920;
|
||||
int screenHeight = height; //1080;
|
||||
|
||||
if (width > screenWidth || height > screenHeight) {
|
||||
throw Exception("TODO - unsupported");
|
||||
}
|
||||
|
||||
var result = await _channel
|
||||
.invokeMethod("createTexture", [screenWidth, screenHeight, 0, 0]);
|
||||
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
final flutterTextureId = result[0] as int?;
|
||||
final hardwareTextureId = result[1] as int?;
|
||||
final surfaceAddress = result[2] as int?;
|
||||
|
||||
|
||||
_logger.info(
|
||||
"Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
return MacOSMethodChannelFlutterTexture(_channel, flutterTextureId!,
|
||||
hardwareTextureId!, screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
// On MacOS, we currently use textures/render targets, so there's no window to resize
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> resizeWindow(
|
||||
int width, int height, int offsetTop, int offsetRight) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class MacOSMethodChannelFlutterTexture extends MethodChannelFlutterTexture {
|
||||
MacOSMethodChannelFlutterTexture(super.channel, super.flutterId,
|
||||
super.hardwareId, super.width, super.height);
|
||||
|
||||
@override
|
||||
Future resize(int width, int height, int left, int top) async {
|
||||
if (width > this.width || height > this.height || left != 0 || top != 0) {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
|
||||
///
|
||||
/// An abstract implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
abstract class ThermionFlutterMethodChannelInterface
|
||||
extends ThermionFlutterPlatform {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterMethodChannelInterface");
|
||||
|
||||
ThermionViewerFFI? viewer;
|
||||
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if (viewer != null) {
|
||||
throw Exception(
|
||||
"Only one viewer can be created over the lifetime of an application");
|
||||
}
|
||||
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await _channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
var renderCallback = nullptr;
|
||||
var renderCallbackOwner = nullptr;
|
||||
|
||||
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
|
||||
var sharedContext = await _channel.invokeMethod("getSharedContext");
|
||||
|
||||
var sharedContextPtr = sharedContext == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(sharedContext);
|
||||
|
||||
viewer = ThermionViewerFFI(
|
||||
resourceLoader: resourceLoader,
|
||||
renderCallback: renderCallback,
|
||||
renderCallbackOwner: renderCallbackOwner,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: options?.uberarchivePath);
|
||||
await viewer!.initialized;
|
||||
return viewer!;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
|
||||
final MethodChannel _channel;
|
||||
|
||||
MethodChannelFlutterTexture(
|
||||
this._channel, this.flutterId, this.hardwareId, this.width, this.height);
|
||||
|
||||
Future destroy() async {
|
||||
await _channel.invokeMethod("destroyTexture", hardwareId);
|
||||
}
|
||||
|
||||
@override
|
||||
final int flutterId;
|
||||
|
||||
@override
|
||||
final int hardwareId;
|
||||
|
||||
@override
|
||||
final int height;
|
||||
|
||||
@override
|
||||
final int width;
|
||||
|
||||
Future markFrameAvailable() async {
|
||||
await _channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterWindows
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
|
||||
final _logger = Logger("ThermionFlutterWindows");
|
||||
|
||||
ThermionViewerFFI? _viewer;
|
||||
|
||||
ThermionFlutterWindows._() {}
|
||||
|
||||
SwapChain? _swapChain;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterWindows._();
|
||||
}
|
||||
|
||||
///
|
||||
/// Not supported on Windows. Throws an exception.
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
throw Exception("Texture not supported on Windows");
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future resizeWindow(
|
||||
int width, int height, int offsetLeft, int offsetTop) async {
|
||||
if (_resizing) {
|
||||
throw Exception("Resize underway");
|
||||
}
|
||||
|
||||
throw Exception("TODO");
|
||||
|
||||
// final view = await this._viewer!.getViewAt(0);
|
||||
// final viewport = await view.getViewport();
|
||||
// final swapChain = await this._viewer.getSwapChainAt(0);
|
||||
|
||||
// if (width == viewport.width && height - viewport.height == 0) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// _resizing = true;
|
||||
// bool wasRendering = _viewer!.rendering;
|
||||
// await _viewer!.setRendering(false);
|
||||
// await _swapChain?.destroy();
|
||||
|
||||
// var result = await _channel
|
||||
// .invokeMethod("createTexture", [width, height, offsetLeft, offsetLeft]);
|
||||
|
||||
// if (result == null || result[0] == -1) {
|
||||
// throw Exception("Failed to create texture");
|
||||
// }
|
||||
|
||||
// var newTexture =
|
||||
// ThermionFlutterTexture(result[0], result[1], width, height, result[2]);
|
||||
|
||||
// await _viewer!.createSwapChain(width, height,
|
||||
// surface: newTexture.surfaceAddress == null
|
||||
// ? nullptr
|
||||
// : Pointer<Void>.fromAddress(newTexture.surfaceAddress!));
|
||||
|
||||
// if (newTexture.hardwareTextureId != null) {
|
||||
// // ignore: unused_local_variable
|
||||
// var renderTarget = await _viewer!
|
||||
// .createRenderTarget(width, height, newTexture.hardwareTextureId!);
|
||||
// }
|
||||
|
||||
// await _viewer!
|
||||
// .updateViewportAndCameraProjection(width.toDouble(), height.toDouble());
|
||||
|
||||
// if (wasRendering) {
|
||||
// await _viewer!.setRendering(true);
|
||||
// }
|
||||
// _textures.add(newTexture);
|
||||
// _resizing = false;
|
||||
// return newTexture;
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ flutter:
|
||||
implements: thermion_flutter_platform_interface
|
||||
platforms:
|
||||
ios:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterIOS
|
||||
android:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterAndroid
|
||||
macos:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterMacOS
|
||||
windows:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterWindows
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
@@ -9,7 +9,6 @@ class ThermionFlutterOptions {
|
||||
|
||||
ThermionFlutterOptions({this.uberarchivePath});
|
||||
const ThermionFlutterOptions.empty() : uberarchivePath = null;
|
||||
|
||||
}
|
||||
|
||||
abstract class ThermionFlutterPlatform extends PlatformInterface {
|
||||
@@ -25,17 +24,23 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
Future<ThermionViewer> createViewerWithOptions(
|
||||
covariant ThermionFlutterOptions options);
|
||||
///
|
||||
///
|
||||
///
|
||||
Future<ThermionViewer> createViewer(
|
||||
{covariant ThermionFlutterOptions? options});
|
||||
|
||||
@deprecated
|
||||
Future<ThermionViewer> createViewer({String? uberarchivePath});
|
||||
///
|
||||
/// Create a rendering surface.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself. May not be supported on all platforms.
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height);
|
||||
|
||||
Future<ThermionFlutterTexture?> createTexture(double width, double height,
|
||||
double offsetLeft, double offsetTop, double pixelRatio);
|
||||
|
||||
Future destroyTexture(ThermionFlutterTexture texture);
|
||||
|
||||
Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
|
||||
int width, int height, int offsetTop, int offsetRight, double pixelRatio);
|
||||
///
|
||||
///
|
||||
///
|
||||
Future resizeWindow(
|
||||
int width, int height, int offsetTop, int offsetRight);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
class ThermionFlutterTexture {
|
||||
final int width;
|
||||
final int height;
|
||||
final int? flutterTextureId;
|
||||
final int? hardwareTextureId;
|
||||
final int? surfaceAddress;
|
||||
bool get usesBackingWindow => flutterTextureId == null;
|
||||
// class ThermionFlutterTextureImpl {
|
||||
// final int width;
|
||||
// final int height;
|
||||
// final int? flutterTextureId;
|
||||
// final int? hardwareTextureId;
|
||||
// final int? surfaceAddress;
|
||||
// bool get usesBackingWindow => flutterTextureId == null;
|
||||
|
||||
ThermionFlutterTexture(this.flutterTextureId, this.hardwareTextureId,
|
||||
this.width, this.height, this.surfaceAddress) {
|
||||
// ThermionFlutterTexture(this.flutterTextureId, this.hardwareTextureId,
|
||||
// this.width, this.height, this.surfaceAddress) {
|
||||
|
||||
}
|
||||
// }
|
||||
// }
|
||||
|
||||
abstract class ThermionFlutterTexture {
|
||||
int get width;
|
||||
int get height;
|
||||
|
||||
int get flutterId;
|
||||
int get hardwareId;
|
||||
|
||||
///
|
||||
/// Destroy a texture and clean up the texture cache (if applicable).
|
||||
///
|
||||
Future destroy();
|
||||
|
||||
Future resize(int width, int height, int left, int top);
|
||||
|
||||
Future markFrameAvailable();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user