implement picker/getNameForEntity

This commit is contained in:
Nick Fisher
2023-10-11 11:10:47 +08:00
parent 79292914d2
commit 98bcf5d7ad
25 changed files with 704 additions and 393 deletions

View File

@@ -0,0 +1,72 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:polyvox_filament/widgets/filament_gesture_detector_desktop.dart';
import 'package:polyvox_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, all gestures will be ignored.
///
final bool listenerEnabled;
final double zoomDelta;
const FilamentGestureDetector(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.listenerEnabled = true,
this.zoomDelta = 1})
: super(key: key);
@override
Widget build(BuildContext context) {
if (kIsWeb) {
throw Exception("TODO");
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
return FilamentGestureDetectorDesktop(
controller: controller,
child: child,
showControlOverlay: showControlOverlay,
listenerEnabled: listenerEnabled,
);
} else {
return FilamentGestureDetectorMobile(
controller: controller,
child: child,
showControlOverlay: showControlOverlay,
listenerEnabled: listenerEnabled,
);
}
}
}

View File

@@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.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, all gestures will be ignored.
///
final bool listenerEnabled;
final double zoomDelta;
const FilamentGestureDetectorDesktop(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.listenerEnabled = true,
this.zoomDelta = 1})
: super(key: key);
@override
State<StatefulWidget> createState() => _FilamentGestureDetectorDesktopState();
}
class _FilamentGestureDetectorDesktopState
extends State<FilamentGestureDetectorDesktop> {
///
///
///
bool _scaling = false;
bool _pointerMoving = false;
@override
void didUpdateWidget(FilamentGestureDetectorDesktop oldWidget) {
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
widget.listenerEnabled != oldWidget.listenerEnabled) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
Timer? _scrollTimer;
///
/// Scroll-wheel on desktop, interpreted as zoom
///
void _zoom(PointerScrollEvent pointerSignal) {
_scrollTimer?.cancel();
widget.controller.zoomBegin();
widget.controller.zoomUpdate(pointerSignal.scrollDelta.dy > 0
? widget.zoomDelta
: -widget.zoomDelta);
_scrollTimer = Timer(const Duration(milliseconds: 100), () {
widget.controller.zoomEnd();
_scrollTimer = null;
});
}
@override
Widget build(BuildContext context) {
if (!widget.listenerEnabled) {
return widget.child ?? Container();
}
return Listener(
onPointerSignal: (PointerSignalEvent pointerSignal) async {
if (pointerSignal is PointerScrollEvent) {
_zoom(pointerSignal);
} else {
throw Exception("TODO");
}
},
onPointerPanZoomStart: (pzs) {
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
},
// ignore all pointer down events
// so we can wait to see if the pointer will be held/moved (interpreted as rotate/pan),
// or if this is a single mousedown event (interpreted as viewport pick)
onPointerDown: (d) async {},
// holding/moving the left mouse button is interpreted as a pan, middle mouse button as a rotate
onPointerMove: (PointerMoveEvent d) async {
// 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.controller.rotateStart(d.position.dx, d.position.dy);
} else {
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.controller.rotateUpdate(d.position.dx, d.position.dy);
} else {
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 (d.buttons == kTertiaryButton) {
widget.controller.rotateEnd();
} else {
if (_pointerMoving) {
widget.controller.panEnd();
} else {
widget.controller
.pick(d.localPosition.dx.toInt(), d.localPosition.dy.toInt());
}
}
_pointerMoving = false;
},
child: widget.child);
}
}

View File

@@ -0,0 +1,195 @@
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, all gestures will be ignored.
///
final bool listenerEnabled;
final double zoomDelta;
const FilamentGestureDetectorMobile(
{Key? key,
required this.controller,
this.child,
this.showControlOverlay = false,
this.listenerEnabled = true,
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.
late Function(double x, double y) _functionStart;
late Function(double x, double y) _functionUpdate;
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.listenerEnabled != oldWidget.listenerEnabled) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
Timer? _scrollTimer;
// pinch zoom on mobile
// couldn't find any equivalent for pointerCount in Listener so we use two widgets:
// - outer is a GestureDetector only for pinch zoom
// - inner is a Listener for all other gestures (including scroll zoom on desktop)
@override
Widget build(BuildContext context) {
if (!widget.listenerEnabled) {
return widget.child ?? Container();
}
return Stack(children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onDoubleTap: () {
setState(() {
_rotateOnPointerMove = !_rotateOnPointerMove;
});
},
onScaleStart: (d) async {
if (d.pointerCount == 2) {
_scaling = true;
widget.controller.zoomBegin();
} else if (!_scaling) {
if (_rotateOnPointerMove) {
widget.controller
.rotateStart(d.focalPoint.dx, d.focalPoint.dy);
} else {
widget.controller
.panStart(d.focalPoint.dx, d.focalPoint.dy);
}
}
},
onScaleEnd: (d) async {
if (d.pointerCount == 2) {
widget.controller.zoomEnd();
_scaling = false;
} else if (!_scaling) {
if (_rotateOnPointerMove) {
widget.controller.rotateEnd();
} else {
widget.controller.panEnd();
}
}
},
onScaleUpdate: (ScaleUpdateDetails d) async {
if (d.pointerCount == 2) {
widget.controller
.zoomUpdate(d.horizontalScale > 1 ? 0.1 : -0.1);
} else if (!_scaling) {
if (_rotateOnPointerMove) {
widget.controller
.rotateUpdate(d.focalPoint.dx, d.focalPoint.dy);
} else {
widget.controller
.panUpdate(d.focalPoint.dx, d.focalPoint.dy);
}
}
},
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,208 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:polyvox_filament/filament_controller.dart';
import 'dart:async';
typedef ResizeCallback = void Function(Size oldSize, 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);
late var _oldSize = size;
@override
void performLayout() {
super.performLayout();
if (size != _oldSize) {
onLayoutChangedCallback(_oldSize, size);
_oldSize = size;
}
}
}
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.
///
late final Widget initial;
final void Function()? onResize;
FilamentWidget(
{Key? key, required this.controller, this.onResize, Widget? initial})
: super(key: key) {
if (initial != null) {
this.initial = initial;
} else {
this.initial = Container(color: Colors.red);
}
}
@override
_FilamentWidgetState createState() => _FilamentWidgetState();
}
class _FilamentWidgetState extends State<FilamentWidget> {
StreamSubscription? _textureIdListener;
int? _textureId;
bool _resizing = false;
late final AppLifecycleListener _listener;
AppLifecycleState? _lastState;
Timer? _resizeTimer;
void _handleStateChange(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.detached:
print("Detached");
_textureId = null;
await widget.controller.destroyViewer();
await widget.controller.destroyTexture();
break;
case AppLifecycleState.hidden:
print("Hidden");
if (Platform.isIOS) {
_textureId = null;
await widget.controller.destroyViewer();
await widget.controller.destroyTexture();
}
break;
case AppLifecycleState.inactive:
print("Inactive");
break;
case AppLifecycleState.paused:
print("Paused");
break;
case AppLifecycleState.resumed:
print("Resumed");
if (_textureId == null) {
var size = ((context.findRenderObject()) as RenderBox).size;
print("Size after resuming : $size");
await widget.controller
.createViewer(size.width.toInt(), size.height.toInt());
print("Created viewer Size after resuming");
}
break;
}
_lastState = state;
}
@override
void initState() {
_listener = AppLifecycleListener(
onStateChange: _handleStateChange,
);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
// when attaching a debugger via Android Studio on startup, this can delay presentation of the widget
// (meaning the widget may attempt to create a viewer with size 0x0).
// we just add a small delay here which should avoid this
if (!kReleaseMode) {
await Future.delayed(Duration(seconds: 2));
}
var size = ((context.findRenderObject()) as RenderBox).size;
widget.controller.createViewer(size.width.toInt(), size.height.toInt());
});
_textureIdListener = widget.controller.textureId.listen((int? textureId) {
var size = ((context.findRenderObject()) as RenderBox).size;
print(
"Received new texture ID $textureId at size $size (current textureID $_textureId)");
setState(() {
_textureId = textureId;
});
});
super.initState();
}
@override
void dispose() {
_textureIdListener?.cancel();
_listener.dispose();
_resizeTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: ((context, constraints) {
if (_textureId == null) {
return widget.initial;
}
var texture = Texture(
key: ObjectKey("texture_$_textureId"),
textureId: _textureId!,
filterQuality: FilterQuality.none,
);
return SizedBox(
height: constraints.maxHeight,
width: constraints.maxWidth,
child: ResizeObserver(
onResized: (Size oldSize, Size newSize) async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!_resizing) {
setState(() {
_resizing = true;
});
}
_resizeTimer?.cancel();
_resizeTimer = Timer(Duration(milliseconds: 500), () async {
await widget.controller
.resize(newSize.width.toInt(), newSize.height.toInt());
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {
_resizing = false;
widget.onResize?.call();
});
});
});
});
},
child: _resizing
? Container()
: 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));
}));
}
}