From 22020d8607556a2b89d0dbe8aa1bd66cc87438b1 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Sat, 28 Sep 2024 18:28:05 +0800 Subject: [PATCH] refactor: continual refactor to support multiple render targets --- .../lib/src/entities/abstract_gizmo.dart | 21 -- .../fixed_orbit_camera_rotation_delegate.dart | 11 +- .../free_flight_camera_delegate.dart | 8 +- .../third_person_camera_delegate.dart | 4 +- .../lib/src/{entities => utils}/gizmo.dart | 59 ++-- .../lib/src/viewer/src/ffi/src/ffi_gizmo.dart | 50 +++ .../lib/src/viewer/src/ffi/src/ffi_view.dart | 42 +++ .../viewer/src/ffi/src/thermion_dart.g.dart | 89 +++-- .../src/ffi/src/thermion_viewer_ffi.dart | 166 +++------ .../viewer/src/shared_types/pick_result.dart | 4 +- .../lib/src/viewer/src/shared_types/view.dart | 4 +- .../src/viewer/src/thermion_viewer_base.dart | 54 +-- .../src/viewer/src/thermion_viewer_stub.dart | 25 +- .../src/web_js/src/thermion_viewer_js.dart | 2 +- .../src/viewer/src/web_wasm/src/camera.dart | 24 ++ .../web_wasm/src/thermion_viewer_wasm.dart | 4 +- .../native/include/FilamentViewer.hpp | 8 + .../native/include/ThermionDartApi.h | 1 + .../include/ThermionDartRenderThreadApi.h | 2 +- .../generated/ThermionTextureSwiftObjCAPI.h | 325 ++++++++++++++++++ thermion_dart/native/src/TView.cpp | 6 + thermion_dart/native/src/ThermionDartApi.cpp | 13 +- thermion_dart/test/camera_tests.dart | 10 +- thermion_dart/test/gizmo_tests.dart | 17 + thermion_dart/test/helpers.dart | 25 +- thermion_dart/test/integration_test.dart | 10 - .../test/libThermionTextureSwift.dylib | Bin 0 -> 110976 bytes thermion_dart/test/render_thread.dart | 33 ++ thermion_dart/test/view_tests.dart | 47 +-- thermion_dart/test/viewport_gizmo.dart | 4 +- thermion_dart/test/viewport_grid.dart | 4 +- .../lib/src/thermion_flutter_plugin.dart | 104 +----- .../src/widgets/src/pixel_ratio_aware.dart | 12 + .../widgets/src/thermion_listener_widget.dart | 63 ++-- .../widgets/src/thermion_texture_widget.dart | 270 +++++++++++++++ .../lib/src/widgets/src/thermion_widget.dart | 150 +++----- .../widgets/src/thermion_widget_web_impl.dart | 12 +- .../widgets/src/thermion_widget_web_stub.dart | 14 +- .../widgets/src/thermion_widget_windows.dart | 14 + .../Classes/SwiftThermionFlutterPlugin.swift | 19 +- .../Classes/ThermionFlutterTexture.swift | 4 +- .../thermion_flutter/test/cpp/test.cpp | 2 +- .../lib/thermion_flutter_android.dart | 130 +++++++ .../lib/thermion_flutter_ffi.dart | 260 +------------- .../lib/thermion_flutter_ios.dart | 131 +++++++ .../lib/thermion_flutter_macos.dart | 77 +++++ ...mion_flutter_method_channel_interface.dart | 86 +++++ .../lib/thermion_flutter_windows.dart | 97 ++++++ .../thermion_flutter_ffi/pubspec.yaml | 8 +- .../thermion_flutter_platform_interface.dart | 29 +- .../lib/thermion_flutter_texture.dart | 37 +- 51 files changed, 1714 insertions(+), 877 deletions(-) delete mode 100644 thermion_dart/lib/src/entities/abstract_gizmo.dart rename thermion_dart/lib/src/{entities => utils}/gizmo.dart (63%) create mode 100644 thermion_dart/lib/src/viewer/src/ffi/src/ffi_gizmo.dart create mode 100644 thermion_dart/lib/src/viewer/src/ffi/src/ffi_view.dart create mode 100644 thermion_dart/native/include/generated/ThermionTextureSwiftObjCAPI.h create mode 100644 thermion_dart/test/gizmo_tests.dart create mode 100644 thermion_dart/test/libThermionTextureSwift.dylib create mode 100644 thermion_dart/test/render_thread.dart create mode 100644 thermion_flutter/thermion_flutter/lib/src/widgets/src/pixel_ratio_aware.dart create mode 100644 thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_texture_widget.dart create mode 100644 thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_windows.dart create mode 100644 thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_android.dart create mode 100644 thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ios.dart create mode 100644 thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_macos.dart create mode 100644 thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_method_channel_interface.dart create mode 100644 thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_windows.dart diff --git a/thermion_dart/lib/src/entities/abstract_gizmo.dart b/thermion_dart/lib/src/entities/abstract_gizmo.dart deleted file mode 100644 index cebe9d42..00000000 --- a/thermion_dart/lib/src/entities/abstract_gizmo.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:vector_math/vector_math_64.dart'; - -import '../../thermion_dart.dart'; - -abstract class AbstractGizmo { - - bool get isVisible; - bool get isHovered; - - Future translate(double transX, double transY); - - void reset(); - - Future attach(ThermionEntity entity); - - Future detach(); - - Stream get boundingBox; - - void checkHover(double x, double y); -} diff --git a/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart index 047dba30..4059a553 100644 --- a/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart @@ -56,6 +56,9 @@ class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate { return; } + final view = await viewer.getViewAt(0); + final viewport = await view.getViewport(); + var viewMatrix = await viewer.getCameraViewMatrix(); var modelMatrix = await viewer.getCameraModelMatrix(); var projectionMatrix = await viewer.getCameraProjectionMatrix(); @@ -80,12 +83,8 @@ class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate { intersectionInClipSpace / intersectionInClipSpace.w; // Calculate new camera position based on rotation - final ndcX = 2 * - ((-_queuedRotationDelta.x * viewer.pixelRatio) / - viewer.viewportDimensions.$1); - final ndcY = 2 * - ((_queuedRotationDelta.y * viewer.pixelRatio) / - viewer.viewportDimensions.$2); + final ndcX = 2 * ((-_queuedRotationDelta.x) / viewport.width); + final ndcY = 2 * ((_queuedRotationDelta.y) / viewport.height); final ndc = Vector4(ndcX, ndcY, intersectionInNdcSpace.z, 1.0); var clipSpace = Vector4( diff --git a/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart index 5ceaf84f..eae5040c 100644 --- a/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart @@ -93,9 +93,9 @@ class FreeFlightInputHandlerDelegate implements InputHandlerDelegate { // Apply rotation if (_queuedRotationDelta.length2 > 0.0) { double deltaX = - _queuedRotationDelta.x * rotationSensitivity * viewer.pixelRatio; + _queuedRotationDelta.x * rotationSensitivity; double deltaY = - _queuedRotationDelta.y * rotationSensitivity * viewer.pixelRatio; + _queuedRotationDelta.y * rotationSensitivity; Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX); Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY); @@ -109,8 +109,8 @@ class FreeFlightInputHandlerDelegate implements InputHandlerDelegate { Vector3 right = _right.clone()..applyQuaternion(currentRotation); Vector3 up = _up.clone()..applyQuaternion(currentRotation); - double deltaX = _queuedPanDelta.x * panSensitivity * viewer.pixelRatio; - double deltaY = _queuedPanDelta.y * panSensitivity * viewer.pixelRatio; + double deltaX = _queuedPanDelta.x * panSensitivity; + double deltaY = _queuedPanDelta.y * panSensitivity; relativeTranslation += right * deltaX + up * deltaY; _queuedPanDelta = Vector2.zero(); diff --git a/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart b/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart index edbee074..16e794f3 100644 --- a/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart @@ -96,9 +96,9 @@ class OverTheShoulderCameraDelegate implements InputHandlerDelegate { // camera is always looking at -Z, whereas models generally face towards +Z if (_queuedRotationDelta.length2 > 0.0) { double deltaX = - _queuedRotationDelta.x * rotationSensitivity * viewer.pixelRatio; + _queuedRotationDelta.x * rotationSensitivity; double deltaY = - _queuedRotationDelta.y * rotationSensitivity * viewer.pixelRatio; + _queuedRotationDelta.y * rotationSensitivity; cameraLookAt = Matrix4.rotationY(-deltaX) * Matrix4.rotationX(-deltaY) * diff --git a/thermion_dart/lib/src/entities/gizmo.dart b/thermion_dart/lib/src/utils/gizmo.dart similarity index 63% rename from thermion_dart/lib/src/entities/gizmo.dart rename to thermion_dart/lib/src/utils/gizmo.dart index aef7fd60..ad023f0c 100644 --- a/thermion_dart/lib/src/entities/gizmo.dart +++ b/thermion_dart/lib/src/utils/gizmo.dart @@ -1,19 +1,31 @@ import 'dart:async'; -import 'package:thermion_dart/src/entities/abstract_gizmo.dart'; +import 'package:thermion_dart/src/viewer/viewer.dart'; import 'package:vector_math/vector_math_64.dart'; -import '../viewer/viewer.dart'; -class Gizmo extends AbstractGizmo { +abstract class Gizmo { + bool get isVisible; + bool get isHovered; + + void reset(); + + Future attach(ThermionEntity entity); + Future detach(); + + Stream get boundingBox; + + void checkHover(int x, int y); +} + +abstract class BaseGizmo extends Gizmo { final ThermionEntity x; final ThermionEntity y; final ThermionEntity z; final ThermionEntity center; - final ThermionViewer _viewer; - ThermionEntity? _activeAxis; ThermionEntity? _activeEntity; + ThermionViewer viewer; bool _visible = false; bool get isVisible => _visible; @@ -26,10 +38,9 @@ class Gizmo extends AbstractGizmo { Stream get boundingBox => _boundingBoxController.stream; final _boundingBoxController = StreamController.broadcast(); - Gizmo(this.x, this.y, this.z, this.center, this._viewer, - {this.ignore = const {}}) { - _viewer.gizmoPickResult.listen(_onGizmoPickResult); - _viewer.pickResult.listen(_onPickResult); + BaseGizmo({required this.x, required this.y, required this.z, required this.center, required this.viewer, + this.ignore = const {}}) { + onPick(_onGizmoPickResult); } final _stopwatch = Stopwatch(); @@ -52,12 +63,10 @@ class Gizmo extends AbstractGizmo { final axis = Vector3(_activeAxis == x ? 1.0 : 0.0, _activeAxis == y ? 1.0 : 0.0, _activeAxis == z ? 1.0 : 0.0); - await _viewer.queueRelativePositionUpdateWorldAxis( + await viewer.queueRelativePositionUpdateWorldAxis( _activeEntity!, - _transX * _viewer.pixelRatio, - -_transY * - _viewer - .pixelRatio, // flip the sign because "up" in NDC Y axis is positive, but negative in Flutter + _transX, + -_transY, // flip the sign because "up" in NDC Y axis is positive, but negative in Flutter axis.x, axis.y, axis.z); @@ -70,10 +79,6 @@ class Gizmo extends AbstractGizmo { _activeAxis = null; } - void _onPickResult(FilamentPickResult result) async { - await attach(result.entity); - } - void _onGizmoPickResult(FilamentPickResult result) async { if (result.entity == x || result.entity == y || result.entity == z) { _activeAxis = result.entity; @@ -98,21 +103,25 @@ class Gizmo extends AbstractGizmo { _visible = true; if (_activeEntity != null) { - await _viewer.removeStencilHighlight(_activeEntity!); + await viewer.removeStencilHighlight(_activeEntity!); } _activeEntity = entity; - await _viewer.setGizmoVisibility(true); - await _viewer.setParent(center, entity, preserveScaling: false); - _boundingBoxController.sink.add(await _viewer.getViewportBoundingBox(x)); + await viewer.setParent(center, entity, preserveScaling: false); + _boundingBoxController.sink.add(await viewer.getViewportBoundingBox(x)); } Future detach() async { - await _viewer.setGizmoVisibility(false); + await setVisibility(false); } @override - void checkHover(double x, double y) { - _viewer.pickGizmo(x.toInt(), y.toInt()); + void checkHover(int x, int y) { + pick(x, y); } + + Future pick(int x, int y); + + Future setVisibility(bool visible); + void onPick(void Function(PickResult result) callback); } diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/ffi_gizmo.dart b/thermion_dart/lib/src/viewer/src/ffi/src/ffi_gizmo.dart new file mode 100644 index 00000000..0ff2b8a6 --- /dev/null +++ b/thermion_dart/lib/src/viewer/src/ffi/src/ffi_gizmo.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'dart:ffi'; + +import 'package:thermion_dart/src/viewer/src/ffi/src/thermion_dart.g.dart'; + +import '../../../../utils/gizmo.dart'; +import '../../../viewer.dart'; + +class FFIGizmo extends BaseGizmo { + Pointer pointer; + + late NativeCallable _nativeCallback; + FFIGizmo( + this.pointer, ThermionViewer viewer) : super(x: 0, y: 0, z: 0, center: 0, viewer: viewer) { + _nativeCallback = + NativeCallable.listener(_onPickResult); + } + + /// + /// The result(s) of calling [pickGizmo] (see below). + /// + // Stream get onPick => _pickResultController.stream; + // final _pickResultController = StreamController.broadcast(); + + void Function(PickResult)? _callback; + + void onPick(void Function(PickResult) callback) { + _callback = callback; + } + + void _onPickResult(DartEntityId entityId, int x, int y, Pointer view) { + _callback?.call((entity: entityId, x: x, y: y)); + } + + /// + /// Used to test whether a Gizmo is at the given viewport coordinates. + /// Called by `FilamentGestureDetector` on a mouse/finger down event. You probably don't want to call this yourself. + /// This is asynchronous and will require 2-3 frames to complete - subscribe to the [gizmoPickResult] stream to receive the results of this method. + /// [x] and [y] must be in local logical coordinates (i.e. where 0,0 is at top-left of the ThermionWidget). + /// + @override + Future pick(int x, int y) async { + Gizmo_pick(pointer, x.toInt(), y, _nativeCallback.nativeFunction); + } + + @override + Future setVisibility(bool visible) async { + Gizmo_setVisibility(pointer, visible); + } +} diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/ffi_view.dart b/thermion_dart/lib/src/viewer/src/ffi/src/ffi_view.dart new file mode 100644 index 00000000..81dca075 --- /dev/null +++ b/thermion_dart/lib/src/viewer/src/ffi/src/ffi_view.dart @@ -0,0 +1,42 @@ +import 'dart:ffi'; + +import 'package:thermion_dart/src/viewer/src/ffi/src/thermion_dart.g.dart'; +import 'package:thermion_dart/src/viewer/src/shared_types/camera.dart'; + +import '../../shared_types/view.dart'; +import '../thermion_viewer_ffi.dart'; +import 'thermion_viewer_ffi.dart'; + +class FFIView extends View { + final Pointer view; + final Pointer viewer; + + FFIView(this.view, this.viewer); + + @override + Future updateViewport(int width, int height) async { + View_updateViewport(view, width, height); + } + + @override + Future setRenderTarget(covariant FFIRenderTarget renderTarget) async { + View_setRenderTarget(view, renderTarget.renderTarget); + } + + @override + Future setCamera(FFICamera camera) async { + View_setCamera(view, camera.camera); + } + + @override + Future getViewport() async { + TViewport vp = View_getViewport(view); + return Viewport(vp.left, vp.bottom, vp.width, vp.height); + } + + @override + Camera getCamera() { + final engine = Viewer_getEngine(viewer); + return FFICamera(View_getCamera(view), engine); + } +} diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_dart.g.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_dart.g.dart index 4aacfc3b..1bb847f0 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_dart.g.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_dart.g.dart @@ -139,6 +139,13 @@ external void Viewer_setMainCamera( ffi.Pointer tView, ); +@ffi.Native Function(ffi.Pointer, ffi.Int)>( + isLeaf: true) +external ffi.Pointer Viewer_getSwapChainAt( + ffi.Pointer tViewer, + int index, +); + @ffi.Native Function(ffi.Pointer)>(isLeaf: true) external ffi.Pointer Viewer_getEngine( ffi.Pointer viewer, @@ -662,9 +669,9 @@ external void SceneManager_setVisibilityLayer( int layer, ); -@ffi.Native Function(ffi.Pointer)>( +@ffi.Native Function(ffi.Pointer)>( isLeaf: true) -external ffi.Pointer SceneManager_getGizmo( +external ffi.Pointer SceneManager_getScene( ffi.Pointer tSceneManager, ); @@ -1176,14 +1183,6 @@ external void set_priority( int priority, ); -@ffi.Native< - ffi.Void Function( - ffi.Pointer, ffi.Pointer)>(isLeaf: true) -external void get_gizmo( - ffi.Pointer sceneManager, - ffi.Pointer out, -); - @ffi.Native< Aabb2 Function( ffi.Pointer, ffi.Pointer, EntityId)>(isLeaf: true) @@ -1212,13 +1211,6 @@ external void get_bounding_box_to_out( ffi.Pointer maxY, ); -@ffi.Native, ffi.Bool)>( - isLeaf: true) -external void set_gizmo_visibility( - ffi.Pointer sceneManager, - bool visible, -); - @ffi.Native< ffi.Void Function(ffi.Pointer, EntityId, ffi.Float, ffi.Float, ffi.Float)>(isLeaf: true) @@ -1480,10 +1472,15 @@ external void set_rendering_render_thread( ); @ffi.Native< - ffi.Void Function(ffi.Pointer, + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, ffi.Pointer>)>(isLeaf: true) -external void request_frame_render_thread( +external void Viewer_requestFrameRenderThread( ffi.Pointer viewer, + ffi.Pointer view, + ffi.Pointer tSwapChain, ffi.Pointer> onComplete, ); @@ -1892,7 +1889,7 @@ external void View_setBloom( @ffi.Native< ffi.Void Function( - ffi.Pointer, ffi.Pointer, ffi.Int)>(isLeaf: true) + ffi.Pointer, ffi.Pointer, ffi.Int32)>(isLeaf: true) external void View_setToneMapping( ffi.Pointer tView, ffi.Pointer tEngine, @@ -1924,30 +1921,39 @@ external void View_setCamera( ffi.Pointer tCamera, ); +@ffi.Native Function(ffi.Pointer)>(isLeaf: true) +external ffi.Pointer View_getScene( + ffi.Pointer tView, +); + @ffi.Native Function(ffi.Pointer)>(isLeaf: true) external ffi.Pointer View_getCamera( ffi.Pointer tView, ); @ffi.Native< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Int, - ffi.Int, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - EntityId entityId, ffi.Int x, ffi.Int y)>>)>(isLeaf: true) + ffi.Pointer Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(isLeaf: true) +external ffi.Pointer Gizmo_new( + ffi.Pointer tEngine, + ffi.Pointer tView, + ffi.Pointer tScene, +); + +@ffi.Native< + ffi.Void Function(ffi.Pointer, ffi.Uint32, ffi.Uint32, + GizmoPickCallback)>(isLeaf: true) external void Gizmo_pick( ffi.Pointer tGizmo, - ffi.Pointer tView, int x, int y, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(EntityId entityId, ffi.Int x, ffi.Int y)>> - callback, + GizmoPickCallback callback, +); + +@ffi.Native, ffi.Bool)>(isLeaf: true) +external void Gizmo_setVisibility( + ffi.Pointer tGizmo, + bool visible, ); final class TCamera extends ffi.Opaque {} @@ -1970,6 +1976,8 @@ final class TView extends ffi.Opaque {} final class TGizmo extends ffi.Opaque {} +final class TScene extends ffi.Opaque {} + final class TMaterialKey extends ffi.Struct { @ffi.Bool() external bool doubleSided; @@ -2216,6 +2224,19 @@ final class TViewport extends ffi.Struct { external int height; } +abstract class ToneMapping { + static const int ACES = 0; + static const int FILMIC = 1; + static const int LINEAR = 2; +} + +typedef GizmoPickCallback + = ffi.Pointer>; +typedef GizmoPickCallbackFunction = ffi.Void Function( + EntityId entityId, ffi.Uint32 x, ffi.Uint32 y, ffi.Pointer view); +typedef DartGizmoPickCallbackFunction = void Function( + DartEntityId entityId, int x, int y, ffi.Pointer view); + const int __bool_true_false_are_defined = 1; const int true1 = 1; diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart index c0988b62..2a54072c 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart @@ -1,22 +1,20 @@ import 'dart:async'; -import 'dart:ffi'; import 'dart:math'; import 'dart:typed_data'; import 'package:animation_tools_dart/animation_tools_dart.dart'; -import 'package:thermion_dart/src/viewer/src/shared_types/swap_chain.dart'; +import 'package:thermion_dart/src/viewer/src/ffi/src/ffi_gizmo.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart' as v64; -import '../../../../entities/gizmo.dart'; +import '../../../../utils/gizmo.dart'; import '../../../../utils/matrix.dart'; import '../../events.dart'; -import '../../shared_types/camera.dart'; import '../../shared_types/view.dart'; import '../../thermion_viewer_base.dart'; import 'package:logging/logging.dart'; import 'callbacks.dart'; import 'camera_ffi.dart'; -import 'thermion_dart.g.dart'; +import 'ffi_view.dart'; // ignore: constant_identifier_names const ThermionEntity _FILAMENT_ASSET_ERROR = 0; @@ -26,8 +24,6 @@ typedef RenderCallback = Pointer)>>; class ThermionViewerFFI extends ThermionViewer { final _logger = Logger("ThermionViewerFFI"); - double pixelRatio = 1.0; - Pointer? _sceneManager; Pointer? _viewer; @@ -87,13 +83,9 @@ class ThermionViewerFFI extends ThermionViewer { this._driver = driver ?? nullptr; this._sharedContext = sharedContext ?? nullptr; - _onPickResultCallable = - NativeCallable.listener( - _onPickResult); - - _onGizmoPickResultCallable = - NativeCallable.listener( - _onGizmoPickResult); + // _onPickResultCallable = + // NativeCallable.listener( + // _onPickResult); _initialize(); } @@ -131,7 +123,6 @@ class ThermionViewerFFI extends ThermionViewer { /// /// Future updateViewportAndCameraProjection(double width, double height) async { - viewportDimensions = (width * pixelRatio, height * pixelRatio); var mainView = FFIView(Viewer_getViewAt(_viewer!, 0), _viewer!); mainView.updateViewport(width.toInt(), height.toInt()); @@ -148,7 +139,7 @@ class ThermionViewerFFI extends ThermionViewer { far = kFar; } - var aspect = viewportDimensions.$1 / viewportDimensions.$2; + var aspect = width / height; var focalLength = await camera.getFocalLength(); if (focalLength.abs() < 0.1) { focalLength = kFocalLength; @@ -161,8 +152,8 @@ class ThermionViewerFFI extends ThermionViewer { Future createSwapChain(int width, int height, {Pointer? surface}) async { var swapChain = await withPointerCallback((callback) { - return Viewer_createSwapChainRenderThread(_viewer!, surface ?? nullptr, - width, height, callback); + return Viewer_createSwapChainRenderThread( + _viewer!, surface ?? nullptr, width, height, callback); }); return FFISwapChain(swapChain, _viewer!); } @@ -193,11 +184,6 @@ class ThermionViewerFFI extends ThermionViewer { _sceneManager = Viewer_getSceneManager(_viewer!); - final gizmoEntities = allocator(4); - get_gizmo(_sceneManager!, gizmoEntities); - _gizmo = Gizmo(gizmoEntities[0], gizmoEntities[1], gizmoEntities[2], - gizmoEntities[3], this); - allocator.free(gizmoEntities); this._initialized.complete(true); } @@ -224,8 +210,9 @@ class ThermionViewerFFI extends ThermionViewer { /// /// @override - Future render(FFISwapChain swapChain) async { + Future render({FFISwapChain? swapChain}) async { final view = (await getViewAt(0)) as FFIView; + swapChain ??= FFISwapChain(Viewer_getSwapChainAt(_viewer!, 0), _viewer!); Viewer_renderRenderThread(_viewer!, view.view, swapChain.swapChain); } @@ -233,20 +220,24 @@ class ThermionViewerFFI extends ThermionViewer { /// /// @override - Future capture(FFISwapChain swapChain, - {FFIView? view, FFIRenderTarget? renderTarget}) async { - final length = this.viewportDimensions.$1.toInt() * - this.viewportDimensions.$2.toInt() * - 4; - final out = Uint8List(length); + Future capture( + {FFIView? view, + FFISwapChain? swapChain, + FFIRenderTarget? renderTarget}) async { view ??= (await getViewAt(0)) as FFIView; + final vp = await view.getViewport(); + final length = vp.width * vp.height * 4; + final out = Uint8List(length); + + swapChain ??= FFISwapChain(Viewer_getSwapChainAt(_viewer!, 0), _viewer!); + await withVoidCallback((cb) { if (renderTarget != null) { Viewer_captureRenderTargetRenderThread(_viewer!, view!.view, - swapChain.swapChain, renderTarget.renderTarget, out.address, cb); + swapChain!.swapChain, renderTarget.renderTarget, out.address, cb); } else { Viewer_captureRenderThread( - _viewer!, view!.view, swapChain.swapChain, out.address, cb); + _viewer!, view!.view, swapChain!.swapChain, out.address, cb); } }); return out; @@ -1341,20 +1332,6 @@ class ThermionViewerFFI extends ThermionViewer { set_camera_model_matrix(mainCamera.camera, out); } - /// - /// - /// - @override - Future setCameraLensProjection( - {double near = kNear, - double far = kFar, - double? aspect, - double focalLength = kFocalLength}) async { - aspect ??= viewportDimensions.$1 / viewportDimensions.$2; - var mainCamera = get_camera(_viewer!, get_main_camera(_viewer!)); - Camera_setLensProjection(mainCamera, near, far, aspect, focalLength); - } - /// /// /// @@ -1492,50 +1469,31 @@ class ThermionViewerFFI extends ThermionViewer { return result.cast().toDartString(); } - void _onPickResult(ThermionEntity entityId, int x, int y) { - _pickResultController.add(( - entity: entityId, - x: (x / pixelRatio).toDouble(), - y: (viewportDimensions.$2 - y) / pixelRatio - )); - } + void _onPickResult( + ThermionEntity entityId, int x, int y, Pointer viewPtr) async { + final view = FFIView(viewPtr, _viewer!); + final viewport = await view.getViewport(); - void _onGizmoPickResult(ThermionEntity entityId, int x, int y) { - _gizmoPickResultController.add(( - entity: entityId, - x: (x / pixelRatio).toDouble(), - y: (viewportDimensions.$2 - y) / pixelRatio - )); + // _pickResultController.add(( + // entity: entityId, + // x: (x / pixelRatio).toDouble(), + // y: (viewport.height - y) / pixelRatio + // )); } late NativeCallable _onPickResultCallable; - late NativeCallable - _onGizmoPickResultCallable; /// /// /// @override void pick(int x, int y) async { - x = (x * pixelRatio).ceil(); - y = (viewportDimensions.$2 - (y * pixelRatio)).ceil(); - final view = (await getViewAt(0)) as FFIView; - filament_pick( - _viewer!, view.view, x, y, _onPickResultCallable.nativeFunction); - } - - /// - /// - /// - @override - void pickGizmo(int x, int y) async { - x = (x * pixelRatio).ceil(); - y = (viewportDimensions.$2 - (y * pixelRatio)).ceil(); - final view = (await getViewAt(0)) as FFIView; - final gizmo = SceneManager_getGizmo(_sceneManager!); - Gizmo_pick( - gizmo, view.view, x, y, _onGizmoPickResultCallable.nativeFunction); + // x = (x * pixelRatio).ceil(); + // y = (viewportDimensions.$2 - (y * pixelRatio)).ceil(); + // final view = (await getViewAt(0)) as FFIView; + // filament_pick( + // _viewer!, view.view, x, y, _onPickResultCallable.nativeFunction); } /// @@ -1866,13 +1824,6 @@ class ThermionViewerFFI extends ThermionViewer { SceneManager_setVisibilityLayer(_sceneManager!, entity, layer); } - /// - /// - /// - Future setGizmoVisibility(bool visible) async { - set_gizmo_visibility(_sceneManager!, visible); - } - /// /// /// @@ -2088,7 +2039,11 @@ class ThermionViewerFFI extends ThermionViewer { completer.complete(true); }); - request_frame_render_thread(_viewer!, callback.nativeFunction); + final swapChain = Viewer_getSwapChainAt(_viewer!, 0); + final view = Viewer_getViewAt(_viewer!, 0); + + Viewer_requestFrameRenderThread( + _viewer!, view, swapChain, callback.nativeFunction); await completer.future; } @@ -2164,6 +2119,14 @@ class ThermionViewerFFI extends ThermionViewer { } return FFIView(view, _viewer!); } + + @override + Future createGizmo(FFIView view) async { + var view = (await getViewAt(0)) as FFIView; + var scene = View_getScene(view.view); + final gizmo = Gizmo_new(Viewer_getEngine(_viewer!), view.view, scene); + return FFIGizmo(gizmo, this); + } } class ThermionFFITexture extends ThermionTexture { @@ -2218,32 +2181,3 @@ class FFISwapChain extends SwapChain { }); } } - -class FFIView extends View { - final Pointer view; - final Pointer viewer; - - FFIView(this.view, this.viewer); - - @override - Future updateViewport(int width, int height) async { - View_updateViewport(view, width, height); - } - - @override - Future setRenderTarget(covariant FFIRenderTarget renderTarget) async { - View_setRenderTarget(view, renderTarget.renderTarget); - } - - @override - Future setCamera(FFICamera camera) async { - View_setCamera(view, camera.camera); - } - - @override - Future getViewport() async { - TViewport vp = View_getViewport(view); - return Viewport(vp.left, vp.bottom, vp.width, vp.height); - } - -} diff --git a/thermion_dart/lib/src/viewer/src/shared_types/pick_result.dart b/thermion_dart/lib/src/viewer/src/shared_types/pick_result.dart index 1c935e18..64a01290 100644 --- a/thermion_dart/lib/src/viewer/src/shared_types/pick_result.dart +++ b/thermion_dart/lib/src/viewer/src/shared_types/pick_result.dart @@ -1,5 +1,5 @@ // "picking" means clicking/tapping on the viewport, and unprojecting the X/Y coordinate to determine whether any renderable entities were present at those coordinates. import '../../viewer.dart'; -typedef FilamentPickResult = ({ThermionEntity entity, double x, double y}); -typedef ThermionPickResult = FilamentPickResult; +typedef FilamentPickResult = ({ThermionEntity entity, int x, int y}); +typedef PickResult = FilamentPickResult; diff --git a/thermion_dart/lib/src/viewer/src/shared_types/view.dart b/thermion_dart/lib/src/viewer/src/shared_types/view.dart index 7496ec94..c627446b 100644 --- a/thermion_dart/lib/src/viewer/src/shared_types/view.dart +++ b/thermion_dart/lib/src/viewer/src/shared_types/view.dart @@ -12,7 +12,7 @@ class Viewport { abstract class View { Future getViewport(); Future updateViewport(int width, int height); - Future setRenderTarget(covariant RenderTarget renderTarget); + Future setRenderTarget(covariant RenderTarget? renderTarget); Future setCamera(covariant Camera camera); - + Camera getCamera(); } diff --git a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart index 6a747dd6..ff252209 100644 --- a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart +++ b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart @@ -1,6 +1,5 @@ import 'package:thermion_dart/src/viewer/src/events.dart'; -import '../../entities/abstract_gizmo.dart'; -import 'shared_types/camera.dart'; +import '../../utils/gizmo.dart'; import 'shared_types/shared_types.dart'; export 'shared_types/shared_types.dart'; @@ -10,7 +9,6 @@ import 'package:vector_math/vector_math_64.dart'; import 'dart:async'; import 'package:animation_tools_dart/animation_tools_dart.dart'; -import 'shared_types/swap_chain.dart'; import 'shared_types/view.dart'; const double kNear = 0.05; @@ -23,16 +21,6 @@ abstract class ThermionViewer { /// Future get initialized; - /// - /// The current dimensions of the viewport (in physical pixels). - /// - var viewportDimensions = (0.0, 0.0); - - /// - /// The current ratio of logical to physical pixels. - /// - late double pixelRatio; - /// /// The result(s) of calling [pick] (see below). /// This may be a broadcast stream, so you should ensure you have subscribed to this stream before calling [pick]. @@ -40,11 +28,6 @@ abstract class ThermionViewer { /// Stream get pickResult; - /// - /// The result(s) of calling [pickGizmo] (see below). - /// - Stream get gizmoPickResult; - /// /// A Stream containing entities added/removed to/from to the scene. /// @@ -63,7 +46,7 @@ abstract class ThermionViewer { /// /// Render a single frame immediately. /// - Future render(covariant SwapChain swapChain); + Future render({covariant SwapChain? swapChain}); /// /// Requests a single frame to be rendered. This is only intended to be used internally. @@ -73,8 +56,8 @@ abstract class ThermionViewer { /// /// Render a single frame and copy the pixel buffer to [out]. /// - Future capture(covariant SwapChain swapChain, - {covariant View? view, covariant RenderTarget? renderTarget}); + Future capture({covariant SwapChain? swapChain, + covariant View? view, covariant RenderTarget? renderTarget}); /// /// @@ -96,7 +79,7 @@ abstract class ThermionViewer { /// /// Future createView(); - + /// /// /// @@ -400,7 +383,6 @@ abstract class ThermionViewer { /// Future clearEntities(); - /// /// Schedules the glTF animation at [index] in [entity] to start playing on the next frame. /// @@ -493,15 +475,6 @@ abstract class ThermionViewer { /// Future getCameraCullingFar(); - /// - /// - /// - Future setCameraLensProjection( - {double near = kNear, - double far = kFar, - double? aspect, - double focalLength = kFocalLength}); - /// /// Sets the focus distance for the camera. /// @@ -701,14 +674,6 @@ abstract class ThermionViewer { /// void pick(int x, int y); - /// - /// Used to test whether a Gizmo is at the given viewport coordinates. - /// Called by `FilamentGestureDetector` on a mouse/finger down event. You probably don't want to call this yourself. - /// This is asynchronous and will require 2-3 frames to complete - subscribe to the [gizmoPickResult] stream to receive the results of this method. - /// [x] and [y] must be in local logical coordinates (i.e. where 0,0 is at top-left of the ThermionWidget). - /// - void pickGizmo(int x, int y); - /// /// Retrieves the name assigned to the given ThermionEntity (usually corresponds to the glTF mesh name). /// @@ -793,9 +758,9 @@ abstract class ThermionViewer { Future setPriority(ThermionEntity entityId, int priority); /// - /// The gizmo for translating/rotating objects. Only one gizmo is present in the scene. + /// The gizmo for translating/rotating objects. Only one gizmo can be active for a given view. /// - AbstractGizmo? get gizmo; + Future createGizmo(covariant View view); /// /// Register a callback to be invoked when this viewer is disposed. @@ -819,11 +784,6 @@ abstract class ThermionViewer { /// Future setVisibilityLayer(ThermionEntity entity, int layer); - /// - /// Show/hide the translation gizmo. - /// - Future setGizmoVisibility(bool visible); - /// /// Renders an outline around [entity] with the given color. /// diff --git a/thermion_dart/lib/src/viewer/src/thermion_viewer_stub.dart b/thermion_dart/lib/src/viewer/src/thermion_viewer_stub.dart index 771379b4..73795553 100644 --- a/thermion_dart/lib/src/viewer/src/thermion_viewer_stub.dart +++ b/thermion_dart/lib/src/viewer/src/thermion_viewer_stub.dart @@ -1,14 +1,13 @@ import 'dart:math'; import 'dart:typed_data'; +import 'package:thermion_dart/src/utils/gizmo.dart'; import 'package:thermion_dart/src/viewer/src/shared_types/swap_chain.dart'; import 'package:thermion_dart/src/viewer/src/shared_types/view.dart'; import 'package:thermion_dart/thermion_dart.dart'; import 'package:vector_math/vector_math_64.dart'; import 'dart:async'; import 'package:animation_tools_dart/animation_tools_dart.dart'; - -import '../../entities/abstract_gizmo.dart'; import 'events.dart'; import 'shared_types/camera.dart'; @@ -242,10 +241,6 @@ class ThermionViewerStub extends ThermionViewer { throw UnimplementedError(); } - @override - // TODO: implement gizmo - AbstractGizmo? get gizmo => throw UnimplementedError(); - @override Future hide(ThermionEntity entity, String? meshName) { // TODO: implement hide @@ -1028,12 +1023,6 @@ class ThermionViewerStub extends ThermionViewer { // TODO: implement setRenderTarget throw UnimplementedError(); } - - @override - Future capture(covariant SwapChain swapChain, {covariant RenderTarget? renderTarget}) { - // TODO: implement capture - throw UnimplementedError(); - } @override Future createView() { @@ -1052,6 +1041,18 @@ class ThermionViewerStub extends ThermionViewer { // TODO: implement render throw UnimplementedError(); } + + @override + Future capture(covariant SwapChain swapChain, {covariant View? view, covariant RenderTarget? renderTarget}) { + // TODO: implement capture + throw UnimplementedError(); + } + + @override + Future createGizmo(covariant View view) { + // TODO: implement createGizmo + throw UnimplementedError(); + } } diff --git a/thermion_dart/lib/src/viewer/src/web_js/src/thermion_viewer_js.dart b/thermion_dart/lib/src/viewer/src/web_js/src/thermion_viewer_js.dart index c066711a..68295449 100644 --- a/thermion_dart/lib/src/viewer/src/web_js/src/thermion_viewer_js.dart +++ b/thermion_dart/lib/src/viewer/src/web_js/src/thermion_viewer_js.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:animation_tools_dart/animation_tools_dart.dart'; import 'package:logging/logging.dart'; import 'package:vector_math/vector_math_64.dart'; -import '../../../../entities/abstract_gizmo.dart'; +import '../../shared_types/internal/gizmo.dart'; import '../../../viewer.dart'; import '../../events.dart'; import '../../shared_types/camera.dart'; diff --git a/thermion_dart/lib/src/viewer/src/web_wasm/src/camera.dart b/thermion_dart/lib/src/viewer/src/web_wasm/src/camera.dart index 4296f7e5..1b158caa 100644 --- a/thermion_dart/lib/src/viewer/src/web_wasm/src/camera.dart +++ b/thermion_dart/lib/src/viewer/src/web_wasm/src/camera.dart @@ -45,4 +45,28 @@ class ThermionWasmCamera extends Camera { // TODO: implement setModelMatrix throw UnimplementedError(); } + + @override + Future getCullingFar() { + // TODO: implement getCullingFar + throw UnimplementedError(); + } + + @override + Future getFocalLength() { + // TODO: implement getFocalLength + throw UnimplementedError(); + } + + @override + Future getNear() { + // TODO: implement getNear + throw UnimplementedError(); + } + + @override + Future getViewMatrix() { + // TODO: implement getViewMatrix + throw UnimplementedError(); + } } diff --git a/thermion_dart/lib/src/viewer/src/web_wasm/src/thermion_viewer_wasm.dart b/thermion_dart/lib/src/viewer/src/web_wasm/src/thermion_viewer_wasm.dart index 3a524515..88814f29 100644 --- a/thermion_dart/lib/src/viewer/src/web_wasm/src/thermion_viewer_wasm.dart +++ b/thermion_dart/lib/src/viewer/src/web_wasm/src/thermion_viewer_wasm.dart @@ -11,8 +11,8 @@ import 'package:animation_tools_dart/animation_tools_dart.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; -import '../../../../entities/abstract_gizmo.dart'; -import '../../../../entities/gizmo.dart'; +import '../../shared_types/internal/gizmo.dart'; +import '../../shared_types/internal/gizmo.dart'; import '../../../viewer.dart'; import '../../events.dart'; import '../../shared_types/camera.dart'; diff --git a/thermion_dart/native/include/FilamentViewer.hpp b/thermion_dart/native/include/FilamentViewer.hpp index d998bffd..80647b51 100644 --- a/thermion_dart/native/include/FilamentViewer.hpp +++ b/thermion_dart/native/include/FilamentViewer.hpp @@ -132,6 +132,14 @@ namespace thermion return (SceneManager *const)_sceneManager; } + SwapChain* getSwapChainAt(int index) { + if(index < _swapChains.size()) { + return _swapChains[index]; + } + Log("Error: index %d is greater than available swapchains", index); + return nullptr; + } + void unprojectTexture(EntityId entity, uint8_t* input, uint32_t inputWidth, uint32_t inputHeight, uint8_t* out, uint32_t outWidth, uint32_t outHeight); private: diff --git a/thermion_dart/native/include/ThermionDartApi.h b/thermion_dart/native/include/ThermionDartApi.h index 45296c15..a0eb5869 100644 --- a/thermion_dart/native/include/ThermionDartApi.h +++ b/thermion_dart/native/include/ThermionDartApi.h @@ -85,6 +85,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE TView* Viewer_createView(TViewer *viewer); EMSCRIPTEN_KEEPALIVE TView* Viewer_getViewAt(TViewer *viewer, int index); EMSCRIPTEN_KEEPALIVE void Viewer_setMainCamera(TViewer *tViewer, TView *tView); + EMSCRIPTEN_KEEPALIVE TSwapChain* Viewer_getSwapChainAt(TViewer *tViewer, int index); // Engine EMSCRIPTEN_KEEPALIVE TEngine *Viewer_getEngine(TViewer* viewer); diff --git a/thermion_dart/native/include/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/ThermionDartRenderThreadApi.h index 6794ef41..5f2c15b5 100644 --- a/thermion_dart/native/include/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/ThermionDartRenderThreadApi.h @@ -35,7 +35,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE FilamentRenderCallback make_render_callback_fn_pointer(FilamentRenderCallback); EMSCRIPTEN_KEEPALIVE void set_rendering_render_thread(TViewer *viewer, bool rendering, void(*onComplete)()); - EMSCRIPTEN_KEEPALIVE void request_frame_render_thread(TViewer *viewer, void(*onComplete)()); + EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, TView *view, TSwapChain *tSwapChain, void(*onComplete)()); EMSCRIPTEN_KEEPALIVE void set_frame_interval_render_thread(TViewer *viewer, float frameInterval); EMSCRIPTEN_KEEPALIVE void set_background_color_render_thread(TViewer *viewer, const float r, const float g, const float b, const float a); EMSCRIPTEN_KEEPALIVE void clear_background_image_render_thread(TViewer *viewer); diff --git a/thermion_dart/native/include/generated/ThermionTextureSwiftObjCAPI.h b/thermion_dart/native/include/generated/ThermionTextureSwiftObjCAPI.h new file mode 100644 index 00000000..d42397bb --- /dev/null +++ b/thermion_dart/native/include/generated/ThermionTextureSwiftObjCAPI.h @@ -0,0 +1,325 @@ +// Generated by Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) +#ifndef SWIFT_MODULE_SWIFT_H +#define SWIFT_MODULE_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +@import CoreVideo; +@import ObjectiveC; +#endif + +#endif +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="swift_module",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) +@protocol MTLDevice; +@protocol MTLTexture; +@class NSData; + +SWIFT_CLASS("_TtC12swift_module20ThermionTextureSwift") +@interface ThermionTextureSwift : NSObject +@property (nonatomic) CVMetalTextureCacheRef _Nullable cvMetalTextureCache; +@property (nonatomic, strong) id _Nullable metalDevice; +@property (nonatomic) CVMetalTextureRef _Nullable cvMetalTexture; +@property (nonatomic, strong) id _Nullable metalTexture; +@property (nonatomic) NSInteger metalTextureAddress; +- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER; +- (nonnull instancetype)initWithWidth:(int64_t)width height:(int64_t)height OBJC_DESIGNATED_INITIALIZER; +- (void)destroyTexture; +- (void)fillColor; +- (NSData * _Nullable)getTextureBytes SWIFT_WARN_UNUSED_RESULT; +@end + +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif diff --git a/thermion_dart/native/src/TView.cpp b/thermion_dart/native/src/TView.cpp index acd4f116..cf8ec654 100644 --- a/thermion_dart/native/src/TView.cpp +++ b/thermion_dart/native/src/TView.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "ThermionDartApi.h" #include "TView.h" @@ -150,6 +151,11 @@ using namespace filament; return reinterpret_cast(view->getScene()); } + EMSCRIPTEN_KEEPALIVE TCamera* View_getCamera(TView *tView) { + auto view = reinterpret_cast(tView); + return reinterpret_cast(&(view->getCamera())); + } + #ifdef __cplusplus } diff --git a/thermion_dart/native/src/ThermionDartApi.cpp b/thermion_dart/native/src/ThermionDartApi.cpp index 5ab4f09a..c51d7e3b 100644 --- a/thermion_dart/native/src/ThermionDartApi.cpp +++ b/thermion_dart/native/src/ThermionDartApi.cpp @@ -348,8 +348,13 @@ extern "C" void (*callback)(void *buf, size_t size, void *data), void *data) { - auto swapChain = reinterpret_cast(tSwapChain); auto viewer = reinterpret_cast(tViewer); + auto swapChain = reinterpret_cast(tSwapChain); + + if(!swapChain) { + swapChain = viewer->getSwapChainAt(0); + } + auto *view = reinterpret_cast(tView); return viewer->render(frameTimeInNanos, view, swapChain, pixelBuffer, callback, data); } @@ -413,6 +418,12 @@ extern "C" return reinterpret_cast(swapChain); } + EMSCRIPTEN_KEEPALIVE TSwapChain* Viewer_getSwapChainAt(TViewer *tViewer, int index) { + auto viewer = reinterpret_cast(tViewer); + auto swapChain = viewer->getSwapChainAt(index); + return reinterpret_cast(swapChain); + } + EMSCRIPTEN_KEEPALIVE TView *Viewer_createView(TViewer *tViewer) { auto viewer = reinterpret_cast(tViewer); diff --git a/thermion_dart/test/camera_tests.dart b/thermion_dart/test/camera_tests.dart index 42538537..c93e4fde 100644 --- a/thermion_dart/test/camera_tests.dart +++ b/thermion_dart/test/camera_tests.dart @@ -67,7 +67,9 @@ void main() async { print(frustum.plane5.normal); print(frustum.plane5.constant); - await viewer.setCameraLensProjection( + var camera = await viewer.getMainCamera(); + + await camera.setLensProjection( near: 10.0, far: 1000.0, aspect: 1.0, focalLength: 28.0); frustum = await viewer.getCameraFrustum(); print(frustum.plane5.normal); @@ -75,8 +77,8 @@ void main() async { }); test('set custom projection/culling matrix', () async { - var viewer = - await testHelper.createViewer(bg: kRed, cameraPosition: Vector3(0, 0, 4)); + var viewer = await testHelper.createViewer( + bg: kRed, cameraPosition: Vector3(0, 0, 4)); var camera = await viewer.getMainCamera(); final cube = await viewer.createGeometry(GeometryHelper.cube()); @@ -155,7 +157,7 @@ void main() async { expect(await viewer.getActiveCamera(), newCamera); await testHelper.capture(viewer, "create_camera_new_camera"); - + final mainCamera = await viewer.getMainCamera(); await viewer.setActiveCamera(mainCamera); expect(await viewer.getActiveCamera(), mainCamera); diff --git a/thermion_dart/test/gizmo_tests.dart b/thermion_dart/test/gizmo_tests.dart new file mode 100644 index 00000000..ad3ba1a3 --- /dev/null +++ b/thermion_dart/test/gizmo_tests.dart @@ -0,0 +1,17 @@ +import 'package:test/test.dart'; + +import 'helpers.dart'; + +void main() async { + final testHelper = TestHelper("gizmo"); + + group('gizmo', () { + test('add gizmo to scene', () async { + var viewer = await testHelper.createViewer(); + var view = await viewer.getViewAt(0); + var gizmo = await viewer.createGizmo(view); + await testHelper.capture(viewer, "gizmo_add_to_scene"); + await viewer.dispose(); + }); + }); +} diff --git a/thermion_dart/test/helpers.dart b/thermion_dart/test/helpers.dart index ef92397a..c1661106 100644 --- a/thermion_dart/test/helpers.dart +++ b/thermion_dart/test/helpers.dart @@ -77,6 +77,8 @@ class TestHelper { outDir = Directory("$testDir/output/${dir}"); // outDir.deleteSync(recursive: true); outDir.createSync(); + DynamicLibrary.open('${testDir}/libThermionTextureSwift.dylib'); + } Future capture(ThermionViewer viewer, String outputFilename, @@ -84,33 +86,28 @@ class TestHelper { await Future.delayed(Duration(milliseconds: 10)); var outPath = p.join(outDir.path, "$outputFilename.bmp"); var pixelBuffer = await viewer.capture( - view: view, swapChain ?? this.swapChain, renderTarget: renderTarget); + view: view, + swapChain: swapChain ?? this.swapChain, + renderTarget: renderTarget); view ??= await viewer.getViewAt(0); var vp = await view.getViewport(); - await savePixelBufferToBmp( - pixelBuffer, - vp.width, - vp.height, - outPath); + await savePixelBufferToBmp(pixelBuffer, vp.width, vp.height, outPath); return pixelBuffer; } - Future createTexture(int width, int height) async { + Future createTexture(int width, int height) async { final packageUri = findPackageRoot('thermion_dart'); - final lib = ThermionDartTexture1(DynamicLibrary.open( - '${packageUri.toFilePath()}/native/lib/macos/swift/libthermion_swift.dylib')); - final object = ThermionDartTexture.new1(lib); + var testDir = Directory("${packageUri.toFilePath()}/test").path; + + final object = ThermionTextureSwift.new1(); object.initWithWidth_height_(width, height); - return object.metalTextureAddress; + return object; } Future createViewer( {img.Color? bg, Vector3? cameraPosition, viewportDimensions = (width: 500, height: 500)}) async { - final texture = await createTexture( - viewportDimensions.width, viewportDimensions.height); - final resourceLoader = calloc(1); var loadToOut = NativeCallable< Void Function(Pointer, diff --git a/thermion_dart/test/integration_test.dart b/thermion_dart/test/integration_test.dart index d00292cc..c5cb71ec 100644 --- a/thermion_dart/test/integration_test.dart +++ b/thermion_dart/test/integration_test.dart @@ -647,16 +647,6 @@ void main() async { }); }); - group("render thread", () { - test("request frame on render thread", () async { - var viewer = await testHelper.createViewer(); - viewer.requestFrame(); - - await Future.delayed(Duration(milliseconds: 20)); - await viewer.dispose(); - }); - }); - // group("unproject", () { // test("unproject", () async { // final dimensions = (width: 1280, height: 768); diff --git a/thermion_dart/test/libThermionTextureSwift.dylib b/thermion_dart/test/libThermionTextureSwift.dylib new file mode 100644 index 0000000000000000000000000000000000000000..5a91f7886819e72b51acec4e0f2e32acc78a43d1 GIT binary patch literal 110976 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 literal 0 HcmV?d00001 diff --git a/thermion_dart/test/render_thread.dart b/thermion_dart/test/render_thread.dart new file mode 100644 index 00000000..ea8579be --- /dev/null +++ b/thermion_dart/test/render_thread.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'package:test/test.dart'; + +import 'helpers.dart'; + +void main() async { + final testHelper = TestHelper("render_thread"); + group("render thread/capture", () { + test("request frame on render thread", () async { + var viewer = await testHelper.createViewer(); + await viewer.setBackgroundColor(1.0, 0.0, 0.0, 1.0); + + var texture = await testHelper.createTexture(500, 500); + var renderTarget = await viewer.createRenderTarget( + 500, 500, texture.metalTextureAddress); + + final view = await viewer.getViewAt(0); + await view.setRenderTarget(renderTarget); + + await viewer.render(); + + await Future.delayed(Duration(milliseconds: 1)); + + var data = texture.getTextureBytes()!; + var pixels = data.bytes.cast().asTypedList(data.length); + + savePixelBufferToBmp( + pixels, 500, 500, "${testHelper.testDir}/request_frame.bmp"); + await viewer.dispose(); + }); + }); +} diff --git a/thermion_dart/test/view_tests.dart b/thermion_dart/test/view_tests.dart index 40019693..794d9478 100644 --- a/thermion_dart/test/view_tests.dart +++ b/thermion_dart/test/view_tests.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:test/test.dart'; import 'package:thermion_dart/thermion_dart.dart'; import 'package:vector_math/vector_math_64.dart'; @@ -9,11 +7,17 @@ void main() async { final testHelper = TestHelper("view"); group('view tests', () { + test('get camera from view', () async { + var viewer = await testHelper.createViewer(); + var view = await viewer.getViewAt(0); + expect(await view.getCamera(), isNotNull); + }); + test('one swapchain, render view to render target', () async { var viewer = await testHelper.createViewer(); final texture = await testHelper.createTexture(500, 500); - final renderTarget = await viewer.createRenderTarget(500, 500, texture); + final renderTarget = await viewer.createRenderTarget(500, 500, texture.metalTextureAddress); viewer.setRenderTarget(renderTarget); await viewer.setBackgroundColor(1.0, 0, 0, 1); @@ -27,9 +31,10 @@ void main() async { "default_swapchain_default_view_render_target"); }); - test('create secondary view, same swapchain', () async { + test('create secondary view, default swapchain', () async { var viewer = await testHelper.createViewer(); await viewer.setBackgroundColor(1.0, 0, 0, 1); + final cube = await viewer.createGeometry(GeometryHelper.cube()); var mainCamera = await viewer.getMainCamera(); @@ -71,48 +76,28 @@ void main() async { var mainCamera = await viewer.getMainCamera(); mainCamera.setTransform(Matrix4.translation(Vector3(0, 0, 5))); - final swapChain = await viewer.createSwapChain(200, 400); + final swapChain = await viewer.createSwapChain(1, 1); await testHelper.capture( viewer, "create_swapchain_default_view_default_swapchain"); final view = await viewer.createView(); - final texture = await testHelper.createTexture(400, 400); - final renderTarget = await viewer.createRenderTarget(400, 400, texture); + final texture = await testHelper.createTexture(200, 400); + final renderTarget = await viewer.createRenderTarget(200, 400, texture.metalTextureAddress); await view.setRenderTarget(renderTarget); - await view.updateViewport(400, 400); + await view.updateViewport(200, 400); view.setCamera(mainCamera); - + mainCamera.setLensProjection(aspect: 0.5); + await testHelper.capture( viewer, view: view, swapChain: swapChain, renderTarget: renderTarget, - "create_swapchain_new_view_new_swapchain", + "create_swapchain_secondary_view_new_swapchain", ); - // var newCamera = await viewer.createCamera(); - // newCamera.setTransform(Matrix4.translation(Vector3(0.0, 0.0, 10.0))); - // newCamera.setLensProjection(); - // view.setCamera(newCamera); - - // await testHelper.capture( - // viewer, - // "created_view_with_new_camera", - // view: view, - // ); - - // await testHelper.capture( - // viewer, - // "default_view_main_camera_no_change", - // ); - - // // await view.updateViewport(200, 400); - // // await view.setRenderTarget(renderTarget); - // // await viewer.setBackgroundColor(1.0, 0.0, 0.0, 1.0); - // // await testHelper.capture(viewer, "create_view_with_render_target", - // // renderTarget: renderTarget); await viewer.dispose(); }); }); diff --git a/thermion_dart/test/viewport_gizmo.dart b/thermion_dart/test/viewport_gizmo.dart index 52f90e2d..99a7d90e 100644 --- a/thermion_dart/test/viewport_gizmo.dart +++ b/thermion_dart/test/viewport_gizmo.dart @@ -47,9 +47,9 @@ late String testDir; void main() async { final packageUri = findPackageRoot('thermion_dart'); testDir = Directory("${packageUri.toFilePath()}/test").path; - final lib = ThermionDartTexture1(DynamicLibrary.open( + final lib = ThermionTexture1(DynamicLibrary.open( '${packageUri.toFilePath()}/native/lib/macos/swift/libthermion_swift.dylib')); - final object = ThermionDartTexture.new1(lib); + final object = ThermionTexture.new1(lib); object.initWithWidth_height_(500, 500); final resourceLoader = calloc(1); diff --git a/thermion_dart/test/viewport_grid.dart b/thermion_dart/test/viewport_grid.dart index fb727013..89463ff0 100644 --- a/thermion_dart/test/viewport_grid.dart +++ b/thermion_dart/test/viewport_grid.dart @@ -43,9 +43,9 @@ late String testDir; void main() async { final packageUri = findPackageRoot('thermion_dart'); testDir = Directory("${packageUri.toFilePath()}/test").path; - final lib = ThermionDartTexture1(DynamicLibrary.open( + final lib = ThermionTexture1(DynamicLibrary.open( '${packageUri.toFilePath()}/native/lib/macos/swift/libthermion_swift.dylib')); - final object = ThermionDartTexture.new1(lib); + final object = ThermionTexture.new1(lib); object.initWithWidth_height_(500, 500); final resourceLoader = calloc(1); diff --git a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart index b37a158b..7a1ba8d6 100644 --- a/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart +++ b/thermion_flutter/thermion_flutter/lib/src/thermion_flutter_plugin.dart @@ -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 createViewer({String? uberArchivePath}) async { + static Future 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 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 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 resizeTexture( - ThermionFlutterTexture texture, - int width, - int height, - int offsetLeft, - int offsetTop, - double pixelRatio) async { - return ThermionFlutterPlatform.instance.resizeTexture( - texture, width, height, offsetLeft, offsetTop, pixelRatio); - } } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/pixel_ratio_aware.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/pixel_ratio_aware.dart new file mode 100644 index 00000000..1d92e832 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/pixel_ratio_aware.dart @@ -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); + } +} \ No newline at end of file diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_listener_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_listener_widget.dart index d0a50817..8cfd2f68 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_listener_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_listener_widget.dart @@ -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 { 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); diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_texture_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_texture_widget.dart new file mode 100644 index 00000000..04909ec3 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_texture_widget.dart @@ -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 createState() { + return _ThermionTextureWidgetState(); + } +} + +class _ThermionTextureWidgetState extends State { + 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 { + +// 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) +// ])); +// } +// } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart index 6e909c7d..5c5c27e1 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart @@ -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 createState() => _ThermionWidgetState(); } class _ThermionWidgetState extends State { - 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!); } } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_impl.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_impl.dart index 653d774a..29ea1013 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_impl.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_impl.dart @@ -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(); } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_stub.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_stub.dart index 945719c7..6c1c109d 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_stub.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_web_stub.dart @@ -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 createState() => throw Exception(); + Widget build(BuildContext context) { + throw Exception("STUB"); + } } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_windows.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_windows.dart new file mode 100644 index 00000000..c5040b15 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_windows.dart @@ -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(); + } +} diff --git a/thermion_flutter/thermion_flutter/macos/Classes/SwiftThermionFlutterPlugin.swift b/thermion_flutter/thermion_flutter/macos/Classes/SwiftThermionFlutterPlugin.swift index ba07fcc0..c66f124d 100644 --- a/thermion_flutter/thermion_flutter/macos/Classes/SwiftThermionFlutterPlugin.swift +++ b/thermion_flutter/thermion_flutter/macos/Classes/SwiftThermionFlutterPlugin.swift @@ -55,14 +55,7 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin { let instance:SwiftThermionFlutterPlugin = Unmanaged.fromOpaque(resourcesPtr!).takeUnretainedValue() instance.resources.removeValue(forKey:UInt32(rbuf.id)) } - - var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in - let instance:SwiftThermionFlutterPlugin = Unmanaged.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": diff --git a/thermion_flutter/thermion_flutter/macos/Classes/ThermionFlutterTexture.swift b/thermion_flutter/thermion_flutter/macos/Classes/ThermionFlutterTexture.swift index fc8669ae..7fd94a43 100644 --- a/thermion_flutter/thermion_flutter/macos/Classes/ThermionFlutterTexture.swift +++ b/thermion_flutter/thermion_flutter/macos/Classes/ThermionFlutterTexture.swift @@ -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) } diff --git a/thermion_flutter/thermion_flutter/test/cpp/test.cpp b/thermion_flutter/thermion_flutter/test/cpp/test.cpp index b046dff0..fa9f0d6e 100644 --- a/thermion_flutter/thermion_flutter/test/cpp/test.cpp +++ b/thermion_flutter/thermion_flutter/test/cpp/test.cpp @@ -14,7 +14,7 @@ #include "ResourceBuffer.hpp" using namespace filament; -using namespace thermion_filament; +using namespace thermion; using namespace std; int _i = 0; diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_android.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_android.dart new file mode 100644 index 00000000..0f6f43b4 --- /dev/null +++ b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_android.dart @@ -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 = {}; + + 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 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.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"); + } +} diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ffi.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ffi.dart index 232789b4..98d22962 100644 --- a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ffi.dart +++ b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ffi.dart @@ -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 = {}; - - Future createViewerWithOptions( - ThermionFlutterOptions options) async { - return createViewer(uberarchivePath: options.uberarchivePath); - } - - Future createViewer({String? uberarchivePath}) async { - var resourceLoader = Pointer.fromAddress( - await _channel.invokeMethod("getResourceLoaderWrapper")); - - if (resourceLoader == nullptr) { - throw Exception("Failed to get resource loader"); - } - - var renderCallbackResult = await _channel.invokeMethod("getRenderCallback"); - var renderCallback = - Pointer)>>.fromAddress( - renderCallbackResult[0]); - var renderCallbackOwner = - Pointer.fromAddress(renderCallbackResult[1]); - - var driverPlatform = await _channel.invokeMethod("getDriverPlatform"); - var driverPtr = driverPlatform == null - ? nullptr - : Pointer.fromAddress(driverPlatform); - - var sharedContext = await _channel.invokeMethod("getSharedContext"); - - var sharedContextPtr = sharedContext == null - ? nullptr - : Pointer.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 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.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 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.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'; diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ios.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_ios.dart new file mode 100644 index 00000000..acacd036 --- /dev/null +++ b/thermion_flutter/thermion_flutter_ffi/lib/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 = {}; + + 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 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.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"); + } +} diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_macos.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_macos.dart new file mode 100644 index 00000000..50820605 --- /dev/null +++ b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_macos.dart @@ -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 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 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(); + } + } +} diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_method_channel_interface.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_method_channel_interface.dart new file mode 100644 index 00000000..1c8249c9 --- /dev/null +++ b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_method_channel_interface.dart @@ -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 createViewer({ThermionFlutterOptions? options}) async { + if (viewer != null) { + throw Exception( + "Only one viewer can be created over the lifetime of an application"); + } + + var resourceLoader = Pointer.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.fromAddress(driverPlatform); + + var sharedContext = await _channel.invokeMethod("getSharedContext"); + + var sharedContextPtr = sharedContext == null + ? nullptr + : Pointer.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); + } +} diff --git a/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_windows.dart b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_windows.dart new file mode 100644 index 00000000..dd41948c --- /dev/null +++ b/thermion_flutter/thermion_flutter_ffi/lib/thermion_flutter_windows.dart @@ -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 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.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; + } +} diff --git a/thermion_flutter/thermion_flutter_ffi/pubspec.yaml b/thermion_flutter/thermion_flutter_ffi/pubspec.yaml index 1fa1f154..6d45d758 100644 --- a/thermion_flutter/thermion_flutter_ffi/pubspec.yaml +++ b/thermion_flutter/thermion_flutter_ffi/pubspec.yaml @@ -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 diff --git a/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_platform_interface.dart b/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_platform_interface.dart index 2a861b84..70620993 100644 --- a/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_platform_interface.dart +++ b/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_platform_interface.dart @@ -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 createViewerWithOptions( - covariant ThermionFlutterOptions options); + /// + /// + /// + Future createViewer( + {covariant ThermionFlutterOptions? options}); - @deprecated - Future 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 createTexture(int width, int height); - Future createTexture(double width, double height, - double offsetLeft, double offsetTop, double pixelRatio); - - Future destroyTexture(ThermionFlutterTexture texture); - - Future resizeTexture(ThermionFlutterTexture texture, - int width, int height, int offsetTop, int offsetRight, double pixelRatio); + /// + /// + /// + Future resizeWindow( + int width, int height, int offsetTop, int offsetRight); } diff --git a/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_texture.dart b/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_texture.dart index c71723ee..25b62394 100644 --- a/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_texture.dart +++ b/thermion_flutter/thermion_flutter_platform_interface/lib/thermion_flutter_texture.dart @@ -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(); }