add (very rough) gizmo, restructure Dart package into library, add EntityListWidget

This commit is contained in:
Nick Fisher
2024-03-25 22:21:37 +08:00
parent 66ae0a4d08
commit 357c585d44
53 changed files with 2913 additions and 568 deletions

View File

@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.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 FilamentController 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()
],
)
]))));
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
class EntityListWidget extends StatefulWidget {
final FilamentController? 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 ValueListenableBuilder(
valueListenable: widget.controller!.hasViewer,
builder: (_, bool hasViewer, __) => !hasViewer
? 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()))));
}
}

View File

@@ -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);
});
}
}

View File

@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/widgets/filament_gesture_detector_desktop.dart';
import 'package:flutter_filament/filament/widgets/filament_gesture_detector_mobile.dart';
import '../filament_controller.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 FilamentController 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 ValueListenableBuilder(
valueListenable: controller.hasViewer,
builder: (_, bool hasViewer, __) {
if (!hasViewer) {
return Container(child: child);
}
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);
}
});
}
}

View File

@@ -0,0 +1,185 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_filament/filament/entities/gizmo.dart';
import '../filament_controller.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 FilamentController 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 get _gizmo => widget.controller.scene.gizmo;
@override
void initState() {
super.initState();
}
@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) {
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);
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);
}
}

View File

@@ -0,0 +1,234 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../filament_controller.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 FilamentController 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()
]);
}
}

View File

@@ -0,0 +1,309 @@
import 'dart:io';
import 'dart:math';
import 'dart:developer' as dev;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'dart:async';
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);
}
}
}
class FilamentWidget extends StatefulWidget {
final FilamentController controller;
///
/// 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, required this.controller, this.initial})
: super(key: key);
@override
_FilamentWidgetState createState() => _FilamentWidgetState();
}
class _FilamentWidgetState extends State<FilamentWidget> {
int? _width;
int? _height;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
var size = ((context.findRenderObject()) as RenderBox).size;
_width = size.width.ceil();
_height = size.height.ceil();
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
if (_width == null || _height == null) {
return widget.initial ?? Container(color: Colors.red);
}
return ResizeObserver(
onResized: (newSize) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
_width = newSize.width.ceil();
_height = newSize.height.ceil();
});
});
},
child: _SizedFilamentWidget(
initial: widget.initial,
width: _width!,
height: _height!,
controller: widget.controller,
));
}
}
class _SizedFilamentWidget extends StatefulWidget {
final int width;
final int height;
final Widget? initial;
final FilamentController controller;
const _SizedFilamentWidget(
{required this.width,
required this.height,
this.initial,
required this.controller});
@override
State<StatefulWidget> createState() => _SizedFilamentWidgetState();
}
class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
String? _error;
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() {
_appLifecycleListener = AppLifecycleListener(
onStateChange: _handleStateChange,
);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
try {
widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
} catch (err) {
dev.log("Fatal error : $err");
_error = err.toString();
}
});
super.initState();
}
Timer? _resizeTimer;
bool _resizing = false;
Future _resize() async {
final completer = Completer();
// resizing the window can be sluggish (particular in debug mode), exacerbated when simultaneously recreating the swapchain and resize the window.
// to address this, whenever the widget is resized, we set a timer for Xms in the future.
// this timer will call [resize] with the widget size at that point in time.
// any subsequent widget resizes will cancel the timer and replace with a new one.
// debug mode does need a longer timeout.
_resizeTimer?.cancel();
await widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
_resizeTimer = Timer(
Duration(milliseconds: (kReleaseMode || Platform.isWindows) ? 10 : 100),
() async {
try {
while (_resizing) {
await Future.delayed(const Duration(milliseconds: 20));
}
_resizing = true;
await widget.controller
.setDimensions(_rect, MediaQuery.of(context).devicePixelRatio);
await widget.controller.resize();
_resizeTimer = null;
setState(() {});
} catch (err) {
dev.log("Error resizing FilamentWidget: $err");
} finally {
_resizing = false;
completer.complete();
_resizeTimer?.cancel();
}
});
return completer.future;
}
@override
void didUpdateWidget(_SizedFilamentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.height != oldWidget.height || widget.width != oldWidget.width) {
_resize();
}
}
@override
void dispose() {
_appLifecycleListener.dispose();
super.dispose();
}
bool _wasRenderingOnInactive = true;
void _handleStateChange(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.detached:
dev.log("Detached");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.hidden:
dev.log("Hidden");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.inactive:
dev.log("Inactive");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.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.controller.setRendering(false);
break;
case AppLifecycleState.paused:
dev.log("Paused");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.resumed:
dev.log("Resumed");
await widget.controller.setRendering(_wasRenderingOnInactive);
break;
}
}
@override
Widget build(BuildContext context) {
// if an error was encountered in creating a viewer, display the error message and don't even try to display a Texture widget.
if (_error != null) {
return Container(
color: Colors.white,
child: Column(children: [
const Text("A fatal error was encountered"),
Text(_error!)
]));
}
if (!widget.controller.requiresTextureWidget) {
return Stack(children: [
Positioned.fill(child: CustomPaint(painter: TransparencyPainter()))
]);
}
return ListenableBuilder(
listenable: widget.controller.textureDetails,
builder: (BuildContext ctx, Widget? wdgt) {
if (widget.controller.textureDetails.value == null) {
return Stack(children: [
Positioned.fill(
child: widget.initial ?? Container(color: Colors.red))
]);
}
// see [FilamentControllerFFI.resize] for an explanation of how we deal with resizing
var texture = Texture(
key: ObjectKey(
"texture_${widget.controller.textureDetails.value!.textureId}"),
textureId: widget.controller.textureDetails.value!.textureId,
filterQuality: FilterQuality.none,
freeze: false,
);
return 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: texture)
: texture)
]);
});
}
}
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;
}

View File

@@ -0,0 +1,31 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:vector_math/vector_math_64.dart' as v;
class IblRotationSliderWidget extends StatefulWidget {
final FilamentController 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);
});
}
}

View File

@@ -0,0 +1,193 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_filament/filament/filament_controller.dart';
import 'package:flutter_filament/filament/utils/light_options.dart';
import 'package:vector_math/vector_math_64.dart' as v;
class LightSliderWidget extends StatefulWidget {
final FilamentController 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: 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();
})),
])
]))));
}
}