merge
This commit is contained in:
@@ -1,3 +1,117 @@
|
||||
## 0.2.1-dev.7
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.6
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.5
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.4
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0.0.0
|
||||
|
||||
- y
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
|
||||
|
||||
## 0.2.0-dev.8.0.0
|
||||
|
||||
- **REFACTOR**: continual refactor to support multiple render targets.
|
||||
- **FEAT**: camera and resizing improvements.
|
||||
- **FEAT**: support multiple ThermionWidget on Android.
|
||||
- **FEAT**: use imported texture on iOS.
|
||||
- **FEAT**: working implementation of multiple widgets on macos.
|
||||
- **FEAT**: add rendering check to ThermionWidget ticker.
|
||||
|
||||
## 0.2.0-dev.7.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.6.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.6.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **CHORE**: remove superseded HardwareKeyboard* classes.
|
||||
|
||||
## 0.2.0-dev.5.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.4.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.3.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **FIX**: remove EntityControllerMouseWidget (replace with GestureHandler).
|
||||
- **BREAKING** **CHORE**: (flutter) cleanup for pub.dev publishing.
|
||||
|
||||
## 0.2.0-dev.2.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **CHORE**: remove EntityListWidget - will replace with new Scene.
|
||||
|
||||
## 0.2.0-dev.1.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FIX**: (flutter) pass ThermionFlutterOptions to ThermionWidget, use dpr for resizeTexture, delete unnecessary TransparencyPainter class.
|
||||
- **FIX**: (flutter/web) use window.devicePixelRatio for viewport.
|
||||
- **FIX**: (flutter) desktop gesture detector changes for new Gizmo methods.
|
||||
- **FEAT**: allow passing null options to ThermionWidget.
|
||||
- **FEAT**: (flutter) (web) if importCanvasAsWidget is false, render transparency.
|
||||
- **FEAT**: add createViewerWithOptions to ThermionFlutterPlugin and mark createViewer as deprecated.
|
||||
- **FEAT**: add createViewerWithOptions to ThermionFlutterPlugin and mark createViewer as deprecated.
|
||||
- **FEAT**: highlight gizmo on hover.
|
||||
- **BREAKING** **FIX**: (flutter) pass pixelRatio to createTexture.
|
||||
- **BREAKING** **FIX**: (flutter) pass pixelRatio to createTexture.
|
||||
- **BREAKING** **FEAT**: (web) (flutter) create canvas when createViewer is called (no longer need to manually add canvas element to web HTML).
|
||||
- **BREAKING** **FEAT**: resize canvas on web.
|
||||
- **BREAKING** **CHORE**: rename controller to viewer in gesture detector widgets.
|
||||
|
||||
## 0.1.1+13
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.1+12
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.1+11
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.1+10
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -15,3 +15,49 @@
|
||||
<a href="https://discord.gg/h2VdDK3EAQ"><img src="https://img.shields.io/discord/993167615587520602?logo=discord&logoColor=fff&labelColor=333940" alt="discord"></a>
|
||||
<a href="https://github.com/nmfisher/thermion"><img src="https://img.shields.io/github/contributors/nmfisher/flutter_filament?logo=github&labelColor=333940" alt="contributors"></a>
|
||||
|
||||
### Quickstart (Flutter)
|
||||
|
||||
```
|
||||
_thermionViewer = await ThermionFlutterPlugin.createViewer();
|
||||
|
||||
// Geometry and models are represented as "entities". Here, we load a glTF
|
||||
// file containing a plain cube.
|
||||
// By default, all paths are treated as asset paths. To load from a file
|
||||
// instead, use file:// URIs.
|
||||
var entity =
|
||||
await _thermionViewer!.loadGlb("assets/cube.glb", keepData: true);
|
||||
|
||||
// Thermion uses a right-handed coordinate system where +Y is up and -Z is
|
||||
// "into" the screen.
|
||||
// By default, the camera is located at (0,0,0) looking at (0,0,-1); this
|
||||
// would place it directly inside the cube we just loaded.
|
||||
//
|
||||
// Let's move the camera to (0,0,10) to ensure the cube is visible in the
|
||||
// viewport.
|
||||
await _thermionViewer!.setCameraPosition(0, 0, 10);
|
||||
|
||||
// Without a light source, your scene will be totally black. Let's load a skybox
|
||||
// (a cubemap image that is rendered behind everything else in the scene)
|
||||
// and an image-based indirect light that has been precomputed from the same
|
||||
// skybox.
|
||||
await _thermionViewer!.loadSkybox("assets/default_env_skybox.ktx");
|
||||
await _thermionViewer!.loadIbl("assets/default_env_ibl.ktx");
|
||||
|
||||
// Finally, you need to explicitly enable rendering. Setting rendering to
|
||||
// false is designed to allow you to pause rendering to conserve battery life
|
||||
await _thermionViewer!.setRendering(true);
|
||||
```
|
||||
|
||||
and then in your Flutter application:
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
if (_thermionViewer != null)
|
||||
Positioned.fill(
|
||||
child: ThermionWidget(
|
||||
viewer: _thermionViewer!,
|
||||
)),
|
||||
]);
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,7 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
errors:
|
||||
constant_identifier_names: ignore
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
group 'app.polyvox.filament'
|
||||
group 'dev.thermion.android'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
@@ -25,6 +25,7 @@ apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'dev.thermion.android'
|
||||
compileSdkVersion 33
|
||||
ndkVersion '25.2.9519653'
|
||||
compileOptions {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.polyvox.filament">
|
||||
package="dev.thermion.android">
|
||||
</manifest>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.polyvox.filament
|
||||
package dev.thermion.android
|
||||
|
||||
import com.sun.jna.ptr.PointerByReference
|
||||
import com.sun.jna.ptr.IntByReference
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.polyvox.filament
|
||||
package dev.thermion.android
|
||||
|
||||
|
||||
import HotReloadPathHelper
|
||||
@@ -26,6 +26,7 @@ import io.flutter.view.TextureRegistry.SurfaceTextureEntry
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LoadFilamentResourceFromOwnerImpl(plugin:ThermionFlutterPlugin) : LoadFilamentResourceFromOwner {
|
||||
var plugin = plugin
|
||||
override fun loadResourceFromOwner(path: String?, owner: Pointer?): ResourceBuffer {
|
||||
@@ -66,10 +67,18 @@ class ThermionFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
||||
private var lifecycle: Lifecycle? = null
|
||||
|
||||
private lateinit var _lib : FilamentInterop
|
||||
|
||||
private data class TextureEntry(
|
||||
val surfaceTextureEntry: SurfaceTextureEntry,
|
||||
val surfaceTexture: SurfaceTexture,
|
||||
val surface: Surface
|
||||
)
|
||||
|
||||
var _surfaceTexture: SurfaceTexture? = null
|
||||
private var _surfaceTextureEntry: SurfaceTextureEntry? = null
|
||||
var _surface: Surface? = null
|
||||
private val textures: MutableMap<Long, TextureEntry> = mutableMapOf()
|
||||
|
||||
|
||||
private lateinit var activity:Activity
|
||||
|
||||
@@ -153,35 +162,54 @@ class ThermionFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
Log.e("thermion_flutter", call.method, null)
|
||||
when (call.method) {
|
||||
"createTexture" -> {
|
||||
if(_surfaceTextureEntry != null) {
|
||||
result.error("TEXTURE_EXISTS", "Texture already exist. Make sure you call destroyTexture first", null)
|
||||
return
|
||||
}
|
||||
val args = call.arguments as List<*>
|
||||
val width = args[0] as Int
|
||||
val height = args[1] as Int
|
||||
if(width <1 || height < 1) {
|
||||
result.error("DIMENSION_MISMATCH","Both dimensions must be greater than zero (you provided $width x $height)", null);
|
||||
return;
|
||||
}
|
||||
Log.i("thermion_flutter", "Creating SurfaceTexture ${width}x${height}");
|
||||
|
||||
_surfaceTextureEntry = flutterPluginBinding.textureRegistry.createSurfaceTexture()
|
||||
_surfaceTexture = _surfaceTextureEntry!!.surfaceTexture();
|
||||
_surfaceTexture!!.setDefaultBufferSize(width, height)
|
||||
val args = call.arguments as List<*>
|
||||
val width = args[0] as Int
|
||||
val height = args[1] as Int
|
||||
if (width < 1 || height < 1) {
|
||||
result.error("DIMENSION_MISMATCH", "Both dimensions must be greater than zero (you provided $width x $height)", null)
|
||||
return
|
||||
}
|
||||
Log.i("thermion_flutter", "Creating SurfaceTexture ${width}x${height}")
|
||||
|
||||
val surfaceTextureEntry = flutterPluginBinding.textureRegistry.createSurfaceTexture()
|
||||
val surfaceTexture = surfaceTextureEntry.surfaceTexture()
|
||||
surfaceTexture.setDefaultBufferSize(width, height)
|
||||
|
||||
_surface = Surface(_surfaceTexture)
|
||||
val surface = Surface(surfaceTexture)
|
||||
|
||||
if(!_surface!!.isValid) {
|
||||
result.error("SURFACE_INVALID", "Failed to create valid surface", null)
|
||||
} else {
|
||||
val nativeWindow = _lib.get_native_window_from_surface(_surface!! as Object, JNIEnv.CURRENT)
|
||||
result.success(listOf(_surfaceTextureEntry!!.id(), null, Pointer.nativeValue(nativeWindow)))
|
||||
}
|
||||
}
|
||||
if (!surface.isValid) {
|
||||
result.error("SURFACE_INVALID", "Failed to create valid surface", null)
|
||||
} else {
|
||||
val flutterTextureId = surfaceTextureEntry.id()
|
||||
textures[flutterTextureId] = TextureEntry(surfaceTextureEntry, surfaceTexture, surface)
|
||||
val nativeWindow = _lib.get_native_window_from_surface(surface as Object, JNIEnv.CURRENT)
|
||||
result.success(listOf(flutterTextureId, flutterTextureId, Pointer.nativeValue(nativeWindow)))
|
||||
}
|
||||
}
|
||||
"destroyTexture" -> {
|
||||
val args = call.arguments as List<*>
|
||||
val textureId = (args[0] as Int).toLong()
|
||||
val textureEntry = textures[textureId]
|
||||
if (textureEntry != null) {
|
||||
textureEntry.surface.release()
|
||||
textureEntry.surfaceTextureEntry.release()
|
||||
textures.remove(textureId)
|
||||
result.success(true)
|
||||
} else {
|
||||
result.error("TEXTURE_NOT_FOUND", "Texture with id $textureId not found", null)
|
||||
}
|
||||
}
|
||||
"markTextureFrameAvailable" -> {
|
||||
val textureId = (call.arguments as Int).toLong()
|
||||
val textureEntry = textures[textureId]
|
||||
if (textureEntry != null) {
|
||||
result.success(null)
|
||||
} else {
|
||||
result.error("TEXTURE_NOT_FOUND", "Texture with id $textureId not found", null)
|
||||
}
|
||||
}
|
||||
"getResourceLoaderWrapper" -> {
|
||||
val resourceLoader = _lib.make_resource_loader_wrapper_android(loadResourceWrapper, freeResourceWrapper, Pointer(0))
|
||||
result.success(Pointer.nativeValue(resourceLoader))
|
||||
@@ -196,13 +224,6 @@ class ThermionFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
||||
val renderCallbackFnPointer = _lib.make_render_callback_fn_pointer(RenderCallbackImpl(this))
|
||||
result.success(listOf(Pointer.nativeValue(renderCallbackFnPointer), 0))
|
||||
}
|
||||
"destroyTexture" -> {
|
||||
_surface!!.release();
|
||||
_surfaceTextureEntry!!.release();
|
||||
_surface = null
|
||||
_surfaceTextureEntry = null
|
||||
result.success(true)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
@@ -210,7 +231,13 @@ class ThermionFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
channel.setMethodCallHandler(null)
|
||||
// Release all textures
|
||||
for ((_, textureEntry) in textures) {
|
||||
textureEntry.surface.release()
|
||||
textureEntry.surfaceTextureEntry.release()
|
||||
}
|
||||
textures.clear()
|
||||
}
|
||||
|
||||
|
||||
96
thermion_flutter/thermion_flutter/example/README.md
Normal file
96
thermion_flutter/thermion_flutter/example/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
For a more thorough example with both Flutter and pure Dart, see the [example repository](https://github.com/nmfisher/thermion_examples).
|
||||
|
||||
[flutter/quickstart/lib/main.dart](https://github.com/nmfisher/thermion_examples/blob/master/flutter/quickstart/lib/main.dart)
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
import 'dart:math';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
bool _loaded = false;
|
||||
ThermionViewer? _thermionViewer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future _load() async {
|
||||
var viewer = await ThermionFlutterPlugin.createViewer();
|
||||
_thermionViewer = viewer;
|
||||
_thermionViewer!.loadSkybox("assets/default_env_skybox.ktx");
|
||||
_thermionViewer!.loadGlb("assets/cube.glb");
|
||||
|
||||
_thermionViewer!.setCameraPosition(0, 1, 10);
|
||||
_thermionViewer!.setCameraRotation(
|
||||
v.Quaternion.axisAngle(v.Vector3(1, 0, 0), -30 / 180 * pi) *
|
||||
v.Quaternion.axisAngle(v.Vector3(0, 1, 0), 15 / 180 * pi));
|
||||
_thermionViewer!.addLight(LightType.SUN, 7500, 50000, 0, 0, 0, 1, -1, -1);
|
||||
_thermionViewer!.setRendering(true);
|
||||
_loaded = true;
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future _unload() async {
|
||||
var viewer = _thermionViewer!;
|
||||
_thermionViewer = null;
|
||||
setState(() {});
|
||||
await viewer.dispose();
|
||||
_loaded = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _loadButton() {
|
||||
return Center(
|
||||
child: ElevatedButton(child: const Text("Load"), onPressed: _load));
|
||||
}
|
||||
|
||||
Widget _unloadButton() {
|
||||
return Center(
|
||||
child: ElevatedButton(child: const Text("Unload"), onPressed: _unload));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
if (_thermionViewer != null)
|
||||
Positioned.fill(child: ThermionWidget(viewer: _thermionViewer!)),
|
||||
if (!_loaded) _loadButton(),
|
||||
if (_loaded) _unloadButton(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
var registrar : FlutterPluginRegistrar
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionFlutterTexture?
|
||||
var textures: [Int64: ThermionFlutterTexture] = [:]
|
||||
|
||||
var createdAt = Date()
|
||||
|
||||
@@ -116,13 +116,12 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
instance.resources.removeObject(forKey:rbuf.id)
|
||||
}
|
||||
|
||||
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
if(instance.texture != nil) {
|
||||
instance.registry.textureFrameAvailable(instance.texture!.flutterTextureId)
|
||||
var markTextureFrameAvailable: @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance: SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
for (_, texture) in instance.textures {
|
||||
instance.registry.textureFrameAvailable(texture.flutterTextureId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let _messenger = registrar.messenger();
|
||||
@@ -151,22 +150,35 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
result(nil)
|
||||
case "getSharedContext":
|
||||
result(nil)
|
||||
case "markTextureFrameAvailable":
|
||||
let flutterTextureId = call.arguments as! Int64
|
||||
registry.textureFrameAvailable(flutterTextureId)
|
||||
result(nil)
|
||||
case "createTexture":
|
||||
let args = call.arguments as! [Any]
|
||||
let width = args[0] as! Int64
|
||||
let height = args[1] as! Int64
|
||||
|
||||
self.texture = ThermionFlutterTexture(width: width, height: height, registry: registry)
|
||||
let pixelBufferPtr = unsafeBitCast(self.texture!.pixelBuffer, to:UnsafeRawPointer.self)
|
||||
let pixelBufferAddress = Int(bitPattern:pixelBufferPtr);
|
||||
|
||||
let texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
|
||||
|
||||
result([self.texture!.flutterTextureId as Any, nil, pixelBufferAddress])
|
||||
if texture.texture.metalTextureAddress == -1 {
|
||||
result(nil)
|
||||
} else {
|
||||
textures[texture.flutterTextureId] = texture
|
||||
result([texture.flutterTextureId, texture.texture.metalTextureAddress, nil])
|
||||
}
|
||||
case "destroyTexture":
|
||||
let texture = self.texture
|
||||
self.texture = nil
|
||||
texture?.destroy()
|
||||
let args = call.arguments as! [Any]
|
||||
let flutterTextureId = args[0] as! Int64
|
||||
|
||||
result(true)
|
||||
if let texture = textures[flutterTextureId] {
|
||||
registry.unregisterTexture(flutterTextureId)
|
||||
texture.destroy()
|
||||
textures.removeValue(forKey: flutterTextureId)
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
@@ -4,45 +4,31 @@ import Flutter
|
||||
|
||||
public class ThermionFlutterTexture : NSObject, FlutterTexture {
|
||||
|
||||
public var pixelBuffer: CVPixelBuffer?
|
||||
|
||||
var pixelBufferAttrs = [
|
||||
kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_32BGRA),
|
||||
kCVPixelBufferOpenGLCompatibilityKey: kCFBooleanTrue,
|
||||
kCVPixelBufferOpenGLESCompatibilityKey: kCFBooleanTrue,
|
||||
kCVPixelBufferIOSurfacePropertiesKey: [:]
|
||||
] as CFDictionary
|
||||
|
||||
var flutterTextureId: Int64 = -1
|
||||
var registry: FlutterTextureRegistry?
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionTextureSwift
|
||||
|
||||
init(width:Int64, height:Int64, registry:FlutterTextureRegistry) {
|
||||
init(registry:FlutterTextureRegistry, width:Int64, height:Int64) {
|
||||
self.registry = registry
|
||||
|
||||
self.texture = ThermionTextureSwift(width:width, height: height)
|
||||
super.init()
|
||||
|
||||
if(CVPixelBufferCreate(kCFAllocatorDefault, Int(width), Int(height),
|
||||
kCVPixelFormatType_32BGRA, pixelBufferAttrs, &pixelBuffer) != kCVReturnSuccess) {
|
||||
print("Error allocating pixel buffer")
|
||||
} else {
|
||||
self.flutterTextureId = registry.register(self)
|
||||
}
|
||||
self.flutterTextureId = registry.register(self)
|
||||
}
|
||||
|
||||
|
||||
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
return Unmanaged.passRetained(pixelBuffer!);
|
||||
if(self.texture.pixelBuffer == nil) {
|
||||
return nil
|
||||
}
|
||||
return Unmanaged.passRetained(self.texture.pixelBuffer!);
|
||||
}
|
||||
|
||||
public func onTextureUnregistered(_ texture:FlutterTexture) {
|
||||
print("Texture unregistered")
|
||||
|
||||
}
|
||||
|
||||
public func destroy() {
|
||||
if(self.flutterTextureId != -1) {
|
||||
self.registry!.unregisterTexture(self.flutterTextureId)
|
||||
}
|
||||
|
||||
self.pixelBuffer = nil
|
||||
self.registry.unregisterTexture(self.flutterTextureId)
|
||||
self.texture.destroyTexture()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import GLKit
|
||||
|
||||
@objc public class ThermionDartTexture : NSObject {
|
||||
@objc public class ThermionTextureSwift : NSObject {
|
||||
|
||||
public var pixelBuffer: CVPixelBuffer?
|
||||
|
||||
@@ -62,37 +62,6 @@ import GLKit
|
||||
metalTexture = CVMetalTextureGetTexture(cvMetalTexture!)
|
||||
let metalTexturePtr = Unmanaged.passRetained(metalTexture!).toOpaque()
|
||||
metalTextureAddress = Int(bitPattern:metalTexturePtr)
|
||||
|
||||
// CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
// let bufferWidth = Int(CVPixelBufferGetWidth(pixelBuffer!))
|
||||
// let bufferHeight = Int(CVPixelBufferGetHeight(pixelBuffer!))
|
||||
// let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer!)
|
||||
|
||||
// guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer!) else {
|
||||
// return
|
||||
// }
|
||||
|
||||
// for row in 0..<bufferHeight {
|
||||
// var pixel = baseAddress + row * bytesPerRow
|
||||
// for col in 0..<bufferWidth {
|
||||
// let blue = pixel
|
||||
// blue.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
// let red = pixel + 1
|
||||
// red.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
// let green = pixel + 2
|
||||
// green.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
// let alpha = pixel + 3
|
||||
// alpha.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
// pixel += 4;
|
||||
// }
|
||||
// }
|
||||
|
||||
// CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
}
|
||||
|
||||
@objc public func destroyTexture() {
|
||||
@@ -104,5 +73,70 @@ import GLKit
|
||||
self.cvMetalTextureCache = nil
|
||||
}
|
||||
|
||||
@objc public func fillColor() {
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let bufferWidth = Int(CVPixelBufferGetWidth(pixelBuffer!))
|
||||
let bufferHeight = Int(CVPixelBufferGetHeight(pixelBuffer!))
|
||||
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer!)
|
||||
|
||||
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer!) else {
|
||||
return
|
||||
}
|
||||
|
||||
for row in 0..<bufferHeight {
|
||||
var pixel = baseAddress + row * bytesPerRow
|
||||
for col in 0..<bufferWidth {
|
||||
let blue = pixel
|
||||
blue.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
let red = pixel + 1
|
||||
red.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
let green = pixel + 2
|
||||
green.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
let alpha = pixel + 3
|
||||
alpha.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
pixel += 4;
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
}
|
||||
@objc public func getTextureBytes() -> NSData? {
|
||||
guard let texture = self.metalTexture else {
|
||||
print("Metal texture is not available")
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = texture.width
|
||||
let height = texture.height
|
||||
let bytesPerPixel = 4 // RGBA
|
||||
let bytesPerRow = width * bytesPerPixel
|
||||
let byteCount = bytesPerRow * height
|
||||
|
||||
var bytes = [UInt8](repeating: 0, count: byteCount)
|
||||
let region = MTLRegionMake2D(0, 0, width, height)
|
||||
texture.getBytes(&bytes, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)
|
||||
|
||||
// Swizzle bytes from BGRA to RGBA
|
||||
for i in stride(from: 0, to: byteCount, by: 4) {
|
||||
let blue = bytes[i]
|
||||
let green = bytes[i + 1]
|
||||
let red = bytes[i + 2]
|
||||
let alpha = bytes[i + 3]
|
||||
|
||||
bytes[i] = red
|
||||
bytes[i + 1] = green
|
||||
bytes[i + 2] = blue
|
||||
bytes[i + 3] = alpha
|
||||
}
|
||||
|
||||
// Convert Swift Data to Objective-C NSData
|
||||
let nsData = Data(bytes: &bytes, count: byteCount) as NSData
|
||||
return nsData
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:async';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
|
||||
///
|
||||
/// Handles all platform-specific initialization to create a backing rendering
|
||||
/// surface in a Flutter application and lifecycle listeners to pause rendering
|
||||
/// when the app is inactive or in the background.
|
||||
/// Call [createViewerWithOptions] to create an instance of [ThermionViewer].
|
||||
///
|
||||
class ThermionFlutterPlugin {
|
||||
ThermionFlutterPlugin._();
|
||||
|
||||
static bool _initializing = false;
|
||||
|
||||
static ThermionViewer? _viewer;
|
||||
|
||||
static ThermionFlutterOptions? options;
|
||||
|
||||
static Future<ThermionViewer> createViewer(
|
||||
{ThermionFlutterOptions options =
|
||||
const ThermionFlutterOptions.empty()}) async {
|
||||
if (_initializing) {
|
||||
throw Exception("Existing call to createViewer has not completed.");
|
||||
}
|
||||
_initializing = true;
|
||||
|
||||
if (_viewer != null) {
|
||||
throw Exception(
|
||||
"Instance of ThermionViewer has already been created. Ensure you call dispose() on that instance.");
|
||||
}
|
||||
|
||||
options = options;
|
||||
|
||||
_viewer =
|
||||
await ThermionFlutterPlatform.instance.createViewer(options: options);
|
||||
|
||||
var camera = await _viewer!.getActiveCamera();
|
||||
await camera.setLensProjection();
|
||||
|
||||
_viewer!.onDispose(() async {
|
||||
_viewer = null;
|
||||
ThermionFlutterPlugin.options = null;
|
||||
});
|
||||
_initializing = false;
|
||||
return _viewer!;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import '../../utils/camera_orientation.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../utils/camera_orientation.dart';
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:vector_math/vector_math_64.dart' as v64;
|
||||
|
||||
|
||||
class CameraOptionsWidget extends StatefulWidget {
|
||||
final ThermionViewer controller;
|
||||
@@ -82,19 +82,19 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
data: const SliderThemeData(
|
||||
showValueIndicator: ShowValueIndicator.always,
|
||||
valueIndicatorTextStyle: TextStyle(color: Colors.black)),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
Text("Aperture"),
|
||||
const Text("Aperture"),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _apertureController,
|
||||
)),
|
||||
Text("Speed"),
|
||||
const Text("Speed"),
|
||||
Expanded(child: TextField(controller: _speedController)),
|
||||
Text("Sensitivity"),
|
||||
const Text("Sensitivity"),
|
||||
Expanded(
|
||||
child: TextField(controller: _sensitivityController)),
|
||||
]),
|
||||
@@ -112,7 +112,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Focal length"),
|
||||
const Text("Focal length"),
|
||||
Slider(
|
||||
label: _focalLength.toString(),
|
||||
value: _focalLength,
|
||||
@@ -127,7 +127,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("X"),
|
||||
const Text("X"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.x.toString(),
|
||||
value: widget.cameraOrientation.position.x,
|
||||
@@ -141,7 +141,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Y"),
|
||||
const Text("Y"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.y.toString(),
|
||||
value: widget.cameraOrientation.position.y,
|
||||
@@ -155,7 +155,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Z"),
|
||||
const Text("Z"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.z.toString(),
|
||||
value: widget.cameraOrientation.position.z,
|
||||
@@ -169,7 +169,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTX"),
|
||||
const Text("ROTX"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationX.toString(),
|
||||
value: widget.cameraOrientation.rotationX,
|
||||
@@ -183,7 +183,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTY"),
|
||||
const Text("ROTY"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationY.toString(),
|
||||
value: widget.cameraOrientation.rotationY,
|
||||
@@ -197,7 +197,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
}),
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTZ"),
|
||||
const Text("ROTZ"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationZ.toString(),
|
||||
value: widget.cameraOrientation.rotationZ,
|
||||
@@ -213,7 +213,7 @@ class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
Wrap(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Text("Main "),
|
||||
child: const Text("Main "),
|
||||
onTap: () {
|
||||
widget.controller.setMainCamera();
|
||||
},
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' as v64;
|
||||
|
||||
class CameraOrientationWidget extends StatefulWidget {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const CameraOrientationWidget({Key? key, required this.viewer}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraOrientationWidgetState createState() => _CameraOrientationWidgetState();
|
||||
}
|
||||
|
||||
class _CameraOrientationWidgetState extends State<CameraOrientationWidget> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
v64.Vector3? _position;
|
||||
v64.Matrix3? _rotation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 16), // ~60 FPS
|
||||
)..repeat();
|
||||
|
||||
_controller.addListener(_updateCameraInfo);
|
||||
}
|
||||
|
||||
void _updateCameraInfo() async {
|
||||
final position = await widget.viewer.getCameraPosition();
|
||||
final rotation = await widget.viewer.getCameraRotation();
|
||||
setState(() {
|
||||
_position = position;
|
||||
_rotation = rotation;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Position: ${_formatVector(_position)}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rotation: ${_formatMatrix(_rotation)}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatVector(v64.Vector3? vector) {
|
||||
if (vector == null) return 'N/A';
|
||||
return '(${vector.x.toStringAsFixed(2)}, ${vector.y.toStringAsFixed(2)}, ${vector.z.toStringAsFixed(2)})';
|
||||
}
|
||||
|
||||
String _formatMatrix(v64.Matrix3? matrix) {
|
||||
if (matrix == null) return 'N/A';
|
||||
return 'Yaw: ${_getYaw(matrix).toStringAsFixed(2)}°, Pitch: ${_getPitch(matrix).toStringAsFixed(2)}°, Roll: ${_getRoll(matrix).toStringAsFixed(2)}°';
|
||||
}
|
||||
|
||||
double _getYaw(v64.Matrix3 matrix) {
|
||||
return -atan2(matrix[2], matrix[0]) * 180 / pi;
|
||||
}
|
||||
|
||||
double _getPitch(v64.Matrix3 matrix) {
|
||||
return -asin(matrix[5]) * 180 / pi;
|
||||
}
|
||||
|
||||
double _getRoll(v64.Matrix3 matrix) {
|
||||
return atan2(matrix[3], matrix[4]) * 180 / pi;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
|
||||
class CameraSelectorWidget extends StatefulWidget {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const CameraSelectorWidget({Key? key, required this.viewer}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraSelectorWidgetState createState() => _CameraSelectorWidgetState();
|
||||
}
|
||||
|
||||
class _CameraSelectorWidgetState extends State<CameraSelectorWidget> {
|
||||
int _activeIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int cameraCount = widget.viewer.getCameraCount();
|
||||
|
||||
return Container(
|
||||
height:32,
|
||||
margin: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildCameraButton("Main", 0),
|
||||
if (cameraCount > 1) const VerticalDivider(width: 16, thickness: 1),
|
||||
...List.generate(cameraCount - 1, (index) {
|
||||
return _buildCameraButton("${index + 1}", index + 1);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraButton(String label, int index) {
|
||||
bool isActive = _activeIndex == index;
|
||||
return Flexible(child:TextButton(
|
||||
onPressed: () async {
|
||||
if (index == 0) {
|
||||
await widget.viewer.setMainCamera();
|
||||
} else {
|
||||
Camera camera = widget.viewer.getCameraAt(index);
|
||||
await widget.viewer.setActiveCamera(camera);
|
||||
}
|
||||
setState(() {
|
||||
_activeIndex = index;
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
backgroundColor: isActive ? Colors.blue.withOpacity(0.1) : null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
color: isActive ? Colors.blue : Colors.black87,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:animation_tools_dart/animation_tools_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math';
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:animation_tools_dart/animation_tools_dart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
class IblRotationSliderWidget extends StatefulWidget {
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
// import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
// import 'package:thermion_dart/thermion_dart/utils/light_options.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/widgets.dart';
|
||||
|
||||
// import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
// class LightSliderWidget extends StatefulWidget {
|
||||
// final ThermionViewer controller;
|
||||
|
||||
// final LightOptions options;
|
||||
// final bool showControls;
|
||||
|
||||
// LightSliderWidget(
|
||||
// {super.key,
|
||||
// required this.controller,
|
||||
// this.showControls = false,
|
||||
// required this.options});
|
||||
// @override
|
||||
// State<StatefulWidget> createState() => _LightSliderWidgetState();
|
||||
// }
|
||||
|
||||
// class _LightSliderWidgetState extends State<LightSliderWidget> {
|
||||
// ThermionEntity? _light;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// _set();
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
// Future _set() async {
|
||||
// await widget.controller.clearLights();
|
||||
|
||||
// if (widget.options.iblPath != null) {
|
||||
// _light = await widget.controller.loadIbl(widget.options.iblPath!,
|
||||
// intensity: widget.options.iblIntensity);
|
||||
// }
|
||||
|
||||
// _light = await widget.controller.addLight(
|
||||
// LightType.values[
|
||||
// widget.options.directionalType],
|
||||
// widget.options.directionalColor,
|
||||
// widget.options.directionalIntensity,
|
||||
// widget.options.directionalPosition.x,
|
||||
// widget.options.directionalPosition.y,
|
||||
// widget.options.directionalPosition.z,
|
||||
// widget.options.directionalDirection.x,
|
||||
// widget.options.directionalDirection.y,
|
||||
// widget.options.directionalDirection.z,
|
||||
// castShadows:widget.options.directionalCastShadows);
|
||||
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (_light == null || !widget.showControls) {
|
||||
// return Container();
|
||||
// }
|
||||
// return Theme(
|
||||
// data: ThemeData(platform: TargetPlatform.android),
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
|
||||
// child: SliderTheme(
|
||||
// data: const SliderThemeData(
|
||||
// showValueIndicator: ShowValueIndicator.always,
|
||||
// valueIndicatorTextStyle: TextStyle(color: Colors.black)),
|
||||
// child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// Text("Directional"),
|
||||
// Row(children: [
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label:
|
||||
// "POSX ${widget.options.directionalPosition.x}",
|
||||
// value: widget.options.directionalPosition.x,
|
||||
// min: -10.0,
|
||||
// max: 10.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalPosition.x = value;
|
||||
// _set();
|
||||
// })),
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label:
|
||||
// "POSY ${widget.options.directionalPosition.y}",
|
||||
// value: widget.options.directionalPosition.y,
|
||||
// min: -100.0,
|
||||
// max: 100.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalPosition.y = value;
|
||||
// _set();
|
||||
// })),
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label:
|
||||
// "POSZ ${widget.options.directionalPosition.z}",
|
||||
// value: widget.options.directionalPosition.z,
|
||||
// min: -100.0,
|
||||
// max: 100.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalPosition.z = value;
|
||||
// _set();
|
||||
// }))
|
||||
// ]),
|
||||
// Row(children: [
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label: "DIRX",
|
||||
// value: widget.options.directionalDirection.x,
|
||||
// min: -1.0,
|
||||
// max: 1.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalDirection.x = value;
|
||||
// _set();
|
||||
// })),
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label: "DIRY",
|
||||
// value: widget.options.directionalDirection.y,
|
||||
// min: -1.0,
|
||||
// max: 1.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalDirection.y = value;
|
||||
// _set();
|
||||
// })),
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label: "DIRZ",
|
||||
// value: widget.options.directionalDirection.z,
|
||||
// min: -1.0,
|
||||
// max: 1.0,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalDirection.z = value;
|
||||
// _set();
|
||||
// }))
|
||||
// ]),
|
||||
// Slider(
|
||||
// label: "Color",
|
||||
// value: widget.options.directionalColor,
|
||||
// min: 0,
|
||||
// max: 16000,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalColor = value;
|
||||
// _set();
|
||||
// }),
|
||||
// Slider(
|
||||
// label: "Intensity ${widget.options.directionalIntensity}",
|
||||
// value: widget.options.directionalIntensity,
|
||||
// min: 0,
|
||||
// max: 1000000,
|
||||
// onChanged: (value) {
|
||||
// widget.options.directionalIntensity = value;
|
||||
// _set();
|
||||
// }),
|
||||
// DropdownButton(
|
||||
// onChanged: (v) {
|
||||
// this.widget.options.directionalType = v;
|
||||
// _set();
|
||||
// },
|
||||
// value: this.widget.options.directionalType,
|
||||
// items: List<DropdownMenuItem>.generate(
|
||||
// 5,
|
||||
// (idx) => DropdownMenuItem(
|
||||
// value: idx,
|
||||
// child: Text("$idx"),
|
||||
// ))),
|
||||
// Row(children: [
|
||||
// Text(
|
||||
// "Shadows: ${this.widget.options.directionalCastShadows}"),
|
||||
// Checkbox(
|
||||
// value: widget.options.directionalCastShadows,
|
||||
// onChanged: (v) {
|
||||
// this.widget.options.directionalCastShadows = v!;
|
||||
// _set();
|
||||
// })
|
||||
// ]),
|
||||
// Text("Indirect"),
|
||||
// Row(children: [
|
||||
// Expanded(
|
||||
// child: Slider(
|
||||
// label: "Intensity ${widget.options.iblIntensity}",
|
||||
// value: widget.options.iblIntensity,
|
||||
// min: 0.0,
|
||||
// max: 200000,
|
||||
// onChanged: (value) {
|
||||
// widget.options.iblIntensity = value;
|
||||
// _set();
|
||||
// })),
|
||||
// ])
|
||||
// ]))));
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef ResizeCallback = void Function(Size newSize);
|
||||
typedef ResizeCallback = void Function(Size oldSize, Size newSize);
|
||||
|
||||
class ResizeObserver extends SingleChildRenderObjectWidget {
|
||||
final ResizeCallback onResized;
|
||||
@@ -34,7 +34,7 @@ class _RenderResizeObserver extends RenderProxyBox {
|
||||
void performLayout() async {
|
||||
super.performLayout();
|
||||
if (size.width != _oldSize.width || size.height != _oldSize.height) {
|
||||
onLayoutChangedCallback(size);
|
||||
onLayoutChangedCallback(_oldSize, size);
|
||||
_oldSize = Size(size.width, size.height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
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 {
|
||||
Vector2 toVector2() {
|
||||
return Vector2(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// A widget that captures swipe/pointer events.
|
||||
/// This is a dumb listener; events are forwarded to a [InputHandler].
|
||||
///
|
||||
class ThermionListenerWidget extends StatefulWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a ThermionWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
|
||||
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentViewer].
|
||||
///
|
||||
final Widget? child;
|
||||
|
||||
///
|
||||
/// The handler to use for interpreting gestures/pointer movements.
|
||||
///
|
||||
final InputHandler gestureHandler;
|
||||
|
||||
const ThermionListenerWidget({
|
||||
Key? key,
|
||||
required this.gestureHandler,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ThermionListenerWidget> createState() => _ThermionListenerWidgetState();
|
||||
}
|
||||
|
||||
class _ThermionListenerWidgetState extends State<ThermionListenerWidget> {
|
||||
bool get isDesktop =>
|
||||
kIsWeb || Platform.isLinux || Platform.isWindows || Platform.isMacOS;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
}
|
||||
|
||||
final _keyMap = {
|
||||
PhysicalKeyboardKey.keyW: PhysicalKey.W,
|
||||
PhysicalKeyboardKey.keyA: PhysicalKey.A,
|
||||
PhysicalKeyboardKey.keyS: PhysicalKey.S,
|
||||
PhysicalKeyboardKey.keyD: PhysicalKey.D,
|
||||
};
|
||||
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
PhysicalKey? key = _keyMap[event.physicalKey];
|
||||
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
||||
widget.gestureHandler.keyDown(key!);
|
||||
} else if (event is KeyUpEvent) {
|
||||
widget.gestureHandler.keyUp(key!);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
}
|
||||
|
||||
Widget _desktop(double pixelRatio) {
|
||||
return Listener(
|
||||
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() * 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() * 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(double pixelRatio) {
|
||||
return _MobileListenerWidget(
|
||||
gestureHandler: widget.gestureHandler, pixelRatio: pixelRatio, child:widget.child);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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 (isDesktop) Positioned.fill(child: _desktop(pixelRatio)),
|
||||
if (!isDesktop) Positioned.fill(child: _mobile(pixelRatio))
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileListenerWidget extends StatefulWidget {
|
||||
final InputHandler gestureHandler;
|
||||
final double pixelRatio;
|
||||
final Widget? child;
|
||||
|
||||
const _MobileListenerWidget(
|
||||
{Key? key, required this.gestureHandler, required this.pixelRatio, this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MobileListenerWidgetState();
|
||||
}
|
||||
|
||||
class _MobileListenerWidgetState extends State<_MobileListenerWidget> {
|
||||
bool isPan = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (details) => widget.gestureHandler.onPointerDown(
|
||||
details.localPosition.toVector2() * widget.pixelRatio, false),
|
||||
onDoubleTap: () {
|
||||
widget.gestureHandler.setActionForType(InputType.SCALE1,
|
||||
isPan ? InputAction.TRANSLATE : InputAction.ROTATE);
|
||||
},
|
||||
onScaleStart: (details) async {
|
||||
await widget.gestureHandler.onScaleStart(
|
||||
details.localFocalPoint.toVector2(), details.pointerCount);
|
||||
},
|
||||
onScaleUpdate: (ScaleUpdateDetails details) async {
|
||||
await widget.gestureHandler.onScaleUpdate(
|
||||
details.localFocalPoint.toVector2(),
|
||||
details.focalPointDelta.toVector2(),
|
||||
details.horizontalScale,
|
||||
details.verticalScale,
|
||||
details.scale,
|
||||
details.pointerCount);
|
||||
},
|
||||
onScaleEnd: (details) async {
|
||||
await widget.gestureHandler.onScaleEnd(details.pointerCount);
|
||||
},
|
||||
child: widget.child);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.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;
|
||||
|
||||
///
|
||||
/// A callback that will be invoked whenever this widget (and the underlying texture is resized).
|
||||
///
|
||||
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
|
||||
|
||||
const ThermionTextureWidget(
|
||||
{super.key,
|
||||
required this.viewer,
|
||||
required this.view,
|
||||
this.initial,
|
||||
this.onResize});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ThermionTextureWidgetState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
ThermionFlutterTexture? _texture;
|
||||
|
||||
static final _views = <t.View>[];
|
||||
|
||||
final _logger = Logger("_ThermionTextureWidgetState");
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_views.remove(widget.view);
|
||||
_texture?.destroy();
|
||||
_states.remove(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (_views.contains(widget.view)) {
|
||||
throw Exception("View already embedded in a widget");
|
||||
}
|
||||
_views.add(widget.view);
|
||||
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(widget.view, width, height);
|
||||
|
||||
await widget.view.updateViewport(_texture!.width, _texture!.height);
|
||||
|
||||
try {
|
||||
await widget.onResize?.call(
|
||||
Size(_texture!.width.toDouble(), _texture!.height.toDouble()),
|
||||
widget.view,
|
||||
dpr);
|
||||
} catch (err, st) {
|
||||
_logger.severe(err);
|
||||
_logger.severe(st);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
_states.add(this);
|
||||
|
||||
_requestFrame();
|
||||
|
||||
widget.viewer.onDispose(() async {
|
||||
var texture = _texture;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
await texture?.destroy();
|
||||
_views.clear();
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _rendering = false;
|
||||
|
||||
static final _states = <_ThermionTextureWidgetState>{};
|
||||
|
||||
int lastRender = 0;
|
||||
|
||||
///
|
||||
/// Each instance of ThermionTextureWidget in the widget hierarchy must
|
||||
/// call[markFrameAvailable] on every frame to notify Flutter that the content
|
||||
/// of its backing texture has changed.
|
||||
///
|
||||
/// Calling [requestFrame] on [ThermionViewer], however, will render all
|
||||
/// views/swapchains that have been marked as renderable (see [setRenderable]).
|
||||
///
|
||||
/// Only need one instance of [ThermionTextureWidget] needs to call
|
||||
/// [requestFrame]. We manage this by storing all instances of
|
||||
/// [_ThermionTextureWidgetState] in a static set, and allowing the first
|
||||
/// instance to call [requestFrame].
|
||||
///
|
||||
void _requestFrame() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (widget.viewer.rendering && !_rendering) {
|
||||
_rendering = true;
|
||||
if (this == _states.first && _texture != null) {
|
||||
await widget.viewer.requestFrame();
|
||||
lastRender = d.inMilliseconds;
|
||||
}
|
||||
await _texture?.markFrameAvailable();
|
||||
_rendering = false;
|
||||
}
|
||||
_requestFrame();
|
||||
});
|
||||
}
|
||||
|
||||
final _resizing = <Future>[];
|
||||
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resize(Size oldSize, Size newSize) async {
|
||||
await Future.wait(_resizing);
|
||||
|
||||
_resizeTimer?.cancel();
|
||||
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 100), () async {
|
||||
await Future.wait(_resizing);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSize.width == _texture?.width &&
|
||||
newSize.height == _texture?.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer();
|
||||
|
||||
_resizing.add(completer.future);
|
||||
|
||||
final dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
newSize *= dpr;
|
||||
|
||||
var newWidth = newSize.width.ceil();
|
||||
var newHeight = newSize.height.ceil();
|
||||
|
||||
await _texture?.resize(
|
||||
newWidth,
|
||||
newHeight,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
await widget.view.updateViewport(_texture!.width, _texture!.height);
|
||||
|
||||
await widget.onResize?.call(
|
||||
Size(_texture!.width.toDouble(), _texture!.height.toDouble()),
|
||||
widget.view,
|
||||
dpr);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
completer.complete();
|
||||
_resizing.remove(completer.future);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_texture == null) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
return ResizeObserver(
|
||||
onResized: _resize,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(
|
||||
child: Texture(
|
||||
key: ObjectKey("flutter_texture_${_texture!.flutterId}"),
|
||||
textureId: _texture!.flutterId,
|
||||
filterQuality: FilterQuality.none,
|
||||
freeze: false,
|
||||
))
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'dart:io';
|
||||
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/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/shared_types/view.dart' as t;
|
||||
import 'thermion_widget_windows.dart';
|
||||
|
||||
Future kDefaultResizeCallback(Size size, t.View view, double pixelRatio) async {
|
||||
var camera = await view.getCamera();
|
||||
var near = await camera.getNear();
|
||||
var far = await camera.getCullingFar();
|
||||
var focalLength = await camera.getFocalLength();
|
||||
|
||||
await camera.setLensProjection(near:near, far:far, focalLength: focalLength,
|
||||
aspect: size.width.toDouble() / size.height.toDouble());
|
||||
}
|
||||
|
||||
class ThermionWidget extends StatefulWidget {
|
||||
///
|
||||
/// The viewer.
|
||||
///
|
||||
final ThermionViewer viewer;
|
||||
|
||||
///
|
||||
/// The [t.View] associated with this widget. If null, the default View will be used.
|
||||
///
|
||||
final t.View? view;
|
||||
|
||||
///
|
||||
/// A callback to invoke whenever this widget and the underlying surface are
|
||||
/// resized. If a callback is not explicitly provided, the default callback
|
||||
/// will be run, which changes the aspect ratio for the active camera in
|
||||
/// the View managed by this widget. If you specify your own callback,
|
||||
/// you probably want to preserve this behaviour (otherwise the aspect ratio)
|
||||
/// will be incorrect.
|
||||
///
|
||||
/// To completely disable the resize callback, pass [null].
|
||||
///
|
||||
/// IMPORTANT - size is specified in physical pixels, not logical pixels.
|
||||
/// If you need to work with Flutter dimensions, divide [size] by
|
||||
/// [pixelRatio].
|
||||
///
|
||||
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
|
||||
|
||||
///
|
||||
/// The content to render before the texture widget is available.
|
||||
/// The default is a solid red Container, intentionally chosen to make it clear that there will be at least one frame where the Texture widget is not being rendered.
|
||||
///
|
||||
final Widget? initial;
|
||||
|
||||
const ThermionWidget(
|
||||
{Key? key,
|
||||
this.initial,
|
||||
required this.viewer,
|
||||
this.view,
|
||||
this.onResize = kDefaultResizeCallback})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<ThermionWidget> createState() => _ThermionWidgetState();
|
||||
}
|
||||
|
||||
class _ThermionWidgetState extends State<ThermionWidget> {
|
||||
t.View? view;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initialize();
|
||||
}
|
||||
|
||||
Future initialize() async {
|
||||
if (widget.view != null) {
|
||||
view = widget.view;
|
||||
} else {
|
||||
view = await widget.viewer.getViewAt(0);
|
||||
}
|
||||
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) {
|
||||
return ThermionWidgetWeb(
|
||||
viewer: widget.viewer,
|
||||
options: ThermionFlutterPlugin.options as ThermionFlutterWebOptions?);
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
return ThermionWidgetWindows(viewer: widget.viewer, view: view!, initial: widget.initial, onResize: widget.onResize);
|
||||
}
|
||||
|
||||
return ThermionTextureWidget(
|
||||
key: ObjectKey(view!),
|
||||
initial: widget.initial,
|
||||
viewer: widget.viewer,
|
||||
view: view!,
|
||||
onResize: widget.onResize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'thermion_widget_web_stub.dart'
|
||||
if (dart.library.js_interop) 'thermion_widget_web_impl.dart';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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(), 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();
|
||||
}
|
||||
return Container(color: const Color(0x00000000));
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageCopyingWidget extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ImageCopyingWidgetState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageCopyingWidgetState extends State<_ImageCopyingWidget> {
|
||||
final _logger = Logger("_ImageCopyingWidgetState");
|
||||
ui.Image? _img;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
capture();
|
||||
});
|
||||
}
|
||||
|
||||
Future capture() async {
|
||||
try {
|
||||
final ImageBitmap newSource = await promiseToFuture<ImageBitmap>(
|
||||
window.createImageBitmap(
|
||||
document.getElementById("canvas") as HTMLCanvasElement));
|
||||
_img = await ui_web.createImageFromImageBitmap(newSource);
|
||||
setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
capture();
|
||||
});
|
||||
} catch (err) {
|
||||
_logger.severe(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawImage(image: _img!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
|
||||
|
||||
class ThermionWidgetWeb extends StatelessWidget {
|
||||
final ThermionFlutterWebOptions? options;
|
||||
final ThermionViewer viewer;
|
||||
|
||||
const ThermionWidgetWeb({super.key, required this.options, required this.viewer});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
throw Exception("STUB");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/resize_observer.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/transparent_filament_widget.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart' as t;
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
|
||||
|
||||
class ThermionWidgetWindows extends StatefulWidget {
|
||||
|
||||
final t.ThermionViewer viewer;
|
||||
|
||||
final t.View view;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final Widget? initial;
|
||||
|
||||
///
|
||||
/// A callback that will be invoked whenever this widget (and the underlying texture is resized).
|
||||
///
|
||||
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
|
||||
|
||||
const ThermionWidgetWindows({super.key, required this.viewer, this.initial, this.onResize, required this.view});
|
||||
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ThermionWidgetWindowsState();
|
||||
}
|
||||
|
||||
class _ThermionWidgetWindowsState extends State<ThermionWidgetWindows> {
|
||||
|
||||
ThermionFlutterWindow? _window;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await widget.viewer.initialized;
|
||||
|
||||
var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final renderBox = ((context.findRenderObject()) as RenderBox);
|
||||
var size = renderBox.size;
|
||||
var width = (size.width * dpr).ceil();
|
||||
var height = (size.height * dpr).ceil();
|
||||
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final offsetLeft = (offset.dx * dpr).toInt();
|
||||
final offsetTop = (offset.dy * dpr).toInt();
|
||||
|
||||
_window = await t.ThermionFlutterPlatform.instance.createWindow(width, height, offsetLeft, offsetTop);
|
||||
|
||||
await widget.view.updateViewport(_window!.width, _window!.height);
|
||||
|
||||
try {
|
||||
await widget.onResize?.call(
|
||||
Size(_window!.width.toDouble(), _window!.height.toDouble()),
|
||||
widget.view,
|
||||
dpr);
|
||||
} catch (err, st) {
|
||||
print(err);
|
||||
print(st);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
_requestFrame();
|
||||
|
||||
widget.viewer.onDispose(() async {
|
||||
var window = _window;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
await window?.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool _rendering = false;
|
||||
|
||||
void _requestFrame() {
|
||||
WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
if (widget.viewer.rendering && !_rendering) {
|
||||
_rendering = true;
|
||||
await widget.viewer.requestFrame();
|
||||
_rendering = false;
|
||||
}
|
||||
_requestFrame();
|
||||
});
|
||||
}
|
||||
|
||||
final _resizing = <Future>[];
|
||||
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resize(Size oldSize, Size newSize) async {
|
||||
await Future.wait(_resizing);
|
||||
|
||||
_resizeTimer?.cancel();
|
||||
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 100), () async {
|
||||
await Future.wait(_resizing);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSize.width == _window?.width &&
|
||||
newSize.height == _window?.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer();
|
||||
|
||||
_resizing.add(completer.future);
|
||||
|
||||
final dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
newSize *= dpr;
|
||||
|
||||
var newWidth = newSize.width.ceil();
|
||||
var newHeight = newSize.height.ceil();
|
||||
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final offsetLeft = (offset.dx * dpr).toInt();
|
||||
final offsetTop = (offset.dy * dpr).toInt();
|
||||
|
||||
await _window?.resize(
|
||||
newWidth,
|
||||
newHeight,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
);
|
||||
|
||||
await widget.view.updateViewport(_window!.width, _window!.height);
|
||||
|
||||
await widget.onResize?.call(
|
||||
Size(_window!.width.toDouble(), _window!.height.toDouble()),
|
||||
widget.view,
|
||||
dpr);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
completer.complete();
|
||||
_resizing.remove(completer.future);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_window == null) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
return ResizeObserver(
|
||||
onResized: _resize,
|
||||
child: CustomPaint(painter:TransparencyPainter()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
library;
|
||||
|
||||
export 'src/thermion_widget.dart';
|
||||
export 'src/thermion_listener_widget.dart';
|
||||
export 'src/camera/camera_selector_widget.dart';
|
||||
export 'src/camera/camera_orientation_widget.dart';
|
||||
@@ -1,100 +0,0 @@
|
||||
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
|
||||
/// surface in a Flutter application and lifecycle listeners to pause rendering
|
||||
/// when the app is inactive or in the background.
|
||||
/// Call [createViewer] to create an instance of [ThermionViewer].
|
||||
/// This is a lightweight singleton that
|
||||
///
|
||||
class ThermionFlutterPlugin {
|
||||
|
||||
ThermionFlutterPlugin._();
|
||||
|
||||
static AppLifecycleListener? _appLifecycleListener;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ThermionViewer> createViewer({String? uberArchivePath}) 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<ThermionFlutterTexture?> createTexture(
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
return ThermionFlutterPlatform.instance
|
||||
.createTexture(width, height, offsetLeft, offsetRight);
|
||||
}
|
||||
|
||||
static Future destroyTexture(ThermionFlutterTexture texture) async {
|
||||
return ThermionFlutterPlatform.instance.destroyTexture(texture);
|
||||
}
|
||||
|
||||
@override
|
||||
static Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
return ThermionFlutterPlatform.instance
|
||||
.resizeTexture(texture, width, height, offsetLeft, offsetRight);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:thermion_dart/thermion_dart/entities/entity_transform_controller.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HardwareKeyboardListener {
|
||||
final EntityTransformController _controller;
|
||||
var _listening = true;
|
||||
HardwareKeyboardListener(this._controller) {
|
||||
// Get the global handler.
|
||||
final KeyMessageHandler? existing =
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler;
|
||||
// The handler is guaranteed non-null since
|
||||
// `FallbackKeyEventRegistrar.instance` is only called during
|
||||
// `Focus.onFocusChange`, at which time `ServicesBinding.instance` must
|
||||
// have been called somewhere.
|
||||
assert(existing != null);
|
||||
// Assign the global handler with a patched handler.
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = (keyMessage) {
|
||||
if (keyMessage.rawEvent == null) {
|
||||
return false;
|
||||
}
|
||||
if (!_listening) {
|
||||
return false;
|
||||
}
|
||||
var event = keyMessage.rawEvent!;
|
||||
switch (event.logicalKey) {
|
||||
case LogicalKeyboardKey.escape:
|
||||
_listening = false;
|
||||
break;
|
||||
case LogicalKeyboardKey.keyW:
|
||||
(event is RawKeyDownEvent)
|
||||
? _controller.forwardPressed()
|
||||
: _controller.forwardReleased();
|
||||
break;
|
||||
case LogicalKeyboardKey.keyA:
|
||||
event is RawKeyDownEvent
|
||||
? _controller.strafeLeftPressed()
|
||||
: _controller.strafeLeftReleased();
|
||||
break;
|
||||
case LogicalKeyboardKey.keyS:
|
||||
event is RawKeyDownEvent
|
||||
? _controller.backPressed()
|
||||
: _controller.backReleased();
|
||||
break;
|
||||
case LogicalKeyboardKey.keyD:
|
||||
event is RawKeyDownEvent
|
||||
? _controller.strafeRightPressed()
|
||||
: _controller.strafeRightReleased();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||
_controller.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:thermion_dart/thermion_dart/entities/entity_transform_controller.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HardwareKeyboardPoll {
|
||||
final EntityTransformController _controller;
|
||||
late Timer _timer;
|
||||
HardwareKeyboardPoll(this._controller) {
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
||||
_controller.forwardPressed();
|
||||
} else {
|
||||
_controller.forwardReleased();
|
||||
}
|
||||
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
||||
_controller.backPressed();
|
||||
} else {
|
||||
_controller.backReleased();
|
||||
}
|
||||
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
||||
_controller.strafeLeftPressed();
|
||||
} else {
|
||||
_controller.strafeLeftReleased();
|
||||
}
|
||||
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
||||
_controller.strafeRightPressed();
|
||||
} else {
|
||||
_controller.strafeRightReleased();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart/entities/entity_transform_controller.dart';
|
||||
|
||||
///
|
||||
/// A widget that translates mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class EntityTransformMouseControllerWidget extends StatelessWidget {
|
||||
final EntityTransformController? transformController;
|
||||
final Widget? child;
|
||||
|
||||
EntityTransformMouseControllerWidget(
|
||||
{Key? key, required this.transformController, this.child})
|
||||
: super(key: key);
|
||||
|
||||
Timer? _timer;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (kPrimaryMouseButton & event.buttons != 0) {
|
||||
transformController?.mouse1Down();
|
||||
}
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
if (kPrimaryMouseButton & event.buttons != 0) {
|
||||
transformController?.mouse1Up();
|
||||
}
|
||||
},
|
||||
onPointerHover: (event) {
|
||||
_timer?.cancel();
|
||||
if (event.position.dx < 10) {
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 17), (_) {
|
||||
transformController?.look(-30);
|
||||
});
|
||||
} else if (event.position.dx > constraints.maxWidth - 10) {
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 17), (_) {
|
||||
transformController?.look(30);
|
||||
});
|
||||
} else {
|
||||
transformController?.look(event.delta.dx);
|
||||
}
|
||||
},
|
||||
child: child);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'thermion_gesture_detector_desktop.dart';
|
||||
import 'thermion_gesture_detector_mobile.dart';
|
||||
|
||||
enum GestureType { rotateCamera, panCamera, panBackground }
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class ThermionGestureDetector extends StatelessWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a ThermionWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
|
||||
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
|
||||
///
|
||||
final Widget? child;
|
||||
|
||||
///
|
||||
/// The [controller] attached to the [ThermionWidget] you wish to control.
|
||||
///
|
||||
final ThermionViewer controller;
|
||||
|
||||
///
|
||||
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
|
||||
/// 1) rotate or a pan (mobile only),
|
||||
/// 2) moving the camera or the background image (TODO).
|
||||
///
|
||||
final bool showControlOverlay;
|
||||
|
||||
///
|
||||
/// If false, gestures will not manipulate the active camera.
|
||||
///
|
||||
final bool enableCamera;
|
||||
|
||||
///
|
||||
/// If false, pointer down events will not trigger hit-testing (picking).
|
||||
///
|
||||
final bool enablePicking;
|
||||
|
||||
final void Function(ScaleStartDetails)? onScaleStart;
|
||||
final void Function(ScaleUpdateDetails)? onScaleUpdate;
|
||||
final void Function(ScaleEndDetails)? onScaleEnd;
|
||||
|
||||
const ThermionGestureDetector(
|
||||
{Key? key,
|
||||
required this.controller,
|
||||
this.child,
|
||||
this.showControlOverlay = false,
|
||||
this.enableCamera = true,
|
||||
this.enablePicking = true,
|
||||
this.onScaleStart,
|
||||
this.onScaleUpdate,
|
||||
this.onScaleEnd})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: controller.initialized,
|
||||
builder: (_, initialized) {
|
||||
if (initialized.data != true) {
|
||||
return child ?? Container();
|
||||
}
|
||||
if (kIsWeb || Platform.isLinux ||
|
||||
Platform.isWindows ||
|
||||
Platform.isMacOS) {
|
||||
return ThermionGestureDetectorDesktop(
|
||||
controller: controller,
|
||||
child: child,
|
||||
showControlOverlay: showControlOverlay,
|
||||
enableCamera: enableCamera,
|
||||
enablePicking: enablePicking,
|
||||
);
|
||||
} else {
|
||||
return ThermionGestureDetectorMobile(
|
||||
controller: controller,
|
||||
child: child,
|
||||
showControlOverlay: showControlOverlay,
|
||||
enableCamera: enableCamera,
|
||||
enablePicking: enablePicking,
|
||||
onScaleStart: onScaleStart,
|
||||
onScaleUpdate: onScaleUpdate,
|
||||
onScaleEnd: onScaleEnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class ThermionGestureDetectorDesktop extends StatefulWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a ThermionWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
|
||||
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
|
||||
///
|
||||
final Widget? child;
|
||||
|
||||
///
|
||||
/// The [controller] attached to the [ThermionWidget] you wish to control.
|
||||
///
|
||||
final ThermionViewer controller;
|
||||
|
||||
///
|
||||
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
|
||||
/// 1) rotate or a pan (mobile only),
|
||||
/// 2) moving the camera or the background image (TODO).
|
||||
///
|
||||
final bool showControlOverlay;
|
||||
|
||||
///
|
||||
/// If false, gestures will not manipulate the active camera.
|
||||
///
|
||||
final bool enableCamera;
|
||||
|
||||
///
|
||||
/// If false, pointer down events will not trigger hit-testing (picking).
|
||||
///
|
||||
final bool enablePicking;
|
||||
|
||||
const ThermionGestureDetectorDesktop(
|
||||
{Key? key,
|
||||
required this.controller,
|
||||
this.child,
|
||||
this.showControlOverlay = false,
|
||||
this.enableCamera = true,
|
||||
this.enablePicking = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ThermionGestureDetectorDesktopState();
|
||||
}
|
||||
|
||||
class _ThermionGestureDetectorDesktopState
|
||||
extends State<ThermionGestureDetectorDesktop> {
|
||||
final _logger = Logger("_ThermionGestureDetectorDesktopState");
|
||||
|
||||
///
|
||||
///
|
||||
// ignore: unused_field
|
||||
final bool _scaling = false;
|
||||
|
||||
bool _pointerMoving = false;
|
||||
|
||||
AbstractGizmo? _gizmo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
try {
|
||||
_gizmo = widget.controller.gizmo;
|
||||
} catch (err) {
|
||||
_logger.warning(
|
||||
"Failed to get gizmo. If you are running on WASM, this is expected");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ThermionGestureDetectorDesktop oldWidget) {
|
||||
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
|
||||
widget.enableCamera != oldWidget.enableCamera ||
|
||||
widget.enablePicking != oldWidget.enablePicking) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Timer? _scrollTimer;
|
||||
|
||||
///
|
||||
/// Scroll-wheel on desktop, interpreted as zoom
|
||||
///
|
||||
void _zoom(PointerScrollEvent pointerSignal) async {
|
||||
_scrollTimer?.cancel();
|
||||
await widget.controller.zoomBegin();
|
||||
await widget.controller.zoomUpdate(
|
||||
pointerSignal.localPosition.dx,
|
||||
pointerSignal.localPosition.dy,
|
||||
pointerSignal.scrollDelta.dy > 0 ? 1 : -1);
|
||||
|
||||
// we don't want to end the zoom in the same frame, because this will destroy the camera manipulator (and cancel the zoom update).
|
||||
// here, we just defer calling [zoomEnd] for 100ms to ensure the update is propagated through.
|
||||
_scrollTimer = Timer(const Duration(milliseconds: 100), () async {
|
||||
await widget.controller.zoomEnd();
|
||||
});
|
||||
}
|
||||
|
||||
Timer? _pickTimer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
// onPointerHover: (event) async {
|
||||
// if (_gizmo.isActive) {
|
||||
// return;
|
||||
// }
|
||||
// _pickTimer?.cancel();
|
||||
// _pickTimer = Timer(const Duration(milliseconds: 100), () async {
|
||||
// widget.controller
|
||||
// .pick(event.position.dx.toInt(), event.position.dy.toInt());
|
||||
// });
|
||||
// },
|
||||
onPointerSignal: (PointerSignalEvent pointerSignal) async {
|
||||
if (pointerSignal is PointerScrollEvent) {
|
||||
if (widget.enableCamera) {
|
||||
_zoom(pointerSignal);
|
||||
}
|
||||
} else {
|
||||
throw Exception("TODO");
|
||||
}
|
||||
},
|
||||
onPointerPanZoomStart: (pzs) {
|
||||
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
|
||||
},
|
||||
onPointerDown: (d) async {
|
||||
if (_gizmo?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
if (d.buttons != kTertiaryButton && widget.enablePicking) {
|
||||
widget.controller
|
||||
.pick(d.localPosition.dx.toInt(), d.localPosition.dy.toInt());
|
||||
}
|
||||
_pointerMoving = false;
|
||||
},
|
||||
// holding/moving the left mouse button is interpreted as a pan, middle mouse button as a rotate
|
||||
onPointerMove: (PointerMoveEvent d) async {
|
||||
if (_gizmo?.isActive == true) {
|
||||
_gizmo!.translate(d.delta.dx, d.delta.dy);
|
||||
return;
|
||||
}
|
||||
// if this is the first move event, we need to call rotateStart/panStart to set the first coordinates
|
||||
if (!_pointerMoving) {
|
||||
if (d.buttons == kTertiaryButton && widget.enableCamera) {
|
||||
widget.controller
|
||||
.rotateStart(d.localPosition.dx, d.localPosition.dy);
|
||||
} else if (widget.enableCamera) {
|
||||
widget.controller
|
||||
.panStart(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
}
|
||||
// set the _pointerMoving flag so we don't call rotateStart/panStart on future move events
|
||||
_pointerMoving = true;
|
||||
if (d.buttons == kTertiaryButton && widget.enableCamera) {
|
||||
widget.controller
|
||||
.rotateUpdate(d.localPosition.dx, d.localPosition.dy);
|
||||
} else if (widget.enableCamera) {
|
||||
widget.controller.panUpdate(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
},
|
||||
// when the left mouse button is released:
|
||||
// 1) if _pointerMoving is true, this completes the pan
|
||||
// 2) if _pointerMoving is false, this is interpreted as a pick
|
||||
// same applies to middle mouse button, but this is ignored as a pick
|
||||
onPointerUp: (PointerUpEvent d) async {
|
||||
if (_gizmo?.isActive == true) {
|
||||
_gizmo!.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.buttons == kTertiaryButton && widget.enableCamera) {
|
||||
widget.controller.rotateEnd();
|
||||
} else {
|
||||
if (_pointerMoving && widget.enableCamera) {
|
||||
widget.controller.panEnd();
|
||||
}
|
||||
}
|
||||
_pointerMoving = false;
|
||||
},
|
||||
child: widget.child);
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum GestureType { rotateCamera, panCamera, panBackground }
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class ThermionGestureDetectorMobile extends StatefulWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a ThermionWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
|
||||
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
|
||||
///
|
||||
final Widget? child;
|
||||
|
||||
///
|
||||
/// The [controller] attached to the [ThermionWidget] you wish to control.
|
||||
///
|
||||
final ThermionViewer controller;
|
||||
|
||||
///
|
||||
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
|
||||
/// 1) rotate or a pan (mobile only),
|
||||
/// 2) moving the camera or the background image (TODO).
|
||||
///
|
||||
final bool showControlOverlay;
|
||||
|
||||
///
|
||||
/// If false, gestures will not manipulate the active camera.
|
||||
///
|
||||
final bool enableCamera;
|
||||
|
||||
///
|
||||
/// If false, pointer down events will not trigger hit-testing (picking).
|
||||
///
|
||||
final bool enablePicking;
|
||||
|
||||
final double zoomDelta;
|
||||
|
||||
final void Function(ScaleStartDetails)? onScaleStart;
|
||||
final void Function(ScaleUpdateDetails)? onScaleUpdate;
|
||||
final void Function(ScaleEndDetails)? onScaleEnd;
|
||||
|
||||
const ThermionGestureDetectorMobile(
|
||||
{Key? key,
|
||||
required this.controller,
|
||||
this.child,
|
||||
this.showControlOverlay = false,
|
||||
this.enableCamera = true,
|
||||
this.enablePicking = true,
|
||||
this.onScaleStart,
|
||||
this.onScaleUpdate,
|
||||
this.onScaleEnd,
|
||||
this.zoomDelta = 1})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ThermionGestureDetectorMobileState();
|
||||
}
|
||||
|
||||
class _ThermionGestureDetectorMobileState
|
||||
extends State<ThermionGestureDetectorMobile> {
|
||||
GestureType gestureType = GestureType.panCamera;
|
||||
|
||||
final _icons = {
|
||||
GestureType.panBackground: Icons.image,
|
||||
GestureType.panCamera: Icons.pan_tool,
|
||||
GestureType.rotateCamera: Icons.rotate_90_degrees_ccw
|
||||
};
|
||||
|
||||
// on mobile, we can't differentiate between pointer down events like we do on desktop with primary/secondary/tertiary buttons
|
||||
// we allow the user to toggle between panning and rotating by double-tapping the widget
|
||||
bool _rotateOnPointerMove = false;
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
bool _scaling = false;
|
||||
|
||||
// to avoid duplicating code for pan/rotate (panStart, panUpdate, panEnd, rotateStart, rotateUpdate etc)
|
||||
// we have only a single function for start/update/end.
|
||||
// when the gesture type is changed, these properties are updated to point to the correct function.
|
||||
// ignore: unused_field
|
||||
late Function(double x, double y) _functionStart;
|
||||
// ignore: unused_field
|
||||
late Function(double x, double y) _functionUpdate;
|
||||
// ignore: unused_field
|
||||
late Function() _functionEnd;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_setFunction();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setFunction() {
|
||||
switch (gestureType) {
|
||||
case GestureType.rotateCamera:
|
||||
_functionStart = widget.controller.rotateStart;
|
||||
_functionUpdate = widget.controller.rotateUpdate;
|
||||
_functionEnd = widget.controller.rotateEnd;
|
||||
break;
|
||||
case GestureType.panCamera:
|
||||
_functionStart = widget.controller.panStart;
|
||||
_functionUpdate = widget.controller.panUpdate;
|
||||
_functionEnd = widget.controller.panEnd;
|
||||
break;
|
||||
// TODO
|
||||
case GestureType.panBackground:
|
||||
_functionStart = (x, y) async {};
|
||||
_functionUpdate = (x, y) async {};
|
||||
_functionEnd = () async {};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ThermionGestureDetectorMobile oldWidget) {
|
||||
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
|
||||
widget.enableCamera != oldWidget.enableCamera ||
|
||||
widget.enablePicking != oldWidget.enablePicking) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
// ignore: unused_field
|
||||
Timer? _scrollTimer;
|
||||
double _lastScale = 0;
|
||||
|
||||
// pinch zoom on mobile
|
||||
// couldn't find any equivalent for pointerCount in Listener (?) so we use a GestureDetector
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (d) {
|
||||
if (!widget.enablePicking) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.controller.pick(
|
||||
d.globalPosition.dx.toInt(), d.globalPosition.dy.toInt());
|
||||
},
|
||||
onDoubleTap: () {
|
||||
setState(() {
|
||||
_rotateOnPointerMove = !_rotateOnPointerMove;
|
||||
});
|
||||
},
|
||||
onScaleStart: (d) async {
|
||||
if (widget.onScaleStart != null) {
|
||||
widget.onScaleStart!.call(d);
|
||||
return;
|
||||
}
|
||||
if (d.pointerCount == 2 && widget.enableCamera) {
|
||||
_scaling = true;
|
||||
await widget.controller.zoomBegin();
|
||||
} else if (!_scaling && widget.enableCamera) {
|
||||
if (_rotateOnPointerMove) {
|
||||
widget.controller.rotateStart(
|
||||
d.localFocalPoint.dx, d.localFocalPoint.dy);
|
||||
} else {
|
||||
widget.controller
|
||||
.panStart(d.localFocalPoint.dx, d.localFocalPoint.dy);
|
||||
}
|
||||
}
|
||||
},
|
||||
onScaleUpdate: (ScaleUpdateDetails d) async {
|
||||
if (widget.onScaleUpdate != null) {
|
||||
widget.onScaleUpdate!.call(d);
|
||||
return;
|
||||
}
|
||||
if (d.pointerCount == 2 && widget.enableCamera) {
|
||||
if (d.horizontalScale != _lastScale) {
|
||||
widget.controller.zoomUpdate(
|
||||
d.localFocalPoint.dx,
|
||||
d.localFocalPoint.dy,
|
||||
d.horizontalScale > _lastScale ? 0.1 : -0.1);
|
||||
_lastScale = d.horizontalScale;
|
||||
}
|
||||
} else if (!_scaling && widget.enableCamera) {
|
||||
if (_rotateOnPointerMove) {
|
||||
widget.controller
|
||||
.rotateUpdate(d.focalPoint.dx, d.focalPoint.dy);
|
||||
} else {
|
||||
widget.controller
|
||||
.panUpdate(d.focalPoint.dx, d.focalPoint.dy);
|
||||
}
|
||||
}
|
||||
},
|
||||
onScaleEnd: (d) async {
|
||||
if (widget.onScaleEnd != null) {
|
||||
widget.onScaleEnd!.call(d);
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.pointerCount == 2 && widget.enableCamera) {
|
||||
widget.controller.zoomEnd();
|
||||
} else if (!_scaling && widget.enableCamera) {
|
||||
if (_rotateOnPointerMove) {
|
||||
widget.controller.rotateEnd();
|
||||
} else {
|
||||
widget.controller.panEnd();
|
||||
}
|
||||
}
|
||||
_scaling = false;
|
||||
},
|
||||
child: widget.child)),
|
||||
widget.showControlOverlay
|
||||
? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
var curIdx = GestureType.values.indexOf(gestureType);
|
||||
var nextIdx = curIdx == GestureType.values.length - 1
|
||||
? 0
|
||||
: curIdx + 1;
|
||||
gestureType = GestureType.values[nextIdx];
|
||||
_setFunction();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(50),
|
||||
child: Icon(_icons[gestureType], color: Colors.green)),
|
||||
))
|
||||
: Container()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_flutter/thermion/widgets/debug/child_renderable_widget.dart';
|
||||
import 'package:thermion_flutter/thermion/widgets/debug/skeleton_menu_item_widget.dart';
|
||||
|
||||
class EntityListWidget extends StatefulWidget {
|
||||
final ThermionViewer? controller;
|
||||
|
||||
const EntityListWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _EntityListWidget();
|
||||
}
|
||||
|
||||
class _EntityListWidget extends State<EntityListWidget> {
|
||||
@override
|
||||
void didUpdateWidget(EntityListWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Widget _entity(ThermionEntity entity) {
|
||||
return FutureBuilder(
|
||||
future: widget.controller!.getAnimationNames(entity),
|
||||
builder: (_, animations) {
|
||||
if (animations.data == null) {
|
||||
return Container();
|
||||
}
|
||||
final menuController = MenuController();
|
||||
return Row(children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.controller!.scene.select(entity);
|
||||
},
|
||||
child: Text(entity.toString(),
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
entity == widget.controller!.scene.selected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)))),
|
||||
MenuAnchor(
|
||||
controller: menuController,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
menuController.open();
|
||||
},
|
||||
)),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
child: const Text("Remove"),
|
||||
onPressed: () async {
|
||||
await widget.controller!.removeEntity(entity);
|
||||
}),
|
||||
MenuItemButton(
|
||||
child: const Text("Transform to unit cube"),
|
||||
onPressed: () async {
|
||||
await widget.controller!.transformToUnitCube(entity);
|
||||
}),
|
||||
SubmenuButton(
|
||||
child: const Text("Animations"),
|
||||
menuChildren: animations.data!
|
||||
.map((a) => SubmenuButton(
|
||||
child: Text(a),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
child: Text("Play"),
|
||||
onPressed: () async {
|
||||
await widget.controller!
|
||||
.addAnimationComponent(entity);
|
||||
widget.controller!.playAnimation(entity,
|
||||
animations.data!.indexOf(a));
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
child: Text("Loop"),
|
||||
onPressed: () async {
|
||||
await widget.controller!
|
||||
.addAnimationComponent(entity);
|
||||
widget.controller!.playAnimation(
|
||||
entity, animations.data!.indexOf(a),
|
||||
loop: true);
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
child: Text("Stop"),
|
||||
onPressed: () async {
|
||||
await widget.controller!
|
||||
.addAnimationComponent(entity);
|
||||
widget.controller!.stopAnimation(
|
||||
entity, animations.data!.indexOf(a));
|
||||
},
|
||||
)
|
||||
]))
|
||||
.toList()),
|
||||
ChildRenderableWidget(
|
||||
controller: widget.controller!, entity: entity),
|
||||
SkeletonMenuItemWidget(
|
||||
controller: widget.controller!, entity: entity)
|
||||
])
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _light(ThermionEntity entity) {
|
||||
final controller = MenuController();
|
||||
return Row(children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.controller!.scene.select(entity);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Text("Light $entity",
|
||||
style: TextStyle(
|
||||
fontWeight: entity == widget.controller!.scene.selected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)))),
|
||||
MenuAnchor(
|
||||
controller: controller,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
)),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
child: const Text("Remove"),
|
||||
onPressed: () async {
|
||||
await widget.controller!.removeLight(entity);
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.controller == null) {
|
||||
return Container(color: Colors.red);
|
||||
}
|
||||
return FutureBuilder(
|
||||
future: widget.controller!.initialized,
|
||||
builder: (_, snapshot) => snapshot.data != true
|
||||
? Container()
|
||||
: StreamBuilder(
|
||||
stream: widget.controller!.scene.onUpdated,
|
||||
builder: (_, __) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 30, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Colors.white.withOpacity(0.25),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
// reverse: true,
|
||||
children: widget.controller!.scene
|
||||
.listLights()
|
||||
.map(_light)
|
||||
.followedBy(widget.controller!.scene
|
||||
.listEntities()
|
||||
.map(_entity))
|
||||
.cast<Widget>()
|
||||
.toList())))));
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:thermion_dart/thermion_dart/utils/light_options.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
class LightSliderWidget extends StatefulWidget {
|
||||
final ThermionViewer controller;
|
||||
|
||||
final LightOptions options;
|
||||
final bool showControls;
|
||||
|
||||
LightSliderWidget(
|
||||
{super.key,
|
||||
required this.controller,
|
||||
this.showControls = false,
|
||||
required this.options});
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LightSliderWidgetState();
|
||||
}
|
||||
|
||||
class _LightSliderWidgetState extends State<LightSliderWidget> {
|
||||
ThermionEntity? _light;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_set();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future _set() async {
|
||||
await widget.controller.clearLights();
|
||||
|
||||
if (widget.options.iblPath != null) {
|
||||
_light = await widget.controller.loadIbl(widget.options.iblPath!,
|
||||
intensity: widget.options.iblIntensity);
|
||||
}
|
||||
|
||||
_light = await widget.controller.addLight(
|
||||
LightType.values[
|
||||
widget.options.directionalType],
|
||||
widget.options.directionalColor,
|
||||
widget.options.directionalIntensity,
|
||||
widget.options.directionalPosition.x,
|
||||
widget.options.directionalPosition.y,
|
||||
widget.options.directionalPosition.z,
|
||||
widget.options.directionalDirection.x,
|
||||
widget.options.directionalDirection.y,
|
||||
widget.options.directionalDirection.z,
|
||||
castShadows:widget.options.directionalCastShadows);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_light == null || !widget.showControls) {
|
||||
return Container();
|
||||
}
|
||||
return Theme(
|
||||
data: ThemeData(platform: TargetPlatform.android),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
|
||||
child: SliderTheme(
|
||||
data: const SliderThemeData(
|
||||
showValueIndicator: ShowValueIndicator.always,
|
||||
valueIndicatorTextStyle: TextStyle(color: Colors.black)),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text("Directional"),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label:
|
||||
"POSX ${widget.options.directionalPosition.x}",
|
||||
value: widget.options.directionalPosition.x,
|
||||
min: -10.0,
|
||||
max: 10.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalPosition.x = value;
|
||||
_set();
|
||||
})),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label:
|
||||
"POSY ${widget.options.directionalPosition.y}",
|
||||
value: widget.options.directionalPosition.y,
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalPosition.y = value;
|
||||
_set();
|
||||
})),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label:
|
||||
"POSZ ${widget.options.directionalPosition.z}",
|
||||
value: widget.options.directionalPosition.z,
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalPosition.z = value;
|
||||
_set();
|
||||
}))
|
||||
]),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label: "DIRX",
|
||||
value: widget.options.directionalDirection.x,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalDirection.x = value;
|
||||
_set();
|
||||
})),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label: "DIRY",
|
||||
value: widget.options.directionalDirection.y,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalDirection.y = value;
|
||||
_set();
|
||||
})),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label: "DIRZ",
|
||||
value: widget.options.directionalDirection.z,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalDirection.z = value;
|
||||
_set();
|
||||
}))
|
||||
]),
|
||||
Slider(
|
||||
label: "Color",
|
||||
value: widget.options.directionalColor,
|
||||
min: 0,
|
||||
max: 16000,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalColor = value;
|
||||
_set();
|
||||
}),
|
||||
Slider(
|
||||
label: "Intensity ${widget.options.directionalIntensity}",
|
||||
value: widget.options.directionalIntensity,
|
||||
min: 0,
|
||||
max: 1000000,
|
||||
onChanged: (value) {
|
||||
widget.options.directionalIntensity = value;
|
||||
_set();
|
||||
}),
|
||||
DropdownButton(
|
||||
onChanged: (v) {
|
||||
this.widget.options.directionalType = v;
|
||||
_set();
|
||||
},
|
||||
value: this.widget.options.directionalType,
|
||||
items: List<DropdownMenuItem>.generate(
|
||||
5,
|
||||
(idx) => DropdownMenuItem(
|
||||
value: idx,
|
||||
child: Text("$idx"),
|
||||
))),
|
||||
Row(children: [
|
||||
Text(
|
||||
"Shadows: ${this.widget.options.directionalCastShadows}"),
|
||||
Checkbox(
|
||||
value: widget.options.directionalCastShadows,
|
||||
onChanged: (v) {
|
||||
this.widget.options.directionalCastShadows = v!;
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Text("Indirect"),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
label: "Intensity ${widget.options.iblIntensity}",
|
||||
value: widget.options.iblIntensity,
|
||||
min: 0.0,
|
||||
max: 200000,
|
||||
onChanged: (value) {
|
||||
widget.options.iblIntensity = value;
|
||||
_set();
|
||||
})),
|
||||
])
|
||||
]))));
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'resize_observer.dart';
|
||||
|
||||
class ThermionWidget extends StatefulWidget {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
///
|
||||
/// The content to render before the texture widget is available.
|
||||
/// The default is a solid red Container, intentionally chosen to make it clear that there will be at least one frame where the Texture widget is not being rendered.
|
||||
///
|
||||
final Widget? initial;
|
||||
|
||||
const ThermionWidget({Key? key, this.initial, required this.viewer})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_ThermionWidgetState createState() => _ThermionWidgetState();
|
||||
}
|
||||
|
||||
class _ThermionWidgetState extends State<ThermionWidget> {
|
||||
ThermionFlutterTexture? _texture;
|
||||
|
||||
@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;
|
||||
var width = (dpr * size.width).ceil();
|
||||
var height = (dpr * size.height).ceil();
|
||||
_texture = await ThermionFlutterPlugin.createTexture(width, height, 0, 0);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resizeTexture(Size newSize) async {
|
||||
_resizeTimer?.cancel();
|
||||
_resizeTimer = Timer(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!,
|
||||
(dpr * newSize.width).ceil(), (dpr * newSize.height).ceil(), 0, 0);
|
||||
setState(() {});
|
||||
_resizing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_texture?.usesBackingWindow == true) {
|
||||
return Stack(children: [
|
||||
Positioned.fill(child: CustomPaint(painter: TransparencyPainter()))
|
||||
]);
|
||||
}
|
||||
|
||||
if (_texture == null || _resizing) {
|
||||
return widget.initial ??
|
||||
Container(color: kIsWeb ? Colors.transparent : 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)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class TransparencyPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint()
|
||||
..blendMode = BlendMode.clear
|
||||
..color = const Color(0x00000000),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
library thermion_flutter;
|
||||
|
||||
export 'thermion/thermion_flutter_plugin.dart';
|
||||
export 'thermion/widgets/thermion_widget.dart';
|
||||
export 'thermion/widgets/camera/gestures/thermion_gesture_detector.dart';
|
||||
|
||||
export 'src/thermion_flutter_plugin.dart';
|
||||
export 'src/widgets/widgets.dart';
|
||||
export 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
export 'package:thermion_dart/thermion_dart.dart';
|
||||
|
||||
@@ -5,8 +5,8 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
var registrar : FlutterPluginRegistrar
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionFlutterTexture?
|
||||
|
||||
var textures: [Int64: ThermionFlutterTexture] = [:]
|
||||
|
||||
var createdAt = Date()
|
||||
|
||||
var destroying = false
|
||||
@@ -55,14 +55,7 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(resourcesPtr!).takeUnretainedValue()
|
||||
instance.resources.removeValue(forKey:UInt32(rbuf.id))
|
||||
}
|
||||
|
||||
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
if(instance.texture != nil) {
|
||||
instance.registry.textureFrameAvailable(instance.texture!.flutterTextureId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let _messenger = registrar.messenger;
|
||||
@@ -79,6 +72,13 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
self.registry = textureRegistry;
|
||||
self.registrar = registrar
|
||||
}
|
||||
|
||||
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
for (_, texture) in instance.textures {
|
||||
instance.registry.textureFrameAvailable(texture.flutterTextureId)
|
||||
}
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let methodName = call.method;
|
||||
@@ -88,6 +88,10 @@ 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))
|
||||
@@ -99,27 +103,29 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
case "getSharedContext":
|
||||
result(nil)
|
||||
case "createTexture":
|
||||
if(destroying) {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
let args = call.arguments as! [Any]
|
||||
let width = args[0] as! Int64
|
||||
let height = args[1] as! Int64
|
||||
|
||||
self.texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
|
||||
let texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
|
||||
|
||||
if(self.texture!.texture.metalTextureAddress == -1) {
|
||||
if texture.texture.metalTextureAddress == -1 {
|
||||
result(nil)
|
||||
} else {
|
||||
result([self.texture!.flutterTextureId as Any, self.texture!.texture.metalTextureAddress, nil])
|
||||
textures[texture.flutterTextureId] = texture
|
||||
result([texture.flutterTextureId, texture.texture.metalTextureAddress, nil])
|
||||
}
|
||||
case "destroyTexture":
|
||||
self.destroying = true
|
||||
self.texture?.destroy()
|
||||
self.texture = nil
|
||||
result(true)
|
||||
self.destroying = false
|
||||
let args = call.arguments as! [Any]
|
||||
let flutterTextureId = args[0] as! Int64
|
||||
|
||||
if let texture = textures[flutterTextureId] {
|
||||
texture.destroy()
|
||||
textures.removeValue(forKey: flutterTextureId)
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ public class ThermionFlutterTexture : NSObject, FlutterTexture {
|
||||
|
||||
var flutterTextureId: Int64 = -1
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionDartTexture
|
||||
var texture: ThermionTextureSwift
|
||||
|
||||
init(registry:FlutterTextureRegistry, width:Int64, height:Int64) {
|
||||
self.registry = registry
|
||||
self.texture = ThermionDartTexture(width:width, height: height)
|
||||
self.texture = ThermionTextureSwift(width:width, height: height)
|
||||
super.init()
|
||||
self.flutterTextureId = registry.register(self)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class ThermionFlutterTexture : NSObject, FlutterTexture {
|
||||
}
|
||||
|
||||
public func onTextureUnregistered(_ texture:FlutterTexture) {
|
||||
print("Texture unregistered")
|
||||
|
||||
}
|
||||
|
||||
public func destroy() {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import Foundation
|
||||
import GLKit
|
||||
|
||||
@objc public class ThermionTextureSwift : NSObject {
|
||||
|
||||
public var pixelBuffer: CVPixelBuffer?
|
||||
|
||||
var pixelBufferAttrs = [
|
||||
kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_32ABGR ),
|
||||
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary
|
||||
] as [CFString : Any] as CFDictionary
|
||||
|
||||
@objc public var cvMetalTextureCache:CVMetalTextureCache?
|
||||
@objc public var metalDevice:MTLDevice?
|
||||
|
||||
@objc public var cvMetalTexture:CVMetalTexture?
|
||||
@objc public var metalTexture:MTLTexture?
|
||||
@objc public var metalTextureAddress:Int = -1
|
||||
|
||||
@objc override public init() {
|
||||
|
||||
}
|
||||
|
||||
@objc public init(width:Int64, height:Int64) {
|
||||
if(self.metalDevice == nil) {
|
||||
self.metalDevice = MTLCreateSystemDefaultDevice()!
|
||||
}
|
||||
|
||||
// create pixel buffer
|
||||
if(CVPixelBufferCreate(kCFAllocatorDefault, Int(width), Int(height),
|
||||
kCVPixelFormatType_32BGRA, pixelBufferAttrs, &pixelBuffer) != kCVReturnSuccess) {
|
||||
print("Error allocating pixel buffer")
|
||||
metalTextureAddress = -1;
|
||||
return
|
||||
}
|
||||
if self.cvMetalTextureCache == nil {
|
||||
let cacheCreationResult = CVMetalTextureCacheCreate(
|
||||
kCFAllocatorDefault,
|
||||
nil,
|
||||
self.metalDevice!,
|
||||
nil,
|
||||
&self.cvMetalTextureCache)
|
||||
if(cacheCreationResult != kCVReturnSuccess) {
|
||||
print("Error creating Metal texture cache")
|
||||
metalTextureAddress = -1
|
||||
return
|
||||
}
|
||||
}
|
||||
let cvret = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault,
|
||||
self.cvMetalTextureCache!,
|
||||
pixelBuffer!, nil,
|
||||
MTLPixelFormat.bgra8Unorm,
|
||||
Int(width), Int(height),
|
||||
0,
|
||||
&cvMetalTexture)
|
||||
if(cvret != kCVReturnSuccess) {
|
||||
print("Error creating texture from image")
|
||||
metalTextureAddress = -1
|
||||
return
|
||||
}
|
||||
metalTexture = CVMetalTextureGetTexture(cvMetalTexture!)
|
||||
let metalTexturePtr = Unmanaged.passRetained(metalTexture!).toOpaque()
|
||||
metalTextureAddress = Int(bitPattern:metalTexturePtr)
|
||||
}
|
||||
|
||||
@objc public func destroyTexture() {
|
||||
CVMetalTextureCacheFlush(self.cvMetalTextureCache!, 0)
|
||||
self.metalTexture = nil
|
||||
self.cvMetalTexture = nil
|
||||
self.pixelBuffer = nil
|
||||
self.metalDevice = nil
|
||||
self.cvMetalTextureCache = nil
|
||||
}
|
||||
|
||||
@objc public func fillColor() {
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let bufferWidth = Int(CVPixelBufferGetWidth(pixelBuffer!))
|
||||
let bufferHeight = Int(CVPixelBufferGetHeight(pixelBuffer!))
|
||||
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer!)
|
||||
|
||||
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer!) else {
|
||||
return
|
||||
}
|
||||
|
||||
for row in 0..<bufferHeight {
|
||||
var pixel = baseAddress + row * bytesPerRow
|
||||
for col in 0..<bufferWidth {
|
||||
let blue = pixel
|
||||
blue.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
let red = pixel + 1
|
||||
red.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
let green = pixel + 2
|
||||
green.storeBytes(of: 0, as: UInt8.self)
|
||||
|
||||
let alpha = pixel + 3
|
||||
alpha.storeBytes(of: 255, as: UInt8.self)
|
||||
|
||||
pixel += 4;
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
}
|
||||
@objc public func getTextureBytes() -> NSData? {
|
||||
guard let texture = self.metalTexture else {
|
||||
print("Metal texture is not available")
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = texture.width
|
||||
let height = texture.height
|
||||
let bytesPerPixel = 4 // RGBA
|
||||
let bytesPerRow = width * bytesPerPixel
|
||||
let byteCount = bytesPerRow * height
|
||||
|
||||
var bytes = [UInt8](repeating: 0, count: byteCount)
|
||||
let region = MTLRegionMake2D(0, 0, width, height)
|
||||
texture.getBytes(&bytes, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)
|
||||
|
||||
// Swizzle bytes from BGRA to RGBA
|
||||
for i in stride(from: 0, to: byteCount, by: 4) {
|
||||
let blue = bytes[i]
|
||||
let green = bytes[i + 1]
|
||||
let red = bytes[i + 2]
|
||||
let alpha = bytes[i + 3]
|
||||
|
||||
bytes[i] = red
|
||||
bytes[i + 1] = green
|
||||
bytes[i + 2] = blue
|
||||
bytes[i + 3] = alpha
|
||||
}
|
||||
|
||||
// Convert Swift Data to Objective-C NSData
|
||||
let nsData = Data(bytes: &bytes, count: byteCount) as NSData
|
||||
return nsData
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: thermion_flutter
|
||||
description: Flutter plugin for 3D rendering with the Thermion toolkit.
|
||||
version: 0.1.1+10
|
||||
version: 0.2.1-dev.7
|
||||
homepage: https://thermion.dev
|
||||
repository: https://github.com/nmfisher/thermion
|
||||
|
||||
@@ -16,13 +16,20 @@ dependencies:
|
||||
vector_math: ^2.1.2
|
||||
plugin_platform_interface: ^2.0.0
|
||||
ffi: ^2.1.2
|
||||
animation_tools_dart: ^0.0.4
|
||||
thermion_dart: ^0.1.1+5
|
||||
thermion_flutter_platform_interface: ^0.1.0+9
|
||||
thermion_flutter_ffi: ^0.1.0+9
|
||||
thermion_flutter_web: ^0.0.1+9
|
||||
animation_tools_dart: ^0.1.0
|
||||
thermion_dart: ^0.2.1-dev.0.0.8
|
||||
thermion_flutter_platform_interface: ^0.2.1-dev.7
|
||||
thermion_flutter_ffi: ^0.2.1-dev.7
|
||||
thermion_flutter_web: ^0.1.0+9
|
||||
logging: ^1.2.0
|
||||
|
||||
web: ^1.0.0
|
||||
dependency_overrides:
|
||||
thermion_dart:
|
||||
path: ../../thermion_dart
|
||||
thermion_flutter_platform_interface:
|
||||
path: ../thermion_flutter_platform_interface
|
||||
thermion_flutter_ffi:
|
||||
path: ../thermion_flutter_ffi
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@@ -33,7 +40,7 @@ flutter:
|
||||
platforms:
|
||||
android:
|
||||
pluginClass: ThermionFlutterPlugin
|
||||
package: app.polyvox.filament
|
||||
package: dev.thermion.android
|
||||
ios:
|
||||
pluginClass: SwiftThermionFlutterPlugin
|
||||
macos:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "ResourceBuffer.hpp"
|
||||
|
||||
using namespace filament;
|
||||
using namespace thermion_filament;
|
||||
using namespace thermion;
|
||||
using namespace std;
|
||||
|
||||
int _i = 0;
|
||||
|
||||
@@ -152,8 +152,20 @@ LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
||||
break;
|
||||
}
|
||||
case WM_ERASEBKGND: {
|
||||
// Prevent erasing of |window| when it is unfocused and minimized or
|
||||
// moved out of screen etc.
|
||||
HDC hdc = (HDC)wparam;
|
||||
RECT rect;
|
||||
GetClientRect(window, &rect);
|
||||
|
||||
// Get the BackingWindow instance associated with this window
|
||||
BackingWindow* backing_window = reinterpret_cast<BackingWindow*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
|
||||
if (backing_window) {
|
||||
HBRUSH brush = CreateSolidBrush(RGB(0, 255, 0));
|
||||
FillRect(hdc, &rect, brush);
|
||||
DeleteObject(brush);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case WM_SIZE:
|
||||
@@ -346,8 +358,6 @@ void BackingWindow::Resize(int width, int height, int left, int top) {
|
||||
_top = top;
|
||||
RECT flutterViewRect;
|
||||
::GetWindowRect(_flutterViewWindow, &flutterViewRect);
|
||||
std::cout << "Resizing to " << _width << " x " << _height << " with LT" << _left << " " << _top << " flutter view rect" << flutterViewRect.left << " " << flutterViewRect.top << " " << flutterViewRect.right << " " << flutterViewRect.bottom << std::endl;
|
||||
|
||||
::SetWindowPos(_windowHandle, _flutterRootWindow, flutterViewRect.left + _left,
|
||||
flutterViewRect.top + _top, _width, _height,
|
||||
SWP_NOACTIVATE);
|
||||
|
||||
@@ -172,8 +172,6 @@ void ThermionFlutterPlugin::CreateTexture(
|
||||
auto height = (uint32_t)round(dHeight );
|
||||
auto left = (uint32_t)round(dLeft );
|
||||
auto top = (uint32_t)round(dTop );
|
||||
|
||||
std::cout << "Using " << width << "x" << height << std::endl;
|
||||
|
||||
// create a single shared context for the life of the application
|
||||
// this will be used to create a backing texture and passed to Filament
|
||||
@@ -182,8 +180,10 @@ void ThermionFlutterPlugin::CreateTexture(
|
||||
_context = std::make_unique<FlutterEGLContext>(_pluginRegistrar, _textureRegistrar);
|
||||
#else
|
||||
_context = std::make_unique<WGLContext>(_pluginRegistrar, _textureRegistrar);
|
||||
std::cout << "Created WGL context" << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
_context->CreateRenderingSurface(width, height, std::move(result), left, top);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ void ThermionFlutterPlugin::DestroyTexture(
|
||||
void ThermionFlutterPlugin::HandleMethodCall(
|
||||
const flutter::MethodCall<flutter::EncodableValue> &methodCall,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
|
||||
// std::cout << methodCall.method_name().c_str() << std::endl;
|
||||
if (methodCall.method_name() == "usesBackingWindow") {
|
||||
result->Success(flutter::EncodableValue(
|
||||
#ifdef WGL_USE_BACKING_WINDOW
|
||||
@@ -239,18 +239,19 @@ void ThermionFlutterPlugin::HandleMethodCall(
|
||||
int dHeight = *(std::get_if<int>(&(args->at(1))));
|
||||
int dLeft = *(std::get_if<int>(&(args->at(2))));
|
||||
int dTop = *(std::get_if<int>(&(args->at(3))));
|
||||
auto width = (uint32_t)round(dWidth );
|
||||
auto height = (uint32_t)round(dHeight );
|
||||
auto left = (uint32_t)round(dLeft );
|
||||
auto top = (uint32_t)round(dTop );
|
||||
auto width = static_cast<uint32_t>(dWidth);
|
||||
auto height = static_cast<uint32_t>(dHeight);
|
||||
auto left = static_cast<uint32_t>(dLeft);
|
||||
auto top = static_cast<uint32_t>(dTop );
|
||||
|
||||
_context->ResizeRenderingSurface(width, height, left, top);
|
||||
result->Success();
|
||||
#else
|
||||
result->Error("ERROR", "resizeWindow is only available when using a backing window");
|
||||
#endif
|
||||
} else if (methodCall.method_name() == "createTexture") {
|
||||
} else if (methodCall.method_name() == "createWindow") {
|
||||
CreateTexture(methodCall, std::move(result));
|
||||
} else if (methodCall.method_name() == "destroyTexture") {
|
||||
} else if (methodCall.method_name() == "destroyWindow") {
|
||||
DestroyTexture(methodCall, std::move(result));
|
||||
} else if (methodCall.method_name() == "getRenderCallback") {
|
||||
flutter::EncodableList resultList;
|
||||
|
||||
@@ -45,7 +45,7 @@ WGLContext::WGLContext(flutter::PluginRegistrarWindows *pluginRegistrar,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
16, // Number of bits for the depthbuffer
|
||||
24, // Number of bits for the depthbuffer
|
||||
0, // Number of bits for the stencilbuffer
|
||||
0, // Number of Aux buffers in the framebuffer.
|
||||
PFD_MAIN_PLANE,
|
||||
@@ -117,6 +117,8 @@ void WGLContext::CreateRenderingSurface(
|
||||
} else {
|
||||
ResizeRenderingSurface(width, height, left, top);
|
||||
}
|
||||
|
||||
// std::cout << "created window size " << width << "x" << height << " at " << left << "," << top << " with backing handle" << _backingWindow->GetHandle() << std::endl;
|
||||
std::vector<flutter::EncodableValue> resultList;
|
||||
resultList.push_back(flutter::EncodableValue()); // return null for Flutter texture ID
|
||||
resultList.push_back(flutter::EncodableValue()); // return null for hardware texture ID
|
||||
|
||||
@@ -1,3 +1,92 @@
|
||||
## 0.2.1-dev.7
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.6
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.5
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.4
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0.0.0
|
||||
|
||||
- y
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
|
||||
|
||||
## 0.2.0-dev.8.0.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **REFACTOR**: continual refactor to support multiple render targets.
|
||||
- **FEAT**: support multiple ThermionWidget on Android.
|
||||
- **FEAT**: use imported texture on iOS.
|
||||
- **FEAT**: working implementation of multiple widgets on macos.
|
||||
- **BREAKING** **REFACTOR**: refactor to support multiple Views/Render Targets.
|
||||
- **BREAKING** **FEAT**: big refactor to support multiple swapchains.
|
||||
|
||||
## 0.2.0-dev.7.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.6.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.5.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.4.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.1.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FEAT**: (flutter) move DPR calculation to resizeTexture and add createViewerWithOptions method to ThermionFlutterFFI.
|
||||
- **BREAKING** **FIX**: (flutter) pass pixelRatio to createTexture.
|
||||
|
||||
## 0.1.0+12
|
||||
|
||||
- **FIX**: add logging dependency.
|
||||
- **FIX**: web/JS bool checks need to compare to int.
|
||||
- **FIX**: add logging dependency.
|
||||
- **FIX**: web/JS bool checks need to compare to int.
|
||||
|
||||
## 0.1.0+11
|
||||
|
||||
- **FIX**: add logging dependency.
|
||||
|
||||
## 0.1.0+10
|
||||
|
||||
- **FIX**: web/JS bool checks need to compare to int.
|
||||
|
||||
## 0.1.0+9
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
122
thermion_flutter/thermion_flutter_ffi/lib/platform_texture.dart
Normal file
122
thermion_flutter/thermion_flutter_ffi/lib/platform_texture.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'thermion_flutter_method_channel_interface.dart';
|
||||
|
||||
class FlutterPlatformTexture extends MethodChannelFlutterTexture {
|
||||
final _logger = Logger("ThermionFlutterTexture");
|
||||
|
||||
final ThermionViewer viewer;
|
||||
final View view;
|
||||
|
||||
int flutterId = -1;
|
||||
int _lastFlutterId = -1;
|
||||
int _lastHardwareId = -1;
|
||||
int hardwareId = -1;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
|
||||
SwapChain? swapChain;
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
|
||||
late bool destroySwapChainOnResize;
|
||||
|
||||
bool destroyed = false;
|
||||
|
||||
FlutterPlatformTexture(
|
||||
super.channel, this.viewer, this.view, this.swapChain) {
|
||||
if (swapChain == null) {
|
||||
destroySwapChainOnResize = true;
|
||||
} else {
|
||||
destroySwapChainOnResize = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resize(
|
||||
int newWidth, int newHeight, int newLeft, int newTop) async {
|
||||
_logger.info(
|
||||
"Resizing texture to $newWidth x $newHeight (offset $newLeft, $newTop)");
|
||||
if (newWidth == this.width &&
|
||||
newHeight == this.height &&
|
||||
newLeft == 0 &&
|
||||
newTop == 0) {
|
||||
_logger.info("Existing texture matches requested dimensions");
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
var result =
|
||||
await channel.invokeMethod("createTexture", [width, height, 0, 0]);
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
_lastFlutterId = flutterId;
|
||||
_lastHardwareId = hardwareId;
|
||||
flutterId = result[0] as int;
|
||||
hardwareId = result[1] as int;
|
||||
|
||||
_logger.info("Created texture ${flutterId} / ${hardwareId}");
|
||||
|
||||
if (destroySwapChainOnResize) {
|
||||
if (swapChain != null) {
|
||||
await viewer.destroySwapChain(swapChain!);
|
||||
}
|
||||
swapChain = await viewer.createSwapChain(result[2]);
|
||||
await view.setRenderable(true, swapChain!);
|
||||
} else if (hardwareId != _lastHardwareId) {
|
||||
if (_renderTarget != null) {
|
||||
await viewer.destroyRenderTarget(_renderTarget!);
|
||||
}
|
||||
_renderTarget =
|
||||
await viewer.createRenderTarget(width, height, hardwareId);
|
||||
await view.setRenderTarget(_renderTarget!);
|
||||
await view.setRenderable(true, swapChain!);
|
||||
if (_lastFlutterId != -1 && _lastHardwareId != -1) {
|
||||
await _destroyTexture(_lastFlutterId, _lastHardwareId);
|
||||
_lastFlutterId = -1;
|
||||
_lastHardwareId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _destroyTexture(
|
||||
int flutterTextureId, int hardwareTextureId) async {
|
||||
try {
|
||||
await channel.invokeMethod(
|
||||
"destroyTexture", [flutterTextureId, hardwareTextureId]);
|
||||
_logger.info("Destroyed texture $flutterTextureId / $hardwareTextureId");
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to destroy texture: $e");
|
||||
}
|
||||
}
|
||||
|
||||
bool destroying = false;
|
||||
Future destroy() async {
|
||||
if (destroyed || destroying) {
|
||||
return;
|
||||
}
|
||||
destroying = true;
|
||||
await view.setRenderTarget(null);
|
||||
if (_renderTarget != null) {
|
||||
await viewer.destroyRenderTarget(_renderTarget!);
|
||||
_renderTarget = null;
|
||||
}
|
||||
|
||||
if (destroySwapChainOnResize && swapChain != null) {
|
||||
await viewer.destroySwapChain(swapChain!);
|
||||
swapChain = null;
|
||||
}
|
||||
await _destroyTexture(flutterId, hardwareId);
|
||||
flutterId = -1;
|
||||
hardwareId = -1;
|
||||
destroying = false;
|
||||
destroyed = true;
|
||||
}
|
||||
|
||||
Future markFrameAvailable() async {
|
||||
await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class TextureCacheEntry {
|
||||
final int flutterId;
|
||||
final int hardwareId;
|
||||
final DateTime creationTime;
|
||||
DateTime? removalTime;
|
||||
bool inUse;
|
||||
|
||||
TextureCacheEntry(this.flutterId, this.hardwareId,
|
||||
{this.removalTime, this.inUse = true})
|
||||
: creationTime = DateTime.now();
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart/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._() {}
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterFFI._();
|
||||
}
|
||||
|
||||
final _textures = <ThermionFlutterTexture>{};
|
||||
|
||||
Future<ThermionViewer> createViewer({String? uberArchivePath}) async {
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await _channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
|
||||
var renderCallback =
|
||||
Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
|
||||
renderCallbackResult[0]);
|
||||
var renderCallbackOwner =
|
||||
Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
||||
|
||||
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
|
||||
var sharedContext = await _channel.invokeMethod("getSharedContext");
|
||||
|
||||
var sharedContextPtr = sharedContext == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(sharedContext);
|
||||
|
||||
_viewer = ThermionViewerFFI(
|
||||
resourceLoader: resourceLoader,
|
||||
renderCallback: renderCallback,
|
||||
renderCallbackOwner: renderCallbackOwner,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: uberArchivePath);
|
||||
await _viewer!.initialized;
|
||||
return _viewer!;
|
||||
}
|
||||
|
||||
bool _creatingTexture = false;
|
||||
bool _destroyingTexture = false;
|
||||
|
||||
Future _waitForTextureCreationToComplete() async {
|
||||
var iter = 0;
|
||||
|
||||
while (_creatingTexture || _destroyingTexture) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
iter++;
|
||||
if (iter > 10) {
|
||||
throw Exception(
|
||||
"Previous call to createTexture failed to complete within 500ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Create a backing surface for rendering.
|
||||
/// This is called by [ThermionWidget]; don't call this yourself.
|
||||
///
|
||||
/// The name here is slightly misleading because we only create
|
||||
/// a texture render target on macOS and iOS; on Android, we render into
|
||||
/// a native window derived from a Surface, and on Windows we render into
|
||||
/// a HWND.
|
||||
///
|
||||
/// Currently, this only supports a single "texture" (aka rendering surface)
|
||||
/// at any given time. If a [ThermionWidget] is disposed, it will call
|
||||
/// [destroyTexture]; if it is resized, it will call [resizeTexture].
|
||||
///
|
||||
/// In future, we probably want to be able to create multiple distinct
|
||||
/// textures/render targets. This would make it possible to have multiple
|
||||
/// Flutter Texture widgets, each with its own Filament View attached.
|
||||
/// The current design doesn't accommodate this (for example, it seems we can
|
||||
/// only create a single native window from a Surface at any one time).
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
// 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 == height && _textures.first.width == width) {
|
||||
return _textures.first;
|
||||
} else {
|
||||
await _viewer!.setRendering(false);
|
||||
await _viewer!.destroySwapChain();
|
||||
await destroyTexture(_textures.first);
|
||||
_textures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
_creatingTexture = true;
|
||||
|
||||
var result = await _channel
|
||||
.invokeMethod("createTexture", [width, height, 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 = (width.toDouble(), height.toDouble());
|
||||
|
||||
final texture = ThermionFlutterTexture(
|
||||
flutterTextureId, hardwareTextureId, width, height, surfaceAddress);
|
||||
|
||||
await _viewer?.createSwapChain(width.toDouble(), height.toDouble(),
|
||||
surface: texture.surfaceAddress == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(texture.surfaceAddress!));
|
||||
|
||||
if (texture.hardwareTextureId != null) {
|
||||
// ignore: unused_local_variable
|
||||
var renderTarget = await _viewer?.createRenderTarget(
|
||||
width.toDouble(), height.toDouble(), texture.hardwareTextureId!);
|
||||
}
|
||||
|
||||
await _viewer?.updateViewportAndCameraProjection(
|
||||
width.toDouble(), height.toDouble());
|
||||
_viewer?.render();
|
||||
_creatingTexture = false;
|
||||
|
||||
_textures.add(texture);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
///
|
||||
/// Destroy a texture and clean up the texture cache (if applicable).
|
||||
///
|
||||
Future destroyTexture(ThermionFlutterTexture texture) async {
|
||||
if (_creatingTexture || _destroyingTexture) {
|
||||
throw Exception(
|
||||
"Cannot destroy texture while concurrent call to createTexture/destroyTexture has not completed");
|
||||
}
|
||||
_destroyingTexture = true;
|
||||
_textures.remove(texture);
|
||||
await _channel.invokeMethod("destroyTexture", texture.flutterTextureId);
|
||||
_destroyingTexture = false;
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
if (_resizing) {
|
||||
throw Exception("Resize underway");
|
||||
}
|
||||
|
||||
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 _viewer!.destroySwapChain();
|
||||
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.toDouble(), height.toDouble(),
|
||||
surface: newTexture.surfaceAddress == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(newTexture.surfaceAddress!));
|
||||
|
||||
if (newTexture.hardwareTextureId != null) {
|
||||
// ignore: unused_local_variable
|
||||
var renderTarget = await _viewer!.createRenderTarget(
|
||||
width.toDouble(), height.toDouble(), 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ffi';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
|
||||
///
|
||||
/// An abstract implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
abstract class ThermionFlutterMethodChannelInterface
|
||||
extends ThermionFlutterPlatform {
|
||||
final channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterMethodChannelInterface");
|
||||
|
||||
ThermionViewerFFI? viewer;
|
||||
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if (viewer != null) {
|
||||
throw Exception(
|
||||
"Only one ThermionViewer can be created at any given time; ensure you have called [dispose] on the previous instance before constructing a new instance.");
|
||||
}
|
||||
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
var driverPlatform = await channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
|
||||
var sharedContext = await channel.invokeMethod("getSharedContext");
|
||||
|
||||
var sharedContextPtr = sharedContext == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(sharedContext);
|
||||
|
||||
viewer = ThermionViewerFFI(
|
||||
resourceLoader: resourceLoader,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: options?.uberarchivePath);
|
||||
await viewer!.initialized;
|
||||
|
||||
viewer!.onDispose(() async {
|
||||
this.viewer = null;
|
||||
});
|
||||
|
||||
return viewer!;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
|
||||
final MethodChannel channel;
|
||||
|
||||
MethodChannelFlutterTexture(this.channel);
|
||||
|
||||
@override
|
||||
int get flutterId;
|
||||
|
||||
@override
|
||||
int get hardwareId;
|
||||
|
||||
@override
|
||||
int get height;
|
||||
|
||||
@override
|
||||
int get width;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart' as t;
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.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';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
|
||||
|
||||
import 'platform_texture.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and a texture that will be used as a render target
|
||||
/// for a headless swapchain.
|
||||
///
|
||||
class ThermionFlutterTextureBackedPlatform
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _logger = Logger("ThermionFlutterTextureBackedPlatform");
|
||||
|
||||
static SwapChain? _swapChain;
|
||||
|
||||
ThermionFlutterTextureBackedPlatform._();
|
||||
|
||||
static ThermionFlutterTextureBackedPlatform? instance;
|
||||
|
||||
static void registerWith() {
|
||||
instance ??= ThermionFlutterTextureBackedPlatform._();
|
||||
ThermionFlutterPlatform.instance = instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
var viewer = await super.createViewer(options: options);
|
||||
if (_swapChain != null) {
|
||||
throw Exception("Only a single swapchain can be created");
|
||||
}
|
||||
|
||||
// this implementation renders directly into a texture/render target
|
||||
// we still need to create a (headless) swapchain, but the actual dimensions
|
||||
// don't matter
|
||||
if (Platform.isMacOS || Platform.isIOS) {
|
||||
_swapChain = await viewer.createHeadlessSwapChain(1, 1);
|
||||
}
|
||||
|
||||
viewer.onDispose(() async {
|
||||
_swapChain = null;
|
||||
});
|
||||
|
||||
return viewer;
|
||||
}
|
||||
|
||||
// On desktop platforms, textures are always created
|
||||
Future<ThermionFlutterTexture?> createTexture(
|
||||
t.View view, int width, int height) async {
|
||||
var texture = FlutterPlatformTexture(channel, viewer!, view,
|
||||
(Platform.isMacOS || Platform.isIOS) ? _swapChain : null);
|
||||
await texture.resize(width, height, 0, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionFlutterWindow> createWindow(
|
||||
int width, int height, int offsetLeft, int offsetTop) {
|
||||
// TODO: implement createWindow
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
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';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
|
||||
|
||||
///
|
||||
/// A Windows-only implementation of [ThermionFlutterPlatform] that uses
|
||||
/// a Flutter platform channel to create a rendering context,
|
||||
/// resource loader and a native HWND that will be sit behind the running
|
||||
/// Flutter application.
|
||||
///
|
||||
class ThermionFlutterWindows
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
|
||||
final _logger = Logger("ThermionFlutterWindows");
|
||||
|
||||
ThermionViewer? _viewer;
|
||||
|
||||
SwapChain? _swapChain;
|
||||
|
||||
ThermionFlutterWindows._() {}
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterWindows._();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if(_viewer != null) {
|
||||
throw Exception("Only one viewer should be instantiated over the life of the app");
|
||||
}
|
||||
_viewer = await super.createViewer(options: options);
|
||||
return _viewer!;
|
||||
}
|
||||
|
||||
///
|
||||
/// Not supported on Windows. Throws an exception.
|
||||
///
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> createTexture(View view, int width, int height) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<ThermionFlutterWindow> createWindow(int width, int height, int offsetLeft, int offsetTop) async {
|
||||
|
||||
|
||||
var result = await _channel
|
||||
.invokeMethod("createWindow", [width, height, offsetLeft, offsetLeft]);
|
||||
|
||||
if (result == null || result[2] == -1) {
|
||||
throw Exception("Failed to create window");
|
||||
}
|
||||
|
||||
var window =
|
||||
ThermionFlutterWindowImpl(result[2], _channel, viewer!);
|
||||
await window.resize(width, height, offsetLeft, offsetTop);
|
||||
var view = await _viewer!.getViewAt(0);
|
||||
|
||||
await view.updateViewport(width, height);
|
||||
_swapChain = await _viewer!.createSwapChain(window.handle);
|
||||
await view.setRenderable(true, _swapChain!);
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class ThermionFlutterWindowImpl extends ThermionFlutterWindow {
|
||||
|
||||
final ThermionViewer viewer;
|
||||
final int handle;
|
||||
int height = 0;
|
||||
int width = 0;
|
||||
int offsetLeft = 0;
|
||||
int offsetTop = 0;
|
||||
final MethodChannel _channel;
|
||||
|
||||
|
||||
|
||||
ThermionFlutterWindowImpl(this.handle, this._channel, this.viewer);
|
||||
|
||||
@override
|
||||
Future destroy() async {
|
||||
await _channel
|
||||
.invokeMethod("destroyWindow", this.handle);
|
||||
}
|
||||
|
||||
@override
|
||||
Future markFrameAvailable() {
|
||||
// TODO: implement markFrameAvailable
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize the window. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future resize(
|
||||
int width, int height, int offsetLeft, int offsetTop) async {
|
||||
if (_resizing) {
|
||||
throw Exception("Resize underway");
|
||||
}
|
||||
|
||||
if (width == this.width && height == this.height && this.offsetLeft == offsetLeft && this.offsetTop == offsetTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.offsetLeft = offsetLeft;
|
||||
this.offsetTop = offsetTop;
|
||||
|
||||
_resizing = true;
|
||||
|
||||
await _channel
|
||||
.invokeMethod("resizeWindow", [width, height, offsetLeft, offsetTop]);
|
||||
_resizing = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: thermion_flutter_ffi
|
||||
description: An FFI interface for the thermion_flutter plugin (all platforms except web).
|
||||
repository: https://github.com/nmfisher/thermion_flutter/thermion_flutter
|
||||
version: 0.1.0+9
|
||||
version: 0.2.1-dev.7
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
@@ -11,21 +11,28 @@ flutter:
|
||||
implements: thermion_flutter_platform_interface
|
||||
platforms:
|
||||
ios:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
android:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
macos:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
windows:
|
||||
dartPluginClass: ThermionFlutterFFI
|
||||
dartPluginClass: ThermionFlutterWindows
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.1.0
|
||||
thermion_flutter_platform_interface: ^0.1.0+9
|
||||
thermion_dart: ^0.1.1+5
|
||||
thermion_flutter_platform_interface: ^0.2.1-dev.7
|
||||
thermion_dart: ^0.2.1-dev.0.0.8
|
||||
logging: ^1.2.0
|
||||
|
||||
dependency_overrides:
|
||||
thermion_dart:
|
||||
path: ../../thermion_dart
|
||||
thermion_flutter_platform_interface:
|
||||
path: ../thermion_flutter_platform_interface
|
||||
dev_dependencies:
|
||||
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mockito: ^5.0.0
|
||||
|
||||
@@ -1,3 +1,81 @@
|
||||
## 0.2.1-dev.7
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.6
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.5
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.4
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.1-dev.0.0.0
|
||||
|
||||
- y
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
|
||||
|
||||
## 0.2.0-dev.8.0.0
|
||||
|
||||
- **REFACTOR**: continual refactor to support multiple render targets.
|
||||
- **FEAT**: support multiple ThermionWidget on Android.
|
||||
- **FEAT**: working implementation of multiple widgets on macos.
|
||||
|
||||
## 0.2.0-dev.7.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.6.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.5.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.4.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.2.0-dev.1.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FEAT**: add createViewerWithOptions to ThermionFlutterPlugin and mark createViewer as deprecated.
|
||||
- **FEAT**: add ThermionFlutterOptions classes, rename interface parameter for offsetTop and ensure pixelRatio is passed to resizeTexture.
|
||||
- **BREAKING** **FIX**: (flutter) pass pixelRatio to createTexture.
|
||||
|
||||
## 0.1.0+11
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+10
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+9
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart' as t;
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'thermion_flutter_texture.dart';
|
||||
import 'thermion_flutter_window.dart';
|
||||
|
||||
class ThermionFlutterOptions {
|
||||
final String? uberarchivePath;
|
||||
|
||||
ThermionFlutterOptions({this.uberarchivePath});
|
||||
const ThermionFlutterOptions.empty() : uberarchivePath = null;
|
||||
}
|
||||
|
||||
abstract class ThermionFlutterPlatform extends PlatformInterface {
|
||||
ThermionFlutterPlatform() : super(token: _token);
|
||||
@@ -17,14 +26,29 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
Future<ThermionViewer> createViewer({String? uberArchivePath});
|
||||
///
|
||||
///
|
||||
///
|
||||
Future<ThermionViewer> createViewer(
|
||||
{covariant ThermionFlutterOptions? options});
|
||||
|
||||
///
|
||||
/// Create a rendering surface.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself. May not be supported on all platforms.
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(
|
||||
int width, int height, int offsetLeft, int offsetRight);
|
||||
t.View view, int width, int height);
|
||||
|
||||
Future destroyTexture(ThermionFlutterTexture texture);
|
||||
|
||||
Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
|
||||
int width, int height, int offsetLeft, int offsetRight);
|
||||
///
|
||||
/// Create a rendering window.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself. May not be supported on all platforms.
|
||||
///
|
||||
Future<ThermionFlutterWindow> createWindow(
|
||||
int width, int height, int offsetLeft, int offsetTop);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
class ThermionFlutterTexture {
|
||||
final int width;
|
||||
final int height;
|
||||
final int? flutterTextureId;
|
||||
final int? hardwareTextureId;
|
||||
final int? surfaceAddress;
|
||||
bool get usesBackingWindow => flutterTextureId == null;
|
||||
abstract class ThermionFlutterTexture {
|
||||
|
||||
ThermionFlutterTexture(this.flutterTextureId, this.hardwareTextureId,
|
||||
this.width, this.height, this.surfaceAddress) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
abstract class ThermionFlutterWindow {
|
||||
|
||||
int get width;
|
||||
int get height;
|
||||
|
||||
int get handle;
|
||||
|
||||
///
|
||||
/// 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();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: thermion_flutter_platform_interface
|
||||
description: A common platform interface for the thermion_flutter plugin.
|
||||
repository: https://github.com/nmfisher/thermion_flutter/thermion_flutter
|
||||
version: 0.1.0+9
|
||||
version: 0.2.1-dev.7
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
@@ -11,7 +11,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.1.0
|
||||
thermion_dart: ^0.1.1+5
|
||||
thermion_dart: ^0.2.1-dev.0.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,3 +1,85 @@
|
||||
## 0.1.0+9
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+8
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+7
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+6
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+5
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+4
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+3
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0+1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
|
||||
|
||||
## 0.1.0-dev.8.0.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0-dev.7.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0-dev.6.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0-dev.5.0
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.1.0-dev.4.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **CHORE**: restructure viewer folders as libraries to only export the public interface.
|
||||
|
||||
## 0.1.0-dev.1.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FIX**: (flutter/web) use window.devicePixelRatio for viewport.
|
||||
- **FEAT**: (flutter) (web) use options to determine whether to create canvas, and set fixed position + offset.
|
||||
- **FEAT**: add ThermionFlutterOptions classes, rename interface parameter for offsetTop and ensure pixelRatio is passed to resizeTexture.
|
||||
- **BREAKING** **FEAT**: (flutter) (web) upgrade package:web dep to 1.0.0.
|
||||
- **BREAKING** **FEAT**: (web) (flutter) create canvas when createViewer is called (no longer need to manually add canvas element to web HTML).
|
||||
- **BREAKING** **FEAT**: resize canvas on web.
|
||||
|
||||
## 0.0.3
|
||||
|
||||
- **FEAT**: allow passing assetPathPrefix to ThermionViewerWasm to account for Flutter build asset paths.
|
||||
- **FEAT**: allow passing assetPathPrefix to ThermionViewerWasm to account for Flutter build asset paths.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- **FEAT**: allow passing assetPathPrefix to ThermionViewerWasm to account for Flutter build asset paths.
|
||||
|
||||
## 0.0.1+9
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'package:thermion_dart/thermion_dart/compatibility/web/interop/thermion_viewer_wasm.dart';
|
||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
import 'package:web/web.dart';
|
||||
|
||||
class ThermionFlutterWebPlugin extends ThermionFlutterPlatform {
|
||||
static void registerWith(Registrar registrar) {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterWebPlugin();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> createTexture(
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
return ThermionFlutterTexture(null, null, 0, 0, null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future destroyTexture(ThermionFlutterTexture texture) async {
|
||||
// noop
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
return ThermionFlutterTexture(null, null, 0, 0, null);
|
||||
}
|
||||
|
||||
Future<ThermionViewer> createViewer({String? uberArchivePath}) async {
|
||||
final canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
var width = window.innerWidth;
|
||||
var height = window.innerHeight;
|
||||
|
||||
var viewer = ThermionViewerWasm(assetPathPrefix: "/assets/");
|
||||
await viewer.initialize(width, height, uberArchivePath: uberArchivePath);
|
||||
return viewer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
|
||||
class ThermionFlutterWebOptions extends ThermionFlutterOptions {
|
||||
|
||||
final bool createCanvas;
|
||||
final bool importCanvasAsWidget;
|
||||
|
||||
ThermionFlutterWebOptions(
|
||||
{this.importCanvasAsWidget = false,
|
||||
this.createCanvas = true,
|
||||
String? uberarchivePath})
|
||||
: super(uberarchivePath: uberarchivePath);
|
||||
|
||||
const ThermionFlutterWebOptions.empty(
|
||||
{this.importCanvasAsWidget = false,
|
||||
this.createCanvas = true,
|
||||
String? uberarchivePath})
|
||||
: super.empty();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: thermion_flutter_web
|
||||
description: A web platform interface for the thermion_flutter plugin.
|
||||
repository: https://github.com/nmfisher/thermion_flutter/thermion_flutter
|
||||
version: 0.0.1+9
|
||||
version: 0.1.0+9
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
@@ -19,9 +19,9 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.1.0
|
||||
web: ^0.5.1
|
||||
thermion_dart: ^0.1.1+5
|
||||
thermion_flutter_platform_interface: ^0.1.0+9
|
||||
web: ^1.0.0
|
||||
thermion_dart: ^0.2.1-dev.0.0.8
|
||||
thermion_flutter_platform_interface: ^0.2.1-dev.7
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user