initial work to split into dart_filament and flutter_filament
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dart_filament/dart_filament/entities/filament_entity.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
|
||||
import 'package:flutter_filament/filament/utils/hardware_keyboard_listener.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
class EntityTransformController {
|
||||
final FlutterFilamentPlugin controller;
|
||||
final FilamentEntity _entity;
|
||||
|
||||
late Timer _ticker;
|
||||
|
||||
double translationSpeed;
|
||||
double rotationRadsPerSecond;
|
||||
|
||||
bool _forward = false;
|
||||
bool _strafeLeft = false;
|
||||
bool _strafeRight = false;
|
||||
bool _back = false;
|
||||
bool _rotateLeft = false;
|
||||
bool _rotateRight = false;
|
||||
double _rotY = 0;
|
||||
|
||||
int? forwardAnimationIndex;
|
||||
int? backwardAnimationIndex;
|
||||
int? strafeLeftAnimationIndex;
|
||||
int? strafeRightAnimationIndex;
|
||||
|
||||
EntityTransformController(this.controller, this._entity,
|
||||
{this.translationSpeed = 1,
|
||||
this.rotationRadsPerSecond = pi / 2,
|
||||
this.forwardAnimationIndex,
|
||||
this.backwardAnimationIndex,
|
||||
this.strafeLeftAnimationIndex,
|
||||
this.strafeRightAnimationIndex}) {
|
||||
var translationSpeedPerTick = translationSpeed / (1000 / 16.667);
|
||||
var rotationRadsPerTick = rotationRadsPerSecond / (1000 / 16.667);
|
||||
_ticker = Timer.periodic(const Duration(milliseconds: 16), (timer) {
|
||||
_update(translationSpeedPerTick, rotationRadsPerTick);
|
||||
});
|
||||
}
|
||||
|
||||
bool _enabled = true;
|
||||
void enable() {
|
||||
_enabled = true;
|
||||
}
|
||||
|
||||
void disable() {
|
||||
_enabled = false;
|
||||
}
|
||||
|
||||
void _update(
|
||||
double translationSpeedPerTick, double rotationRadsPerTick) async {
|
||||
if (!_enabled) {
|
||||
return;
|
||||
}
|
||||
var _position = v.Vector3.zero();
|
||||
bool updateTranslation = false;
|
||||
if (_forward) {
|
||||
_position.add(v.Vector3(0, 0, -translationSpeedPerTick));
|
||||
updateTranslation = true;
|
||||
}
|
||||
if (_back) {
|
||||
_position.add(v.Vector3(0, 0, translationSpeedPerTick));
|
||||
updateTranslation = true;
|
||||
}
|
||||
if (_strafeLeft) {
|
||||
_position.add(v.Vector3(-translationSpeedPerTick, 0, 0));
|
||||
updateTranslation = true;
|
||||
}
|
||||
if (_strafeRight) {
|
||||
_position.add(v.Vector3(translationSpeedPerTick, 0, 0));
|
||||
updateTranslation = true;
|
||||
}
|
||||
|
||||
// TODO - use pitch/yaw/roll
|
||||
bool updateRotation = false;
|
||||
var _rotation = v.Quaternion.identity();
|
||||
|
||||
double rads = 0.0;
|
||||
if (_rotY != 0) {
|
||||
rads = _rotY * pi / 1000;
|
||||
var rotY = v.Quaternion.axisAngle(v.Vector3(0, 1, 0), rads).normalized();
|
||||
_rotation = rotY;
|
||||
updateRotation = true;
|
||||
_rotY = 0;
|
||||
}
|
||||
|
||||
if (updateTranslation) {
|
||||
await controller.queuePositionUpdate(
|
||||
_entity, _position.x, _position.y, _position.z,
|
||||
relative: true);
|
||||
}
|
||||
if (updateRotation) {
|
||||
await controller.queueRotationUpdateQuat(_entity, _rotation,
|
||||
relative: true);
|
||||
}
|
||||
}
|
||||
|
||||
void look(double deltaX) async {
|
||||
_rotY -= deltaX;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_ticker.cancel();
|
||||
}
|
||||
|
||||
bool _playingForwardAnimation = false;
|
||||
bool _playingBackwardAnimation = false;
|
||||
|
||||
void forwardPressed() async {
|
||||
_forward = true;
|
||||
if (forwardAnimationIndex != null && !_playingForwardAnimation) {
|
||||
await controller.playAnimation(_entity, forwardAnimationIndex!,
|
||||
loop: true, replaceActive: false);
|
||||
_playingForwardAnimation = true;
|
||||
}
|
||||
}
|
||||
|
||||
void forwardReleased() async {
|
||||
_forward = false;
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
if (!_forward) {
|
||||
_playingForwardAnimation = false;
|
||||
if (forwardAnimationIndex != null) {
|
||||
await controller.stopAnimation(_entity, forwardAnimationIndex!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void backPressed() async {
|
||||
_back = true;
|
||||
if (forwardAnimationIndex != null) {
|
||||
if (!_playingBackwardAnimation) {
|
||||
await controller.playAnimation(_entity, forwardAnimationIndex!,
|
||||
loop: true, replaceActive: false, reverse: true);
|
||||
_playingBackwardAnimation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void backReleased() async {
|
||||
_back = false;
|
||||
if (forwardAnimationIndex != null) {
|
||||
await controller.stopAnimation(_entity, forwardAnimationIndex!);
|
||||
}
|
||||
_playingBackwardAnimation = false;
|
||||
}
|
||||
|
||||
void strafeLeftPressed() {
|
||||
_strafeLeft = true;
|
||||
}
|
||||
|
||||
void strafeLeftReleased() async {
|
||||
_strafeLeft = false;
|
||||
}
|
||||
|
||||
void strafeRightPressed() {
|
||||
_strafeRight = true;
|
||||
}
|
||||
|
||||
void strafeRightReleased() async {
|
||||
_strafeRight = false;
|
||||
}
|
||||
|
||||
void Function()? _mouse1DownCallback;
|
||||
void onMouse1Down(void Function() callback) {
|
||||
_mouse1DownCallback = callback;
|
||||
}
|
||||
|
||||
void mouse1Down() async {
|
||||
_mouse1DownCallback?.call();
|
||||
}
|
||||
|
||||
void mouse1Up() async {}
|
||||
|
||||
void mouse2Up() async {}
|
||||
|
||||
void mouse2Down() async {}
|
||||
static HardwareKeyboardListener? _keyboardListener;
|
||||
|
||||
static Future<EntityTransformController> create(
|
||||
FlutterFilamentPlugin controller, FilamentEntity entity,
|
||||
{double? translationSpeed, String? forwardAnimation}) async {
|
||||
int? forwardAnimationIndex;
|
||||
if (forwardAnimation != null) {
|
||||
final animationNames = await controller.getAnimationNames(entity);
|
||||
forwardAnimationIndex = animationNames.indexOf(forwardAnimation);
|
||||
}
|
||||
|
||||
if (forwardAnimationIndex == -1) {
|
||||
throw Exception("Invalid animation : $forwardAnimation");
|
||||
}
|
||||
|
||||
_keyboardListener?.dispose();
|
||||
var transformController = EntityTransformController(controller, entity,
|
||||
translationSpeed: translationSpeed ?? 1.0,
|
||||
forwardAnimationIndex: forwardAnimationIndex);
|
||||
_keyboardListener = HardwareKeyboardListener(transformController);
|
||||
return transformController;
|
||||
}
|
||||
}
|
||||
1711
flutter_filament/lib/filament/flutter_filament.g.dart
Normal file
1711
flutter_filament/lib/filament/flutter_filament.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
105
flutter_filament/lib/filament/flutter_filament_plugin.dart
Normal file
105
flutter_filament/lib/filament/flutter_filament_plugin.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:dart_filament/dart_filament.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_filament/filament/flutter_filament_texture.dart';
|
||||
|
||||
///
|
||||
/// A subclass of [FilamentViewer] that uses Flutter platform channels
|
||||
/// to create rendering contexts, callbacks and surfaces (either backing texture(s).
|
||||
///
|
||||
///
|
||||
class FlutterFilamentPlugin extends FilamentViewer {
|
||||
final MethodChannel _channel;
|
||||
|
||||
FlutterFilamentPlugin._(this._channel,
|
||||
{super.renderCallback,
|
||||
super.renderCallbackOwner,
|
||||
super.surface,
|
||||
required super.resourceLoader,
|
||||
super.driver,
|
||||
super.sharedContext,
|
||||
super.uberArchivePath});
|
||||
|
||||
static Future<FlutterFilamentPlugin> create({String? uberArchivePath}) async {
|
||||
var channel = const MethodChannel("app.polyvox.filament/event");
|
||||
var resourceLoader = Pointer<ResourceLoaderWrapper>.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);
|
||||
|
||||
var window = await channel.invokeMethod("getWindow");
|
||||
var windowPtr =
|
||||
window == null ? nullptr : Pointer<Void>.fromAddress(window);
|
||||
|
||||
return FlutterFilamentPlugin._(channel,
|
||||
renderCallback: renderCallback,
|
||||
renderCallbackOwner: renderCallbackOwner,
|
||||
surface: windowPtr,
|
||||
resourceLoader: resourceLoader,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: uberArchivePath);
|
||||
}
|
||||
|
||||
Future<FlutterFilamentTexture?> createTexture(
|
||||
int width, int height, int offsetLeft, int offsetRight) async {
|
||||
var result = await _channel
|
||||
.invokeMethod("createTexture", [width, height, offsetLeft, offsetLeft]);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
viewportDimensions = (width.toDouble(), height.toDouble());
|
||||
var texture = FlutterFilamentTexture(result[0], result[1], width, height);
|
||||
await createSwapChain(width.toDouble(), height.toDouble());
|
||||
|
||||
var renderTarget = await createRenderTarget(
|
||||
width.toDouble(), height.toDouble(), texture.hardwareTextureId);
|
||||
return texture;
|
||||
}
|
||||
|
||||
Future destroyTexture(FlutterFilamentTexture texture) async {
|
||||
await _channel.invokeMethod("destroyTexture", texture.flutterTextureId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future resizeTexture(FlutterFilamentTexture texture, int width, int height,
|
||||
int offsetLeft, int offsetRight) async {
|
||||
await destroySwapChain();
|
||||
await destroyTexture(texture);
|
||||
await createSwapChain(width.toDouble(), height.toDouble());
|
||||
|
||||
var newTexture =
|
||||
await createTexture(width, height, offsetLeft, offsetRight);
|
||||
await createRenderTarget(
|
||||
width.toDouble(), height.toDouble(), newTexture!.hardwareTextureId);
|
||||
await updateViewportAndCameraProjection(
|
||||
width.toDouble(), height.toDouble());
|
||||
viewportDimensions = (width.toDouble(), height.toDouble());
|
||||
return newTexture;
|
||||
// await _channel.invokeMethod("resizeTexture",
|
||||
// [texture.flutterTextureId, width, height, offsetLeft, offsetRight]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class FlutterFilamentTexture {
|
||||
final int width;
|
||||
final int height;
|
||||
final int flutterTextureId;
|
||||
final int hardwareTextureId;
|
||||
|
||||
FlutterFilamentTexture(
|
||||
this.flutterTextureId, this.hardwareTextureId, this.width, this.height);
|
||||
}
|
||||
1573
flutter_filament/lib/filament/generated_bindings_web.dart
Normal file
1573
flutter_filament/lib/filament/generated_bindings_web.dart
Normal file
File diff suppressed because it is too large
Load Diff
15
flutter_filament/lib/filament/utils/camera_orientation.dart
Normal file
15
flutter_filament/lib/filament/utils/camera_orientation.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
class CameraOrientation {
|
||||
v.Vector3 position = v.Vector3(0, 0, 0);
|
||||
|
||||
var rotationX = 0.0;
|
||||
var rotationY = 0.0;
|
||||
var rotationZ = 0.0;
|
||||
|
||||
v.Quaternion compose() {
|
||||
return v.Quaternion.axisAngle(v.Vector3(0, 0, 1), rotationZ) *
|
||||
v.Quaternion.axisAngle(v.Vector3(0, 1, 0), rotationY) *
|
||||
v.Quaternion.axisAngle(v.Vector3(1, 0, 0), rotationX);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_filament/filament/entities/entity_transform_controller.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_filament/filament/entities/entity_transform_controller.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'package:dart_filament/dart_filament/entities/filament_entity.dart';
|
||||
import 'package:dart_filament/dart_filament/filament_viewer_impl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:flutter_filament/filament/utils/camera_orientation.dart';
|
||||
import 'dart:math';
|
||||
import 'package:vector_math/vector_math_64.dart' as v64;
|
||||
|
||||
class CameraOptionsWidget extends StatefulWidget {
|
||||
final FilamentViewer controller;
|
||||
final CameraOrientation cameraOrientation;
|
||||
final List<({FilamentEntity entity, String name})> cameras;
|
||||
|
||||
CameraOptionsWidget(
|
||||
{super.key,
|
||||
required this.controller,
|
||||
required this.cameras,
|
||||
required this.cameraOrientation}) {}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CameraOptionsWidgetState();
|
||||
}
|
||||
|
||||
class _CameraOptionsWidgetState extends State<CameraOptionsWidget> {
|
||||
final _apertureController = TextEditingController();
|
||||
final _speedController = TextEditingController();
|
||||
final _sensitivityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_apertureController.text = "0";
|
||||
_speedController.text = "0";
|
||||
_sensitivityController.text = "0";
|
||||
|
||||
_apertureController.addListener(() {
|
||||
_set();
|
||||
setState(() {});
|
||||
});
|
||||
_speedController.addListener(() {
|
||||
_set();
|
||||
setState(() {});
|
||||
});
|
||||
_sensitivityController.addListener(() {
|
||||
_set();
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CameraOptionsWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.cameras.length != widget.cameras.length) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future _set() async {
|
||||
await widget.controller.setCameraExposure(
|
||||
double.parse(_apertureController.text),
|
||||
double.parse(_speedController.text),
|
||||
double.parse(_sensitivityController.text));
|
||||
await widget.controller.setCameraPosition(
|
||||
widget.cameraOrientation.position.x,
|
||||
widget.cameraOrientation.position.y,
|
||||
widget.cameraOrientation.position.z);
|
||||
var rotation = widget.cameraOrientation.compose();
|
||||
await widget.controller.setCameraRotation(rotation);
|
||||
print(
|
||||
"Camera : ${widget.cameraOrientation.position} ${widget.cameraOrientation.rotationX} ${widget.cameraOrientation.rotationY} ${widget.cameraOrientation.rotationZ}");
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
double _bloom = 0.0;
|
||||
|
||||
double _focalLength = 26.0;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(platform: TargetPlatform.android),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
showValueIndicator: ShowValueIndicator.always,
|
||||
valueIndicatorTextStyle: TextStyle(color: Colors.black)),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
Text("Aperture"),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _apertureController,
|
||||
)),
|
||||
Text("Speed"),
|
||||
Expanded(child: TextField(controller: _speedController)),
|
||||
Text("Sensitivity"),
|
||||
Expanded(
|
||||
child: TextField(controller: _sensitivityController)),
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Bloom: ${_bloom.toStringAsFixed(2)}"),
|
||||
Slider(
|
||||
value: _bloom,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
_bloom = v;
|
||||
});
|
||||
await widget.controller.setBloom(_bloom);
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Focal length"),
|
||||
Slider(
|
||||
label: _focalLength.toString(),
|
||||
value: _focalLength,
|
||||
min: 1.0,
|
||||
max: 100.0,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
_focalLength = v;
|
||||
});
|
||||
await widget.controller
|
||||
.setCameraFocalLength(_focalLength);
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("X"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.x.toString(),
|
||||
value: widget.cameraOrientation.position.x,
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.position.x = v;
|
||||
});
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Y"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.y.toString(),
|
||||
value: widget.cameraOrientation.position.y,
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.position.y = v;
|
||||
});
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("Z"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.position.z.toString(),
|
||||
value: widget.cameraOrientation.position.z,
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.position.z = v;
|
||||
});
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTX"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationX.toString(),
|
||||
value: widget.cameraOrientation.rotationX,
|
||||
min: -pi,
|
||||
max: pi,
|
||||
onChanged: (value) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.rotationX = value;
|
||||
});
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTY"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationY.toString(),
|
||||
value: widget.cameraOrientation.rotationY,
|
||||
min: -pi,
|
||||
max: pi,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.rotationY = v;
|
||||
});
|
||||
_set();
|
||||
}),
|
||||
]),
|
||||
Row(children: [
|
||||
Text("ROTZ"),
|
||||
Slider(
|
||||
label: widget.cameraOrientation.rotationZ.toString(),
|
||||
value: widget.cameraOrientation.rotationZ,
|
||||
min: -pi,
|
||||
max: pi,
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
widget.cameraOrientation.rotationZ = v;
|
||||
});
|
||||
_set();
|
||||
})
|
||||
]),
|
||||
Wrap(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Text("Main "),
|
||||
onTap: () {
|
||||
widget.controller.setMainCamera();
|
||||
},
|
||||
),
|
||||
...widget.cameras
|
||||
.map((camera) => GestureDetector(
|
||||
onTap: () {
|
||||
widget.controller
|
||||
.setCamera(camera.entity, camera.name);
|
||||
},
|
||||
child: Text(camera.name)))
|
||||
.toList()
|
||||
],
|
||||
)
|
||||
]))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_filament/filament/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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
import 'package:flutter_filament/filament/widgets/camera/gestures/filament_gesture_detector_desktop.dart';
|
||||
import 'package:flutter_filament/filament/widgets/camera/gestures/filament_gesture_detector_mobile.dart';
|
||||
|
||||
enum GestureType { rotateCamera, panCamera, panBackground }
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class FilamentGestureDetector extends StatelessWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a FilamentWidget (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 [FilamentWidget] you wish to control.
|
||||
///
|
||||
final FlutterFilamentPlugin 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 FilamentGestureDetector(
|
||||
{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 Container();
|
||||
}
|
||||
if (kIsWeb) {
|
||||
throw Exception("TODO");
|
||||
} else if (Platform.isLinux ||
|
||||
Platform.isWindows ||
|
||||
Platform.isMacOS) {
|
||||
return FilamentGestureDetectorDesktop(
|
||||
controller: controller,
|
||||
child: child,
|
||||
showControlOverlay: showControlOverlay,
|
||||
enableCamera: enableCamera,
|
||||
enablePicking: enablePicking,
|
||||
);
|
||||
} else {
|
||||
return FilamentGestureDetectorMobile(
|
||||
controller: controller,
|
||||
child: child,
|
||||
showControlOverlay: showControlOverlay,
|
||||
enableCamera: enableCamera,
|
||||
enablePicking: enablePicking,
|
||||
onScaleStart: onScaleStart,
|
||||
onScaleUpdate: onScaleUpdate,
|
||||
onScaleEnd: onScaleEnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dart_filament/dart_filament/entities/gizmo.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class FilamentGestureDetectorDesktop extends StatefulWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a FilamentWidget (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 [FilamentWidget] you wish to control.
|
||||
///
|
||||
final FlutterFilamentPlugin 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 FilamentGestureDetectorDesktop(
|
||||
{Key? key,
|
||||
required this.controller,
|
||||
this.child,
|
||||
this.showControlOverlay = false,
|
||||
this.enableCamera = true,
|
||||
this.enablePicking = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FilamentGestureDetectorDesktopState();
|
||||
}
|
||||
|
||||
class _FilamentGestureDetectorDesktopState
|
||||
extends State<FilamentGestureDetectorDesktop> {
|
||||
///
|
||||
///
|
||||
///
|
||||
// ignore: unused_field
|
||||
final bool _scaling = false;
|
||||
|
||||
bool _pointerMoving = false;
|
||||
|
||||
Gizmo? _gizmo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.scene.gizmo.then((g) {
|
||||
_gizmo = g;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FilamentGestureDetectorDesktop 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) {
|
||||
if (_gizmo == null) {
|
||||
return Container();
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
_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) {
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
|
||||
enum GestureType { rotateCamera, panCamera, panBackground }
|
||||
|
||||
///
|
||||
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||
///
|
||||
class FilamentGestureDetectorMobile extends StatefulWidget {
|
||||
///
|
||||
/// The content to display below the gesture detector/listener widget.
|
||||
/// This will usually be a FilamentWidget (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 [FilamentWidget] you wish to control.
|
||||
///
|
||||
final FlutterFilamentPlugin 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 FilamentGestureDetectorMobile(
|
||||
{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() => _FilamentGestureDetectorMobileState();
|
||||
}
|
||||
|
||||
class _FilamentGestureDetectorMobileState
|
||||
extends State<FilamentGestureDetectorMobile> {
|
||||
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(FilamentGestureDetectorMobile 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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:dart_filament/dart_filament/entities/filament_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
|
||||
class EntityListWidget extends StatefulWidget {
|
||||
final FlutterFilamentPlugin? 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(FilamentEntity 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) => MenuItemButton(
|
||||
child: Text(a),
|
||||
onPressed: () {
|
||||
widget.controller!.playAnimation(
|
||||
entity, animations.data!.indexOf(a));
|
||||
},
|
||||
))
|
||||
.toList())
|
||||
])
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _light(FilamentEntity 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();
|
||||
}
|
||||
return FutureBuilder(
|
||||
future: widget.controller!.initialized,
|
||||
builder: (_, AsyncSnapshot<bool> initialized) =>
|
||||
initialized.data != true
|
||||
? Container()
|
||||
: StreamBuilder(
|
||||
stream: widget.controller!.scene.onUpdated,
|
||||
builder: (_, __) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 30, vertical: 10),
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Colors.white.withOpacity(0.25),
|
||||
),
|
||||
child: ListView(
|
||||
reverse: true,
|
||||
children: widget.controller!.scene
|
||||
.listLights()
|
||||
.map(_light)
|
||||
.followedBy(widget.controller!.scene
|
||||
.listEntities()
|
||||
.map(_entity))
|
||||
.cast<Widget>()
|
||||
.toList()))));
|
||||
}
|
||||
}
|
||||
139
flutter_filament/lib/filament/widgets/filament_widget.dart
Normal file
139
flutter_filament/lib/filament/widgets/filament_widget.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
import 'package:flutter_filament/filament/flutter_filament_texture.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter_filament/filament/widgets/resize_observer.dart';
|
||||
import 'package:flutter_filament/flutter_filament.dart';
|
||||
|
||||
class FilamentWidget extends StatefulWidget {
|
||||
final FlutterFilamentPlugin plugin;
|
||||
|
||||
///
|
||||
/// 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 FilamentWidget({Key? key, this.initial, required this.plugin})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_FilamentWidgetState createState() => _FilamentWidgetState();
|
||||
}
|
||||
|
||||
class _FilamentWidgetState extends State<FilamentWidget> {
|
||||
FlutterFilamentTexture? _texture;
|
||||
|
||||
late final AppLifecycleListener _appLifecycleListener;
|
||||
|
||||
Rect get _rect {
|
||||
final renderBox = (context.findRenderObject()) as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final translation = renderBox.getTransformTo(null).getTranslation();
|
||||
return Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
var size = ((context.findRenderObject()) as RenderBox).size;
|
||||
var width = size.width.ceil();
|
||||
var height = size.height.ceil();
|
||||
_texture = await widget.plugin.createTexture(width, height, 0, 0);
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onStateChange: _handleStateChange,
|
||||
);
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_texture != null) {
|
||||
widget.plugin.destroyTexture(_texture!);
|
||||
}
|
||||
|
||||
_appLifecycleListener.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _wasRenderingOnInactive = false;
|
||||
|
||||
void _handleStateChange(AppLifecycleState state) async {
|
||||
await widget.plugin.initialized;
|
||||
switch (state) {
|
||||
case AppLifecycleState.detached:
|
||||
print("Detached");
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = widget.plugin.rendering;
|
||||
}
|
||||
await widget.plugin.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
print("Hidden");
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = widget.plugin.rendering;
|
||||
}
|
||||
await widget.plugin.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
print("Inactive");
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = widget.plugin.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 widget.plugin.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
print("Paused");
|
||||
if (!_wasRenderingOnInactive) {
|
||||
_wasRenderingOnInactive = widget.plugin.rendering;
|
||||
}
|
||||
await widget.plugin.setRendering(false);
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
print("Resumed");
|
||||
await widget.plugin.setRendering(_wasRenderingOnInactive);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future _resizeTexture(Size newSize) async {
|
||||
_texture = await widget.plugin.resizeTexture(
|
||||
_texture!, newSize.width.toInt(), newSize.height.toInt(), 0, 0);
|
||||
print(
|
||||
"Resized texture, new flutter ID is ${_texture!.flutterTextureId} (hardware ID ${_texture!.hardwareTextureId})");
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_texture == null) {
|
||||
return widget.initial ?? Container(color: 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)
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_filament/filament/flutter_filament_plugin.dart';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart' as v;
|
||||
|
||||
class IblRotationSliderWidget extends StatefulWidget {
|
||||
final FlutterFilamentPlugin controller;
|
||||
|
||||
const IblRotationSliderWidget({super.key, required this.controller});
|
||||
@override
|
||||
State<StatefulWidget> createState() => _IblRotationSliderWidgetState();
|
||||
}
|
||||
|
||||
class _IblRotationSliderWidgetState extends State<IblRotationSliderWidget> {
|
||||
double _iblRotation = 0;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Slider(
|
||||
value: _iblRotation,
|
||||
onChanged: (value) {
|
||||
_iblRotation = value;
|
||||
setState(() {});
|
||||
print(value);
|
||||
var rotation = v.Matrix3.identity();
|
||||
Matrix4.rotationY(value * 2 * pi).copyRotation(rotation);
|
||||
widget.controller.rotateIbl(rotation);
|
||||
});
|
||||
}
|
||||
}
|
||||
195
flutter_filament/lib/filament/widgets/lights/light_slider.dart
Normal file
195
flutter_filament/lib/filament/widgets/lights/light_slider.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dart_filament/dart_filament/entities/filament_entity.dart';
|
||||
import 'package:dart_filament/dart_filament/filament_viewer_impl.dart';
|
||||
import 'package:dart_filament/dart_filament/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 FilamentViewer 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> {
|
||||
FilamentEntity? _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(
|
||||
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,
|
||||
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();
|
||||
})),
|
||||
])
|
||||
]))));
|
||||
}
|
||||
}
|
||||
41
flutter_filament/lib/filament/widgets/resize_observer.dart
Normal file
41
flutter_filament/lib/filament/widgets/resize_observer.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef ResizeCallback = void Function(Size newSize);
|
||||
|
||||
class ResizeObserver extends SingleChildRenderObjectWidget {
|
||||
final ResizeCallback onResized;
|
||||
|
||||
const ResizeObserver({
|
||||
Key? key,
|
||||
required this.onResized,
|
||||
Widget? child,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) =>
|
||||
_RenderResizeObserver(onLayoutChangedCallback: onResized);
|
||||
}
|
||||
|
||||
class _RenderResizeObserver extends RenderProxyBox {
|
||||
final ResizeCallback onLayoutChangedCallback;
|
||||
|
||||
_RenderResizeObserver({
|
||||
RenderBox? child,
|
||||
required this.onLayoutChangedCallback,
|
||||
}) : super(child);
|
||||
|
||||
Size _oldSize = Size.zero;
|
||||
|
||||
@override
|
||||
void performLayout() async {
|
||||
super.performLayout();
|
||||
if (size.width != _oldSize.width || size.height != _oldSize.height) {
|
||||
onLayoutChangedCallback(size);
|
||||
_oldSize = Size(size.width, size.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
10
flutter_filament/lib/flutter_filament.dart
Normal file
10
flutter_filament/lib/flutter_filament.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
library flutter_filament;
|
||||
|
||||
export 'filament/flutter_filament_plugin.dart';
|
||||
|
||||
export 'filament/widgets/camera/camera_options_widget.dart';
|
||||
export 'filament/widgets/camera/gestures/filament_gesture_detector.dart';
|
||||
export 'filament/widgets/filament_widget.dart';
|
||||
export 'filament/widgets/debug/entity_list_widget.dart';
|
||||
export 'filament/entities/entity_transform_controller.dart';
|
||||
export 'filament/widgets/camera/entity_controller_mouse_widget.dart';
|
||||
Reference in New Issue
Block a user