Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
c940282b
Unverified
Commit
c940282b
authored
Jun 03, 2020
by
Justin McCandless
Committed by
GitHub
Jun 03, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
InteractiveViewer Widget (#56409)
parent
37a86c48
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
1774 additions
and
0 deletions
+1774
-0
interactive_viewer.dart
packages/flutter/lib/src/widgets/interactive_viewer.dart
+1162
-0
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
interactive_viewer_test.dart
packages/flutter/test/widgets/interactive_viewer_test.dart
+611
-0
No files found.
packages/flutter/lib/src/widgets/interactive_viewer.dart
0 → 100644
View file @
c940282b
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/physics.dart'
;
import
'package:vector_math/vector_math_64.dart'
show
Quad
,
Vector3
,
Matrix4
;
import
'basic.dart'
;
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'ticker_provider.dart'
;
/// A widget that enables pan and zoom interactions with its child.
///
/// The user can transform the child by dragging to pan or pinching to zoom.
///
/// The [child] must not be null.
///
/// {@tool dartpad --template=stateless_widget_scaffold}
/// This example shows a simple Container that can be panned and zoomed.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Center(
/// child: InteractiveViewer(
/// boundaryMargin: EdgeInsets.all(20.0),
/// minScale: 0.1,
/// maxScale: 1.6,
/// child: Container(
/// decoration: BoxDecoration(
/// gradient: LinearGradient(
/// begin: Alignment.topCenter,
/// end: Alignment.bottomCenter,
/// colors: <Color>[Colors.orange, Colors.red],
/// stops: <double>[0.0, 1.0],
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@immutable
class
InteractiveViewer
extends
StatefulWidget
{
/// Create an InteractiveViewer.
///
/// The [child] parameter must not be null.
InteractiveViewer
({
Key
key
,
this
.
boundaryMargin
=
EdgeInsets
.
zero
,
this
.
constrained
=
true
,
// These default scale values were eyeballed as reasonable limits for common
// use cases.
this
.
maxScale
=
2.5
,
this
.
minScale
=
0.8
,
this
.
onInteractionEnd
,
this
.
onInteractionStart
,
this
.
onInteractionUpdate
,
this
.
panEnabled
=
true
,
this
.
scaleEnabled
=
true
,
this
.
transformationController
,
@required
this
.
child
,
})
:
assert
(
child
!=
null
),
assert
(
constrained
!=
null
),
assert
(
minScale
!=
null
),
assert
(
minScale
>
0
),
assert
(
minScale
.
isFinite
),
assert
(
maxScale
!=
null
),
assert
(
maxScale
>
0
),
assert
(!
maxScale
.
isNaN
),
assert
(
maxScale
>=
minScale
),
assert
(
panEnabled
!=
null
),
assert
(
scaleEnabled
!=
null
),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert
((
boundaryMargin
.
horizontal
.
isInfinite
&&
boundaryMargin
.
vertical
.
isInfinite
)
||
(
boundaryMargin
.
top
.
isFinite
&&
boundaryMargin
.
right
.
isFinite
&&
boundaryMargin
.
bottom
.
isFinite
&&
boundaryMargin
.
left
.
isFinite
)),
super
(
key:
key
);
/// A margin for the visible boundaries of the child.
///
/// Any transformation that results in the viewport being able to view outside
/// of the boundaries will be stopped at the boundary. The boundaries do not
/// rotate with the rest of the scene, so they are always aligned with the
/// viewport.
///
/// To produce no boundaries at all, pass infinite [EdgeInsets], such as
/// `EdgeInsets.all(double.infinity)`.
///
/// No edge can be NaN.
///
/// Defaults to [EdgeInsets.zero], which results in boundaries that are the
/// exact same size and position as the [child].
final
EdgeInsets
boundaryMargin
;
/// The Widget to perform the transformations on.
///
/// Cannot be null.
final
Widget
child
;
/// Whether the normal size constraints at this point in the widget tree are
/// applied to the child.
///
/// If set to false, then the child will be given infinite constraints. This
/// is often useful when a child should be bigger than the InteractiveViewer.
///
/// Defaults to true.
///
/// {@tool dartpad --template=stateless_widget_scaffold}
/// This example shows how to create a pannable table. Because the table is
/// larger than the entire screen, setting `constrained` to false is necessary
/// to allow it to be drawn to its full size. The parts of the table that
/// exceed the screen size can then be panned into view.
///
/// ```dart
/// Widget build(BuildContext context) {
/// const int _rowCount = 20;
/// const int _columnCount = 3;
///
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Pannable Table'),
/// ),
/// body: InteractiveViewer(
/// constrained: false,
/// scaleEnabled: false,
/// child: Table(
/// columnWidths: <int, TableColumnWidth>{
/// for (int column = 0; column < _columnCount; column += 1)
/// column: const FixedColumnWidth(300.0),
/// },
/// children: <TableRow>[
/// for (int row = 0; row < _rowCount; row += 1)
/// TableRow(
/// children: <Widget>[
/// for (int column = 0; column < _columnCount; column += 1)
/// Container(
/// height: 100,
/// color: row % 2 + column % 2 == 1 ? Colors.red : Colors.green,
/// ),
/// ],
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final
bool
constrained
;
/// If false, the user will be prevented from panning.
///
/// Defaults to true.
///
/// See also:
///
/// * [scaleEnabled], which is similar but for scale.
final
bool
panEnabled
;
/// If false, the user will be prevented from scaling.
///
/// Defaults to true.
///
/// See also:
///
/// * [panEnabled], which is similar but for panning.
final
bool
scaleEnabled
;
/// The maximum allowed scale.
///
/// The scale will be clamped between this and [minScale] inclusively.
///
/// Defaults to 2.5.
///
/// Cannot be null, and must be greater than zero and greater than minScale.
final
double
maxScale
;
/// The minimum allowed scale.
///
/// The scale will be clamped between this and [maxScale] inclusively.
///
/// Defaults to 0.8.
///
/// Cannot be null, and must be a finite number greater than zero and less
/// than maxScale.
final
double
minScale
;
/// Called when the user ends a pan or scale gesture on the widget.
///
/// {@template flutter.widgets.interactiveViewer.onInteraction}
/// Will be called even if the interaction is disabled with
/// [panEnabled] or [scaleEnabled].
///
/// A [GestureDetector] wrapping the InteractiveViewer will not respond to
/// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and
/// [GestureDetector.onScaleEnd]. Use [onInteractionStart],
/// [onInteractionUpdate], and [onInteractionEnd] to respond to those
/// gestures.
///
/// The coordinates returned in the details are viewport coordinates relative
/// to the parent. See [TransformationController.toScene] for how to
/// convert the coordinates to scene coordinates relative to the child.
/// {@endtemplate}
///
/// See also:
///
/// * [onInteractionStart], which handles the start of the same interaction.
/// * [onInteractionUpdate], which handles an update to the same interaction.
final
GestureScaleEndCallback
onInteractionEnd
;
/// Called when the user begins a pan or scale gesture on the widget.
///
/// {@macro flutter.widgets.interactiveViewer.onInteraction}
///
/// See also:
///
/// * [onInteractionUpdate], which handles an update to the same interaction.
/// * [onInteractionEnd], which handles the end of the same interaction.
final
GestureScaleStartCallback
onInteractionStart
;
/// Called when the user updates a pan or scale gesture on the widget.
///
/// {@macro flutter.widgets.interactiveViewer.onInteraction}
///
/// See also:
///
/// * [onInteractionStart], which handles the start of the same interaction.
/// * [onInteractionEnd], which handles the end of the same interaction.
final
GestureScaleUpdateCallback
onInteractionUpdate
;
/// A [TransformationController] for the transformation performed on the
/// child.
///
/// Whenever the child is transformed, the [Matrix4] value is updated and all
/// listeners are notified. If the value is set, InteractiveViewer will update
/// to respect the new value.
///
/// {@tool dartpad --template=stateful_widget_material_ticker}
/// This example shows how transformationController can be used to animate the
/// transformation back to its starting position.
///
/// ```dart
/// final TransformationController _transformationController = TransformationController();
/// Animation<Matrix4> _animationReset;
/// AnimationController _controllerReset;
///
/// void _onAnimateReset() {
/// _transformationController.value = _animationReset.value;
/// if (!_controllerReset.isAnimating) {
/// _animationReset?.removeListener(_onAnimateReset);
/// _animationReset = null;
/// _controllerReset.reset();
/// }
/// }
///
/// void _animateResetInitialize() {
/// _controllerReset.reset();
/// _animationReset = Matrix4Tween(
/// begin: _transformationController.value,
/// end: Matrix4.identity(),
/// ).animate(_controllerReset);
/// _animationReset.addListener(_onAnimateReset);
/// _controllerReset.forward();
/// }
///
/// // Stop a running reset to home transform animation.
/// void _animateResetStop() {
/// _controllerReset.stop();
/// _animationReset?.removeListener(_onAnimateReset);
/// _animationReset = null;
/// _controllerReset.reset();
/// }
///
/// void _onInteractionStart(ScaleStartDetails details) {
/// // If the user tries to cause a transformation while the reset animation is
/// // running, cancel the reset animation.
/// if (_controllerReset.status == AnimationStatus.forward) {
/// _animateResetStop();
/// }
/// }
///
/// @override
/// void initState() {
/// super.initState();
/// _controllerReset = AnimationController(
/// vsync: this,
/// duration: const Duration(milliseconds: 400),
/// );
/// }
///
/// @override
/// void dispose() {
/// _controllerReset.dispose();
/// super.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// backgroundColor: Theme.of(context).colorScheme.primary,
/// appBar: AppBar(
/// automaticallyImplyLeading: false,
/// title: const Text('Controller demo'),
/// ),
/// body: Center(
/// child: InteractiveViewer(
/// boundaryMargin: EdgeInsets.all(double.infinity),
/// transformationController: _transformationController,
/// minScale: 0.1,
/// maxScale: 1.0,
/// onInteractionStart: _onInteractionStart,
/// child: Container(
/// decoration: BoxDecoration(
/// gradient: LinearGradient(
/// begin: Alignment.topCenter,
/// end: Alignment.bottomCenter,
/// colors: <Color>[Colors.orange, Colors.red],
/// stops: <double>[0.0, 1.0],
/// ),
/// ),
/// ),
/// ),
/// ),
/// persistentFooterButtons: [
/// IconButton(
/// onPressed: _animateResetInitialize,
/// tooltip: 'Reset',
/// color: Theme.of(context).colorScheme.surface,
/// icon: const Icon(Icons.replay),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [ValueNotifier], the parent class of TransformationController.
/// * [TextEditingController] for an example of another similar pattern.
final
TransformationController
transformationController
;
/// Returns the closest point to the given point on the given line segment.
@visibleForTesting
static
Vector3
getNearestPointOnLine
(
Vector3
point
,
Vector3
l1
,
Vector3
l2
)
{
final
double
lengthSquared
=
math
.
pow
(
l2
.
x
-
l1
.
x
,
2.0
).
toDouble
()
+
math
.
pow
(
l2
.
y
-
l1
.
y
,
2.0
).
toDouble
();
// In this case, l1 == l2.
if
(
lengthSquared
==
0
)
{
return
l1
;
}
// Calculate how far down the line segment the closest point is and return
// the point.
final
Vector3
l1P
=
point
-
l1
;
final
Vector3
l1L2
=
l2
-
l1
;
final
double
fraction
=
(
l1P
.
dot
(
l1L2
)
/
lengthSquared
).
clamp
(
0.0
,
1.0
).
toDouble
();
return
l1
+
l1L2
*
fraction
;
}
/// Given a quad, return its axis aligned bounding box.
@visibleForTesting
static
Quad
getAxisAlignedBoundingBox
(
Quad
quad
)
{
final
double
minX
=
math
.
min
(
quad
.
point0
.
x
,
math
.
min
(
quad
.
point1
.
x
,
math
.
min
(
quad
.
point2
.
x
,
quad
.
point3
.
x
,
),
),
);
final
double
minY
=
math
.
min
(
quad
.
point0
.
y
,
math
.
min
(
quad
.
point1
.
y
,
math
.
min
(
quad
.
point2
.
y
,
quad
.
point3
.
y
,
),
),
);
final
double
maxX
=
math
.
max
(
quad
.
point0
.
x
,
math
.
max
(
quad
.
point1
.
x
,
math
.
max
(
quad
.
point2
.
x
,
quad
.
point3
.
x
,
),
),
);
final
double
maxY
=
math
.
max
(
quad
.
point0
.
y
,
math
.
max
(
quad
.
point1
.
y
,
math
.
max
(
quad
.
point2
.
y
,
quad
.
point3
.
y
,
),
),
);
return
Quad
.
points
(
Vector3
(
minX
,
minY
,
0
),
Vector3
(
maxX
,
minY
,
0
),
Vector3
(
maxX
,
maxY
,
0
),
Vector3
(
minX
,
maxY
,
0
),
);
}
/// Returns true iff the point is inside the rectangle given by the Quad,
/// inclusively.
/// Algorithm from https://math.stackexchange.com/a/190373.
@visibleForTesting
static
bool
pointIsInside
(
Vector3
point
,
Quad
quad
)
{
final
Vector3
aM
=
point
-
quad
.
point0
;
final
Vector3
aB
=
quad
.
point1
-
quad
.
point0
;
final
Vector3
aD
=
quad
.
point3
-
quad
.
point0
;
final
double
aMAB
=
aM
.
dot
(
aB
);
final
double
aBAB
=
aB
.
dot
(
aB
);
final
double
aMAD
=
aM
.
dot
(
aD
);
final
double
aDAD
=
aD
.
dot
(
aD
);
return
0
<=
aMAB
&&
aMAB
<=
aBAB
&&
0
<=
aMAD
&&
aMAD
<=
aDAD
;
}
/// Get the point inside (inclusively) the given Quad that is nearest to the
/// given Vector3.
@visibleForTesting
static
Vector3
getNearestPointInside
(
Vector3
point
,
Quad
quad
)
{
// If the point is inside the axis aligned bounding box, then it's ok where
// it is.
if
(
pointIsInside
(
point
,
quad
))
{
return
point
;
}
// Otherwise, return the nearest point on the quad.
final
List
<
Vector3
>
closestPoints
=
<
Vector3
>[
InteractiveViewer
.
getNearestPointOnLine
(
point
,
quad
.
point0
,
quad
.
point1
),
InteractiveViewer
.
getNearestPointOnLine
(
point
,
quad
.
point1
,
quad
.
point2
),
InteractiveViewer
.
getNearestPointOnLine
(
point
,
quad
.
point2
,
quad
.
point3
),
InteractiveViewer
.
getNearestPointOnLine
(
point
,
quad
.
point3
,
quad
.
point0
),
];
double
minDistance
=
double
.
infinity
;
Vector3
closestOverall
;
for
(
final
Vector3
closePoint
in
closestPoints
)
{
final
double
distance
=
math
.
sqrt
(
math
.
pow
(
point
.
x
-
closePoint
.
x
,
2
)
+
math
.
pow
(
point
.
y
-
closePoint
.
y
,
2
),
);
if
(
distance
<
minDistance
)
{
minDistance
=
distance
;
closestOverall
=
closePoint
;
}
}
return
closestOverall
;
}
@override
_InteractiveViewerState
createState
()
=>
_InteractiveViewerState
();
}
class
_InteractiveViewerState
extends
State
<
InteractiveViewer
>
with
TickerProviderStateMixin
{
TransformationController
_transformationController
;
final
GlobalKey
_childKey
=
GlobalKey
();
final
GlobalKey
_parentKey
=
GlobalKey
();
Animation
<
Offset
>
_animation
;
AnimationController
_controller
;
Offset
_referenceFocalPoint
;
// Point where the current gesture began.
double
_scaleStart
;
// Scale value at start of scaling gesture.
double
_rotationStart
=
0.0
;
// Rotation at start of rotation gesture.
double
_currentRotation
=
0.0
;
// Rotation of _transformationController.value.
_GestureType
_gestureType
;
// TODO(justinmc): Add rotateEnabled parameter to the widget and remove this
// hardcoded value when the rotation feature is implemented.
// https://github.com/flutter/flutter/issues/57698
final
bool
_rotateEnabled
=
false
;
// Used as the coefficient of friction in the inertial translation animation.
// This value was eyeballed to give a feel similar to Google Photos.
static
const
double
_kDrag
=
0.0000135
;
// The _boundaryRect is calculated by adding the boundaryMargin to the size of
// the child.
Rect
_boundaryRectCached
;
Rect
get
_boundaryRect
{
if
(
_boundaryRectCached
!=
null
)
{
return
_boundaryRectCached
;
}
assert
(
_childKey
.
currentContext
!=
null
);
assert
(!
widget
.
boundaryMargin
.
left
.
isNaN
);
assert
(!
widget
.
boundaryMargin
.
right
.
isNaN
);
assert
(!
widget
.
boundaryMargin
.
top
.
isNaN
);
assert
(!
widget
.
boundaryMargin
.
bottom
.
isNaN
);
final
RenderBox
childRenderBox
=
_childKey
.
currentContext
.
findRenderObject
()
as
RenderBox
;
final
Size
childSize
=
childRenderBox
.
size
;
_boundaryRectCached
=
widget
.
boundaryMargin
.
inflateRect
(
Offset
.
zero
&
childSize
);
// Boundaries that are partially infinite are not allowed because Matrix4's
// rotation and translation methods don't handle infinites well.
assert
(
_boundaryRectCached
.
isFinite
||
(
_boundaryRectCached
.
left
.
isInfinite
&&
_boundaryRectCached
.
top
.
isInfinite
&&
_boundaryRectCached
.
right
.
isInfinite
&&
_boundaryRectCached
.
bottom
.
isInfinite
),
'boundaryRect must either be infinite in all directions or finite in all directions.'
);
return
_boundaryRectCached
;
}
// The Rect representing the child's parent.
Rect
get
_viewport
{
assert
(
_parentKey
.
currentContext
!=
null
);
final
RenderBox
parentRenderBox
=
_parentKey
.
currentContext
.
findRenderObject
()
as
RenderBox
;
return
Offset
.
zero
&
parentRenderBox
.
size
;
}
// Return a new matrix representing the given matrix after applying the given
// translation.
Matrix4
_matrixTranslate
(
Matrix4
matrix
,
Offset
translation
)
{
if
(
translation
==
Offset
.
zero
)
{
return
matrix
.
clone
();
}
final
Matrix4
nextMatrix
=
matrix
.
clone
()..
translate
(
translation
.
dx
,
translation
.
dy
,
);
// Transform the viewport to determine where its four corners will be after
// the child has been transformed.
final
Quad
nextViewport
=
_transformViewport
(
nextMatrix
,
_viewport
);
// If the boundaries are infinite, then no need to check if the translation
// fits within them.
if
(
_boundaryRect
.
isInfinite
)
{
return
nextMatrix
;
}
// Expand the boundaries with rotation. This prevents the problem where a
// mismatch in orientation between the viewport and boundaries effectively
// limits translation. With this approach, all points that are visible with
// no rotation are visible after rotation.
final
Quad
boundariesAabbQuad
=
_getAxisAlignedBoundingBoxWithRotation
(
_boundaryRect
,
_currentRotation
,
);
// If the given translation fits completely within the boundaries, allow it.
final
Offset
offendingDistance
=
_exceedsBy
(
boundariesAabbQuad
,
nextViewport
);
if
(
offendingDistance
==
Offset
.
zero
)
{
return
nextMatrix
;
}
// Desired translation goes out of bounds, so translate to the nearest
// in-bounds point instead.
final
Offset
nextTotalTranslation
=
_getMatrixTranslation
(
nextMatrix
);
final
double
currentScale
=
matrix
.
getMaxScaleOnAxis
();
final
Offset
correctedTotalTranslation
=
Offset
(
nextTotalTranslation
.
dx
-
offendingDistance
.
dx
*
currentScale
,
nextTotalTranslation
.
dy
-
offendingDistance
.
dy
*
currentScale
,
);
// TODO(justinmc): This needs some work to handle rotation properly. The
// idea is that the boundaries are axis aligned (boundariesAabbQuad), but
// calculating the translation to put the viewport inside that Quad is more
// complicated than this when rotated.
// https://github.com/flutter/flutter/issues/57698
final
Matrix4
correctedMatrix
=
matrix
.
clone
()..
setTranslation
(
Vector3
(
correctedTotalTranslation
.
dx
,
correctedTotalTranslation
.
dy
,
0.0
,
));
// Double check that the corrected translation fits.
final
Quad
correctedViewport
=
_transformViewport
(
correctedMatrix
,
_viewport
);
final
Offset
offendingCorrectedDistance
=
_exceedsBy
(
boundariesAabbQuad
,
correctedViewport
);
if
(
offendingCorrectedDistance
==
Offset
.
zero
)
{
return
correctedMatrix
;
}
// If the corrected translation doesn't fit in either direction, don't allow
// any translation at all. This happens when the viewport is larger than the
// entire boundary.
if
(
offendingCorrectedDistance
.
dx
!=
0.0
&&
offendingCorrectedDistance
.
dy
!=
0.0
)
{
return
matrix
.
clone
();
}
// Otherwise, allow translation in only the direction that fits. This
// happens when the viewport is larger than the boundary in one direction.
final
Offset
unidirectionalCorrectedTotalTranslation
=
Offset
(
offendingCorrectedDistance
.
dx
==
0.0
?
correctedTotalTranslation
.
dx
:
0.0
,
offendingCorrectedDistance
.
dy
==
0.0
?
correctedTotalTranslation
.
dy
:
0.0
,
);
return
matrix
.
clone
()..
setTranslation
(
Vector3
(
unidirectionalCorrectedTotalTranslation
.
dx
,
unidirectionalCorrectedTotalTranslation
.
dy
,
0.0
,
));
}
// Return a new matrix representing the given matrix after applying the given
// scale.
Matrix4
_matrixScale
(
Matrix4
matrix
,
double
scale
)
{
if
(
scale
==
1.0
)
{
return
matrix
.
clone
();
}
assert
(
scale
!=
0.0
);
// Don't allow a scale that results in an overall scale beyond min/max
// scale.
final
double
currentScale
=
_transformationController
.
value
.
getMaxScaleOnAxis
();
final
double
totalScale
=
currentScale
*
scale
;
final
double
clampedTotalScale
=
totalScale
.
clamp
(
widget
.
minScale
,
widget
.
maxScale
,
)
as
double
;
final
double
clampedScale
=
clampedTotalScale
/
currentScale
;
final
Matrix4
nextMatrix
=
matrix
.
clone
()..
scale
(
clampedScale
);
// Ensure that the scale cannot make the child so big that it can't fit
// inside the boundaries (in either direction).
final
double
minScale
=
math
.
max
(
_viewport
.
width
/
_boundaryRect
.
width
,
_viewport
.
height
/
_boundaryRect
.
height
,
);
if
(
clampedTotalScale
<
minScale
)
{
final
double
minCurrentScale
=
minScale
/
currentScale
;
return
matrix
.
clone
()..
scale
(
minCurrentScale
);
}
return
nextMatrix
;
}
// Return a new matrix representing the given matrix after applying the given
// rotation.
Matrix4
_matrixRotate
(
Matrix4
matrix
,
double
rotation
,
Offset
focalPoint
)
{
if
(
rotation
==
0
)
{
return
matrix
.
clone
();
}
final
Offset
focalPointScene
=
_transformationController
.
toScene
(
focalPoint
,
);
return
matrix
.
clone
()
..
translate
(
focalPointScene
.
dx
,
focalPointScene
.
dy
)
..
rotateZ
(-
rotation
)
..
translate
(-
focalPointScene
.
dx
,
-
focalPointScene
.
dy
);
}
// Returns true iff the given _GestureType is enabled.
bool
_gestureIsSupported
(
_GestureType
gestureType
)
{
if
(
_gestureType
==
_GestureType
.
pan
&&
!
widget
.
panEnabled
)
{
return
false
;
}
if
(
_gestureType
==
_GestureType
.
scale
&&
!
widget
.
scaleEnabled
)
{
return
false
;
}
if
(
_gestureType
==
_GestureType
.
rotate
&&
!
_rotateEnabled
)
{
return
false
;
}
return
true
;
}
// Handle the start of a gesture. All of pan, scale, and rotate are handled
// with GestureDetector's scale gesture.
void
_onScaleStart
(
ScaleStartDetails
details
)
{
if
(
widget
.
onInteractionStart
!=
null
)
{
widget
.
onInteractionStart
(
details
);
}
if
(
_controller
.
isAnimating
)
{
_controller
.
stop
();
_controller
.
reset
();
_animation
?.
removeListener
(
_onAnimate
);
_animation
=
null
;
}
_gestureType
=
null
;
_scaleStart
=
_transformationController
.
value
.
getMaxScaleOnAxis
();
_referenceFocalPoint
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
);
_rotationStart
=
_currentRotation
;
}
// Handle an update to an ongoing gesture. All of pan, scale, and rotate are
// handled with GestureDetector's scale gesture.
void
_onScaleUpdate
(
ScaleUpdateDetails
details
)
{
final
double
scale
=
_transformationController
.
value
.
getMaxScaleOnAxis
();
if
(
widget
.
onInteractionUpdate
!=
null
)
{
widget
.
onInteractionUpdate
(
ScaleUpdateDetails
(
focalPoint:
_transformationController
.
toScene
(
details
.
localFocalPoint
,
),
scale:
details
.
scale
,
rotation:
details
.
rotation
,
));
}
final
Offset
focalPointScene
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
);
_gestureType
??=
_getGestureType
(
!
widget
.
scaleEnabled
?
1.0
:
details
.
scale
,
!
_rotateEnabled
?
0.0
:
details
.
rotation
,
);
if
(!
_gestureIsSupported
(
_gestureType
))
{
return
;
}
switch
(
_gestureType
)
{
case
_GestureType
.
scale
:
if
(
_scaleStart
==
null
)
{
return
;
}
// details.scale gives us the amount to change the scale as of the
// start of this gesture, so calculate the amount to scale as of the
// previous call to _onScaleUpdate.
final
double
desiredScale
=
_scaleStart
*
details
.
scale
;
final
double
scaleChange
=
desiredScale
/
scale
;
_transformationController
.
value
=
_matrixScale
(
_transformationController
.
value
,
scaleChange
,
);
// While scaling, translate such that the user's two fingers stay on
// the same places in the scene. That means that the focal point of
// the scale should be on the same place in the scene before and after
// the scale.
final
Offset
focalPointSceneScaled
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
);
_transformationController
.
value
=
_matrixTranslate
(
_transformationController
.
value
,
focalPointSceneScaled
-
_referenceFocalPoint
,
);
// details.localFocalPoint should now be at the same location as the
// original _referenceFocalPoint point. If it's not, that's because
// the translate came in contact with a boundary. In that case, update
// _referenceFocalPoint so subsequent updates happen in relation to
// the new effective focal point.
final
Offset
focalPointSceneCheck
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
);
if
(
_round
(
_referenceFocalPoint
)
!=
_round
(
focalPointSceneCheck
))
{
_referenceFocalPoint
=
focalPointSceneCheck
;
}
return
;
case
_GestureType
.
rotate
:
if
(
details
.
rotation
==
0.0
)
{
return
;
}
final
double
desiredRotation
=
_rotationStart
+
details
.
rotation
;
_transformationController
.
value
=
_matrixRotate
(
_transformationController
.
value
,
_currentRotation
-
desiredRotation
,
details
.
localFocalPoint
,
);
_currentRotation
=
desiredRotation
;
return
;
case
_GestureType
.
pan
:
if
(
_referenceFocalPoint
==
null
||
details
.
scale
!=
1.0
)
{
return
;
}
// Translate so that the same point in the scene is underneath the
// focal point before and after the movement.
final
Offset
translationChange
=
focalPointScene
-
_referenceFocalPoint
;
_transformationController
.
value
=
_matrixTranslate
(
_transformationController
.
value
,
translationChange
,
);
_referenceFocalPoint
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
);
return
;
}
}
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
// are handled with GestureDetector's scale gesture.
void
_onScaleEnd
(
ScaleEndDetails
details
)
{
if
(
widget
.
onInteractionEnd
!=
null
)
{
widget
.
onInteractionEnd
(
details
);
}
_scaleStart
=
null
;
_rotationStart
=
null
;
_referenceFocalPoint
=
null
;
_animation
?.
removeListener
(
_onAnimate
);
_controller
.
reset
();
if
(!
_gestureIsSupported
(
_gestureType
))
{
return
;
}
// If the scale ended with enough velocity, animate inertial movement.
if
(
_gestureType
!=
_GestureType
.
pan
||
details
.
velocity
.
pixelsPerSecond
.
distance
<
kMinFlingVelocity
)
{
return
;
}
final
Vector3
translationVector
=
_transformationController
.
value
.
getTranslation
();
final
Offset
translation
=
Offset
(
translationVector
.
x
,
translationVector
.
y
);
final
FrictionSimulation
frictionSimulationX
=
FrictionSimulation
(
_kDrag
,
translation
.
dx
,
details
.
velocity
.
pixelsPerSecond
.
dx
,
);
final
FrictionSimulation
frictionSimulationY
=
FrictionSimulation
(
_kDrag
,
translation
.
dy
,
details
.
velocity
.
pixelsPerSecond
.
dy
,
);
final
double
tFinal
=
_getFinalTime
(
details
.
velocity
.
pixelsPerSecond
.
distance
,
_kDrag
,
);
_animation
=
Tween
<
Offset
>(
begin:
translation
,
end:
Offset
(
frictionSimulationX
.
finalX
,
frictionSimulationY
.
finalX
),
).
animate
(
CurvedAnimation
(
parent:
_controller
,
curve:
Curves
.
decelerate
,
));
_controller
.
duration
=
Duration
(
milliseconds:
(
tFinal
*
1000
).
round
());
_animation
.
addListener
(
_onAnimate
);
_controller
.
forward
();
}
// Handle mousewheel scroll events.
void
_receivedPointerSignal
(
PointerSignalEvent
event
)
{
if
(
event
is
PointerScrollEvent
)
{
final
RenderBox
childRenderBox
=
_childKey
.
currentContext
.
findRenderObject
()
as
RenderBox
;
final
Size
childSize
=
childRenderBox
.
size
;
final
double
scaleChange
=
1.0
+
event
.
scrollDelta
.
dy
/
childSize
.
height
;
if
(
scaleChange
==
0.0
)
{
return
;
}
final
Offset
focalPointScene
=
_transformationController
.
toScene
(
event
.
localPosition
,
);
_transformationController
.
value
=
_matrixScale
(
_transformationController
.
value
,
scaleChange
,
);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final
Offset
focalPointSceneScaled
=
_transformationController
.
toScene
(
event
.
localPosition
,
);
_transformationController
.
value
=
_matrixTranslate
(
_transformationController
.
value
,
focalPointSceneScaled
-
focalPointScene
,
);
}
}
// Handle inertia drag animation.
void
_onAnimate
()
{
if
(!
_controller
.
isAnimating
)
{
_animation
?.
removeListener
(
_onAnimate
);
_animation
=
null
;
_controller
.
reset
();
return
;
}
// Translate such that the resulting translation is _animation.value.
final
Vector3
translationVector
=
_transformationController
.
value
.
getTranslation
();
final
Offset
translation
=
Offset
(
translationVector
.
x
,
translationVector
.
y
);
final
Offset
translationScene
=
_transformationController
.
toScene
(
translation
,
);
final
Offset
animationScene
=
_transformationController
.
toScene
(
_animation
.
value
,
);
final
Offset
translationChangeScene
=
animationScene
-
translationScene
;
_transformationController
.
value
=
_matrixTranslate
(
_transformationController
.
value
,
translationChangeScene
,
);
}
void
_onTransformationControllerChange
()
{
// A change to the TransformationController's value is a change to the
// state.
setState
(()
{});
}
@override
void
initState
()
{
super
.
initState
();
_transformationController
=
widget
.
transformationController
??
TransformationController
();
_transformationController
.
addListener
(
_onTransformationControllerChange
);
_controller
=
AnimationController
(
vsync:
this
,
);
}
@override
void
didUpdateWidget
(
InteractiveViewer
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
child
!=
oldWidget
.
child
||
widget
.
boundaryMargin
!=
oldWidget
.
boundaryMargin
)
{
_boundaryRectCached
=
null
;
}
// Handle all cases of needing to dispose and initialize
// transformationControllers.
if
(
oldWidget
.
transformationController
==
null
)
{
if
(
widget
.
transformationController
!=
null
)
{
_transformationController
.
removeListener
(
_onTransformationControllerChange
);
_transformationController
.
dispose
();
_transformationController
=
widget
.
transformationController
;
_transformationController
.
addListener
(
_onTransformationControllerChange
);
}
}
else
{
if
(
widget
.
transformationController
==
null
)
{
_transformationController
.
removeListener
(
_onTransformationControllerChange
);
_transformationController
=
TransformationController
();
_transformationController
.
addListener
(
_onTransformationControllerChange
);
}
else
if
(
widget
.
transformationController
!=
oldWidget
.
transformationController
)
{
_transformationController
.
removeListener
(
_onTransformationControllerChange
);
_transformationController
=
widget
.
transformationController
;
_transformationController
.
addListener
(
_onTransformationControllerChange
);
}
}
}
@override
void
dispose
()
{
_controller
.
dispose
();
_transformationController
.
removeListener
(
_onTransformationControllerChange
);
if
(
widget
.
transformationController
==
null
)
{
_transformationController
.
dispose
();
}
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
Widget
child
=
Transform
(
transform:
_transformationController
.
value
,
child:
KeyedSubtree
(
key:
_childKey
,
child:
widget
.
child
,
),
);
if
(!
widget
.
constrained
)
{
child
=
ClipRect
(
child:
OverflowBox
(
alignment:
Alignment
.
topLeft
,
minWidth:
0.0
,
minHeight:
0.0
,
maxWidth:
double
.
infinity
,
maxHeight:
double
.
infinity
,
child:
child
,
),
);
}
// A GestureDetector allows the detection of panning and zooming gestures on
// the child.
return
Listener
(
key:
_parentKey
,
onPointerSignal:
_receivedPointerSignal
,
child:
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
// Necessary when panning off screen.
onScaleEnd:
_onScaleEnd
,
onScaleStart:
_onScaleStart
,
onScaleUpdate:
_onScaleUpdate
,
child:
child
,
),
);
}
}
/// A thin wrapper on [ValueNotifier] whose value is a [Matrix4] representing a
/// transformation.
///
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
///
/// See also:
///
/// * [InteractiveViewer.transformationController] for detailed documentation
/// on how to use TransformationController with [InteractiveViewer].
class
TransformationController
extends
ValueNotifier
<
Matrix4
>
{
/// Create an instance of [TransformationController].
///
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
TransformationController
([
Matrix4
value
])
:
super
(
value
??
Matrix4
.
identity
());
/// Return the scene point at the given viewport point.
///
/// A viewport point is relative to the parent while a scene point is relative
/// to the child, regardless of transformation. Calling toScene with a
/// viewport point essentially returns the scene coordinate that lies
/// underneath the viewport point given the transform.
///
/// The viewport transforms as the inverse of the child (i.e. moving the child
/// left is equivalent to moving the viewport right).
///
/// This method is often useful when determining where an event on the parent
/// occurs on the child. This example shows how to determine where a tap on
/// the parent occurred on the child.
///
/// ```dart
/// @override
/// void build(BuildContext context) {
/// return GestureDetector(
/// onTapUp: (TapUpDetails details) {
/// _childWasTappedAt = _transformationController.toScene(
/// details.localPosition,
/// );
/// },
/// child: InteractiveViewer(
/// transformationController: _transformationController,
/// child: child,
/// ),
/// );
/// }
/// ```
Offset
toScene
(
Offset
viewportPoint
)
{
// On viewportPoint, perform the inverse transformation of the scene to get
// where the point would be in the scene before the transformation.
final
Matrix4
inverseMatrix
=
Matrix4
.
inverted
(
value
);
final
Vector3
untransformed
=
inverseMatrix
.
transform3
(
Vector3
(
viewportPoint
.
dx
,
viewportPoint
.
dy
,
0
,
));
return
Offset
(
untransformed
.
x
,
untransformed
.
y
);
}
}
// A classification of relevant user gestures. Each contiguous user gesture is
// represented by exactly one _GestureType.
enum
_GestureType
{
pan
,
scale
,
rotate
,
}
// Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless.
double
_getFinalTime
(
double
velocity
,
double
drag
)
{
const
double
effectivelyMotionless
=
10.0
;
return
math
.
log
(
effectivelyMotionless
/
velocity
)
/
math
.
log
(
drag
/
100
);
}
// Decide which type of gesture this is by comparing the amount of scale
// and rotation in the gesture, if any. Scale starts at 1 and rotation
// starts at 0. Pan will have 0 scale and 0 rotation because it uses only one
// finger.
_GestureType
_getGestureType
(
double
scale
,
double
rotation
)
{
if
((
scale
-
1
).
abs
()
>
rotation
.
abs
())
{
return
_GestureType
.
scale
;
}
else
if
(
rotation
!=
0
)
{
return
_GestureType
.
rotate
;
}
else
{
return
_GestureType
.
pan
;
}
}
// Return the translation from the given Matrix4 as an Offset.
Offset
_getMatrixTranslation
(
Matrix4
matrix
)
{
final
Vector3
nextTranslation
=
matrix
.
getTranslation
();
return
Offset
(
nextTranslation
.
x
,
nextTranslation
.
y
);
}
// Transform the four corners of the viewport by the inverse of the given
// matrix. This gives the viewport after the child has been transformed by the
// given matrix. The viewport transforms as the inverse of the child (i.e.
// moving the child left is equivalent to moving the viewport right).
Quad
_transformViewport
(
Matrix4
matrix
,
Rect
viewport
)
{
final
Matrix4
inverseMatrix
=
matrix
.
clone
()..
invert
();
return
Quad
.
points
(
inverseMatrix
.
transform3
(
Vector3
(
viewport
.
topLeft
.
dx
,
viewport
.
topLeft
.
dy
,
0.0
,
)),
inverseMatrix
.
transform3
(
Vector3
(
viewport
.
topRight
.
dx
,
viewport
.
topRight
.
dy
,
0.0
,
)),
inverseMatrix
.
transform3
(
Vector3
(
viewport
.
bottomRight
.
dx
,
viewport
.
bottomRight
.
dy
,
0.0
,
)),
inverseMatrix
.
transform3
(
Vector3
(
viewport
.
bottomLeft
.
dx
,
viewport
.
bottomLeft
.
dy
,
0.0
,
)),
);
}
// Find the axis aligned bounding box for the rect rotated about its center by
// the given amount.
Quad
_getAxisAlignedBoundingBoxWithRotation
(
Rect
rect
,
double
rotation
)
{
final
Matrix4
rotationMatrix
=
Matrix4
.
identity
()
..
translate
(
rect
.
size
.
width
/
2
,
rect
.
size
.
height
/
2
)
..
rotateZ
(
rotation
)
..
translate
(-
rect
.
size
.
width
/
2
,
-
rect
.
size
.
height
/
2
);
final
Quad
boundariesRotated
=
Quad
.
points
(
rotationMatrix
.
transform3
(
Vector3
(
rect
.
left
,
rect
.
top
,
0.0
)),
rotationMatrix
.
transform3
(
Vector3
(
rect
.
right
,
rect
.
top
,
0.0
)),
rotationMatrix
.
transform3
(
Vector3
(
rect
.
right
,
rect
.
bottom
,
0.0
)),
rotationMatrix
.
transform3
(
Vector3
(
rect
.
left
,
rect
.
bottom
,
0.0
)),
);
return
InteractiveViewer
.
getAxisAlignedBoundingBox
(
boundariesRotated
);
}
// Return the amount that viewport lies outside of boundary. If the viewport
// is completely contained within the boundary (inclusively), then returns
// Offset.zero.
Offset
_exceedsBy
(
Quad
boundary
,
Quad
viewport
)
{
final
List
<
Vector3
>
viewportPoints
=
<
Vector3
>[
viewport
.
point0
,
viewport
.
point1
,
viewport
.
point2
,
viewport
.
point3
,
];
Offset
largestExcess
=
Offset
.
zero
;
for
(
final
Vector3
point
in
viewportPoints
)
{
final
Vector3
pointInside
=
InteractiveViewer
.
getNearestPointInside
(
point
,
boundary
);
final
Offset
excess
=
Offset
(
pointInside
.
x
-
point
.
x
,
pointInside
.
y
-
point
.
y
,
);
if
(
excess
.
dx
.
abs
()
>
largestExcess
.
dx
.
abs
())
{
largestExcess
=
Offset
(
excess
.
dx
,
largestExcess
.
dy
);
}
if
(
excess
.
dy
.
abs
()
>
largestExcess
.
dy
.
abs
())
{
largestExcess
=
Offset
(
largestExcess
.
dx
,
excess
.
dy
);
}
}
return
_round
(
largestExcess
);
}
// Round the output values. This works around a precision problem where
// values that should have been zero were given as within 10^-10 of zero.
Offset
_round
(
Offset
offset
)
{
return
Offset
(
double
.
parse
(
offset
.
dx
.
toStringAsFixed
(
9
)),
double
.
parse
(
offset
.
dy
.
toStringAsFixed
(
9
)),
);
}
packages/flutter/lib/widgets.dart
View file @
c940282b
...
@@ -56,6 +56,7 @@ export 'src/widgets/implicit_animations.dart';
...
@@ -56,6 +56,7 @@ export 'src/widgets/implicit_animations.dart';
export
'src/widgets/inherited_model.dart'
;
export
'src/widgets/inherited_model.dart'
;
export
'src/widgets/inherited_notifier.dart'
;
export
'src/widgets/inherited_notifier.dart'
;
export
'src/widgets/inherited_theme.dart'
;
export
'src/widgets/inherited_theme.dart'
;
export
'src/widgets/interactive_viewer.dart'
;
export
'src/widgets/layout_builder.dart'
;
export
'src/widgets/layout_builder.dart'
;
export
'src/widgets/list_wheel_scroll_view.dart'
;
export
'src/widgets/list_wheel_scroll_view.dart'
;
export
'src/widgets/localizations.dart'
;
export
'src/widgets/localizations.dart'
;
...
...
packages/flutter/test/widgets/interactive_viewer_test.dart
0 → 100644
View file @
c940282b
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:vector_math/vector_math_64.dart'
show
Quad
,
Vector3
,
Matrix4
;
void
main
(
)
{
group
(
'InteractiveViewer'
,
()
{
testWidgets
(
'child fits in viewport'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Attempting to drag to pan doesn't work because the child fits inside
// the viewport and has a tight boundary.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
20.0
,
);
TestGesture
gesture
=
await
tester
.
startGesture
(
childInterior
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Pinch to zoom works.
final
Offset
scaleStart1
=
childInterior
;
final
Offset
scaleStart2
=
Offset
(
childInterior
.
dx
+
10.0
,
childInterior
.
dy
);
final
Offset
scaleEnd1
=
Offset
(
childInterior
.
dx
-
10.0
,
childInterior
.
dy
);
final
Offset
scaleEnd2
=
Offset
(
childInterior
.
dx
+
20.0
,
childInterior
.
dy
);
gesture
=
await
tester
.
createGesture
();
final
TestGesture
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
isNot
(
equals
(
Matrix4
.
identity
())));
});
testWidgets
(
'boundary slightly bigger than child'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
boundaryMargin
=
10.0
;
const
double
minScale
=
0.8
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
boundaryMargin:
const
EdgeInsets
.
all
(
boundaryMargin
),
minScale:
minScale
,
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Dragging to pan works only until it hits the boundary.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
20.0
,
);
TestGesture
gesture
=
await
tester
.
startGesture
(
childInterior
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
-
boundaryMargin
);
expect
(
translation
.
y
,
-
boundaryMargin
);
// Pinch to zoom also only works until expanding to the boundary.
final
Offset
scaleStart1
=
childInterior
;
final
Offset
scaleStart2
=
Offset
(
childInterior
.
dx
+
20.0
,
childInterior
.
dy
);
final
Offset
scaleEnd1
=
Offset
(
scaleStart1
.
dx
+
5.0
,
scaleStart1
.
dy
);
final
Offset
scaleEnd2
=
Offset
(
scaleStart2
.
dx
-
5.0
,
scaleStart2
.
dy
);
gesture
=
await
tester
.
createGesture
();
final
TestGesture
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
// The new scale is the scale that makes the original size (200.0) as big
// as the boundary (220.0).
expect
(
transformationController
.
value
.
getMaxScaleOnAxis
(),
200.0
/
220.0
);
});
testWidgets
(
'child bigger than viewport'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
constrained:
false
,
scaleEnabled:
false
,
transformationController:
transformationController
,
child:
Container
(
width:
2000.0
,
height:
2000.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Attempting to move against the boundary doesn't work.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
20.0
,
);
TestGesture
gesture
=
await
tester
.
startGesture
(
childOffset
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childInterior
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Attempting to pinch to zoom doens't work because it's disabled.
final
Offset
scaleStart1
=
childInterior
;
final
Offset
scaleStart2
=
Offset
(
childInterior
.
dx
+
10.0
,
childInterior
.
dy
);
final
Offset
scaleEnd1
=
Offset
(
childInterior
.
dx
-
10.0
,
childInterior
.
dy
);
final
Offset
scaleEnd2
=
Offset
(
childInterior
.
dx
+
20.0
,
childInterior
.
dy
);
gesture
=
await
tester
.
startGesture
(
scaleStart1
);
TestGesture
gesture2
=
await
tester
.
startGesture
(
scaleStart2
);
addTearDown
(
gesture2
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Attempting to pinch to rotate doesn't work because it's disabled.
final
Offset
rotateStart1
=
childInterior
;
final
Offset
rotateStart2
=
Offset
(
childInterior
.
dx
+
10.0
,
childInterior
.
dy
);
final
Offset
rotateEnd1
=
Offset
(
childInterior
.
dx
+
5.0
,
childInterior
.
dy
+
5.0
);
final
Offset
rotateEnd2
=
Offset
(
childInterior
.
dx
-
5.0
,
childInterior
.
dy
-
5.0
);
gesture
=
await
tester
.
startGesture
(
rotateStart1
);
gesture2
=
await
tester
.
startGesture
(
rotateStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
rotateEnd1
);
await
gesture2
.
moveTo
(
rotateEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Drag to pan away from the boundary.
gesture
=
await
tester
.
startGesture
(
childInterior
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
,
isNot
(
equals
(
Matrix4
.
identity
())));
});
testWidgets
(
'no boundary'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
minScale
=
0.8
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
boundaryMargin:
const
EdgeInsets
.
all
(
double
.
infinity
),
minScale:
minScale
,
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Drag to pan works because even though the viewport fits perfectly
// around the child, there is no boundary.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
20.0
,
);
TestGesture
gesture
=
await
tester
.
startGesture
(
childInterior
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
childOffset
.
dx
-
childInterior
.
dx
);
expect
(
translation
.
y
,
childOffset
.
dy
-
childInterior
.
dy
);
// It's also possible to zoom out and view beyond the child because there
// is no boundary.
final
Offset
scaleStart1
=
childInterior
;
final
Offset
scaleStart2
=
Offset
(
childInterior
.
dx
+
20.0
,
childInterior
.
dy
);
final
Offset
scaleEnd1
=
Offset
(
childInterior
.
dx
+
5.0
,
childInterior
.
dy
);
final
Offset
scaleEnd2
=
Offset
(
childInterior
.
dx
-
5.0
,
childInterior
.
dy
);
gesture
=
await
tester
.
createGesture
();
final
TestGesture
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
.
getMaxScaleOnAxis
(),
minScale
);
});
testWidgets
(
'inertia fling and boundary sliding'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
boundaryMargin
=
50.0
;
const
double
minScale
=
0.8
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
boundaryMargin:
const
EdgeInsets
.
all
(
boundaryMargin
),
minScale:
minScale
,
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
// Fling the child.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
const
Offset
flingEnd
=
Offset
(
20.0
,
15.0
);
await
tester
.
flingFrom
(
childOffset
,
flingEnd
,
1000.0
);
await
tester
.
pump
();
// Immediately after the gesture, the child has moved to exactly follow
// the gesture.
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
flingEnd
.
dx
);
expect
(
translation
.
y
,
flingEnd
.
dy
);
// A short time after the gesture was released, it continues to move with
// inertia.
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
greaterThan
(
20.0
));
expect
(
translation
.
y
,
greaterThan
(
10.0
));
expect
(
translation
.
x
,
lessThan
(
boundaryMargin
));
expect
(
translation
.
y
,
lessThan
(
boundaryMargin
));
// It hits the boundary in the x direction first.
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
closeTo
(
boundaryMargin
,
.
000000001
));
expect
(
translation
.
y
,
lessThan
(
boundaryMargin
));
final
double
yWhenXHits
=
translation
.
y
;
// x is held to the boundary while y slides along.
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
closeTo
(
boundaryMargin
,
.
000000001
));
expect
(
translation
.
y
,
greaterThan
(
yWhenXHits
));
expect
(
translation
.
y
,
lessThan
(
boundaryMargin
));
// Eventually it ends up in the corner.
await
tester
.
pumpAndSettle
();
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
closeTo
(
boundaryMargin
,
.
000000001
));
expect
(
translation
.
y
,
closeTo
(
boundaryMargin
,
.
000000001
));
});
testWidgets
(
'Scaling automatically causes a centering translation'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
boundaryMargin
=
50.0
;
const
double
minScale
=
0.1
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
boundaryMargin:
const
EdgeInsets
.
all
(
boundaryMargin
),
minScale:
minScale
,
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
0.0
);
expect
(
translation
.
y
,
0.0
);
// Pan into the corner of the boundaries.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
const
Offset
flingEnd
=
Offset
(
20.0
,
15.0
);
await
tester
.
flingFrom
(
childOffset
,
flingEnd
,
1000.0
);
await
tester
.
pumpAndSettle
();
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
closeTo
(
boundaryMargin
,
.
000000001
));
expect
(
translation
.
y
,
closeTo
(
boundaryMargin
,
.
000000001
));
// Zoom out so the entire child is visible. The child will also be
// translated in order to keep it inside the boundaries.
final
Offset
childCenter
=
tester
.
getCenter
(
find
.
byType
(
Container
));
Offset
scaleStart1
=
Offset
(
childCenter
.
dx
-
40.0
,
childCenter
.
dy
);
Offset
scaleStart2
=
Offset
(
childCenter
.
dx
+
40.0
,
childCenter
.
dy
);
Offset
scaleEnd1
=
Offset
(
childCenter
.
dx
-
10.0
,
childCenter
.
dy
);
Offset
scaleEnd2
=
Offset
(
childCenter
.
dx
+
10.0
,
childCenter
.
dy
);
TestGesture
gesture
=
await
tester
.
createGesture
();
TestGesture
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
.
getMaxScaleOnAxis
(),
lessThan
(
1.0
));
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
lessThan
(
boundaryMargin
));
expect
(
translation
.
y
,
lessThan
(
boundaryMargin
));
expect
(
translation
.
x
,
greaterThan
(
0.0
));
expect
(
translation
.
y
,
greaterThan
(
0.0
));
expect
(
translation
.
x
,
closeTo
(
translation
.
y
,
.
000000001
));
// Zoom in on a point that's not the center, and see that it remains at
// roughly the same location in the viewport after the zoom.
scaleStart1
=
Offset
(
childCenter
.
dx
-
50.0
,
childCenter
.
dy
);
scaleStart2
=
Offset
(
childCenter
.
dx
-
30.0
,
childCenter
.
dy
);
scaleEnd1
=
Offset
(
childCenter
.
dx
-
51.0
,
childCenter
.
dy
);
scaleEnd2
=
Offset
(
childCenter
.
dx
-
29.0
,
childCenter
.
dy
);
final
Offset
viewportFocalPoint
=
Offset
(
childCenter
.
dx
-
40.0
-
childOffset
.
dx
,
childCenter
.
dy
-
childOffset
.
dy
,
);
final
Offset
sceneFocalPoint
=
transformationController
.
toScene
(
viewportFocalPoint
);
gesture
=
await
tester
.
createGesture
();
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
final
Offset
newSceneFocalPoint
=
transformationController
.
toScene
(
viewportFocalPoint
);
expect
(
newSceneFocalPoint
.
dx
,
closeTo
(
sceneFocalPoint
.
dx
,
1.0
));
expect
(
newSceneFocalPoint
.
dy
,
closeTo
(
sceneFocalPoint
.
dy
,
1.0
));
});
});
group
(
'getNearestPointOnLine'
,
()
{
test
(
'does not modify parameters'
,
()
{
final
Vector3
point
=
Vector3
(
5.0
,
5.0
,
0.0
);
final
Vector3
a
=
Vector3
(
0.0
,
0.0
,
0.0
);
final
Vector3
b
=
Vector3
(
10.0
,
0.0
,
0.0
);
final
Vector3
closestPoint
=
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
);
expect
(
closestPoint
,
Vector3
(
5.0
,
0.0
,
0.0
));
expect
(
point
,
Vector3
(
5.0
,
5.0
,
0.0
));
expect
(
a
,
Vector3
(
0.0
,
0.0
,
0.0
));
expect
(
b
,
Vector3
(
10.0
,
0.0
,
0.0
));
});
test
(
'simple example'
,
()
{
final
Vector3
point
=
Vector3
(
0.0
,
5.0
,
0.0
);
final
Vector3
a
=
Vector3
(
0.0
,
0.0
,
0.0
);
final
Vector3
b
=
Vector3
(
5.0
,
5.0
,
0.0
);
expect
(
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
),
Vector3
(
2.5
,
2.5
,
0.0
));
});
test
(
'closest to a'
,
()
{
final
Vector3
point
=
Vector3
(-
1.0
,
-
1.0
,
0.0
);
final
Vector3
a
=
Vector3
(
0.0
,
0.0
,
0.0
);
final
Vector3
b
=
Vector3
(
5.0
,
5.0
,
0.0
);
expect
(
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
),
a
);
});
test
(
'closest to b'
,
()
{
final
Vector3
point
=
Vector3
(
6.0
,
6.0
,
0.0
);
final
Vector3
a
=
Vector3
(
0.0
,
0.0
,
0.0
);
final
Vector3
b
=
Vector3
(
5.0
,
5.0
,
0.0
);
expect
(
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
),
b
);
});
test
(
'point already on the line returns the point'
,
()
{
final
Vector3
point
=
Vector3
(
2.0
,
2.0
,
0.0
);
final
Vector3
a
=
Vector3
(
0.0
,
0.0
,
0.0
);
final
Vector3
b
=
Vector3
(
5.0
,
5.0
,
0.0
);
expect
(
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
),
point
);
});
test
(
'real example'
,
()
{
final
Vector3
point
=
Vector3
(-
436.9
,
433.6
,
0.0
);
final
Vector3
a
=
Vector3
(-
1114.0
,
-
60.3
,
0.0
);
final
Vector3
b
=
Vector3
(
288.8
,
432.7
,
0.0
);
final
Vector3
closestPoint
=
InteractiveViewer
.
getNearestPointOnLine
(
point
,
a
,
b
);
expect
(
closestPoint
.
x
,
closeTo
(-
356.8
,
0.1
));
expect
(
closestPoint
.
y
,
closeTo
(
205.8
,
0.1
));
});
});
group
(
'getAxisAlignedBoundingBox'
,
()
{
test
(
'rectangle already axis aligned returns the rectangle'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
);
final
Quad
aabb
=
InteractiveViewer
.
getAxisAlignedBoundingBox
(
quad
);
expect
(
aabb
.
point0
,
quad
.
point0
);
expect
(
aabb
.
point1
,
quad
.
point1
);
expect
(
aabb
.
point2
,
quad
.
point2
);
expect
(
aabb
.
point3
,
quad
.
point3
);
});
test
(
'rectangle rotated by 45 degrees'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
5.0
,
0.0
),
Vector3
(
5.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
5.0
,
0.0
),
Vector3
(
5.0
,
0.0
,
0.0
),
);
final
Quad
aabb
=
InteractiveViewer
.
getAxisAlignedBoundingBox
(
quad
);
expect
(
aabb
.
point0
,
Vector3
(
0.0
,
0.0
,
0.0
));
expect
(
aabb
.
point1
,
Vector3
(
10.0
,
0.0
,
0.0
));
expect
(
aabb
.
point2
,
Vector3
(
10.0
,
10.0
,
0.0
));
expect
(
aabb
.
point3
,
Vector3
(
0.0
,
10.0
,
0.0
));
});
test
(
'rectangle rotated very slightly'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
1.0
,
0.0
),
Vector3
(
1.0
,
11.0
,
0.0
),
Vector3
(
11.0
,
9.0
,
0.0
),
Vector3
(
9.0
,
-
1.0
,
0.0
),
);
final
Quad
aabb
=
InteractiveViewer
.
getAxisAlignedBoundingBox
(
quad
);
expect
(
aabb
.
point0
,
Vector3
(
0.0
,
-
1.0
,
0.0
));
expect
(
aabb
.
point1
,
Vector3
(
11.0
,
-
1.0
,
0.0
));
expect
(
aabb
.
point2
,
Vector3
(
11.0
,
11.0
,
0.0
));
expect
(
aabb
.
point3
,
Vector3
(
0.0
,
11.0
,
0.0
));
});
test
(
'example from hexagon board'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(-
462.7
,
165.9
,
0.0
),
Vector3
(
690.6
,
-
576.7
,
0.0
),
Vector3
(
1188.1
,
196.0
,
0.0
),
Vector3
(
34.9
,
938.6
,
0.0
),
);
final
Quad
aabb
=
InteractiveViewer
.
getAxisAlignedBoundingBox
(
quad
);
expect
(
aabb
.
point0
,
Vector3
(-
462.7
,
-
576.7
,
0.0
));
expect
(
aabb
.
point1
,
Vector3
(
1188.1
,
-
576.7
,
0.0
));
expect
(
aabb
.
point2
,
Vector3
(
1188.1
,
938.6
,
0.0
));
expect
(
aabb
.
point3
,
Vector3
(-
462.7
,
938.6
,
0.0
));
});
});
group
(
'pointIsInside'
,
()
{
test
(
'inside'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
);
final
Vector3
point
=
Vector3
(
5.0
,
5.0
,
0.0
);
expect
(
InteractiveViewer
.
pointIsInside
(
point
,
quad
),
true
);
});
test
(
'outside'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
);
final
Vector3
point
=
Vector3
(
12.0
,
0.0
,
0.0
);
expect
(
InteractiveViewer
.
pointIsInside
(
point
,
quad
),
false
);
});
test
(
'on the edge'
,
()
{
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
);
final
Vector3
point
=
Vector3
(
0.0
,
0.0
,
0.0
);
expect
(
InteractiveViewer
.
pointIsInside
(
point
,
quad
),
true
);
});
});
group
(
'getNearestPointInside'
,
()
{
test
(
'point already inside quad'
,
()
{
final
Vector3
point
=
Vector3
(
5.0
,
5.0
,
0.0
);
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
);
final
Vector3
nearestPoint
=
InteractiveViewer
.
getNearestPointInside
(
point
,
quad
);
expect
(
nearestPoint
,
point
);
});
test
(
'axis aligned quad'
,
()
{
final
Vector3
point
=
Vector3
(
5.0
,
15.0
,
0.0
);
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
0.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
10.0
,
0.0
),
Vector3
(
10.0
,
0.0
,
0.0
),
);
final
Vector3
nearestPoint
=
InteractiveViewer
.
getNearestPointInside
(
point
,
quad
);
expect
(
nearestPoint
,
Vector3
(
5.0
,
10.0
,
0.0
));
});
test
(
'not axis aligned quad'
,
()
{
final
Vector3
point
=
Vector3
(
5.0
,
15.0
,
0.0
);
final
Quad
quad
=
Quad
.
points
(
Vector3
(
0.0
,
0.0
,
0.0
),
Vector3
(
2.0
,
10.0
,
0.0
),
Vector3
(
12.0
,
12.0
,
0.0
),
Vector3
(
10.0
,
2.0
,
0.0
),
);
final
Vector3
nearestPoint
=
InteractiveViewer
.
getNearestPointInside
(
point
,
quad
);
expect
(
nearestPoint
.
x
,
closeTo
(
5.8
,
0.1
));
expect
(
nearestPoint
.
y
,
closeTo
(
10.8
,
0.1
));
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment