support for lazy building widgets within viewport
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/core/controller.dart';
|
|
||||||
import 'package:infinite_lazy_2d_grid/infinite_lazy_2d_grid.dart';
|
import 'package:infinite_lazy_2d_grid/infinite_lazy_2d_grid.dart';
|
||||||
|
|
||||||
class App extends StatefulWidget {
|
class App extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:infinite_lazy_2d_grid/utils/conversions.dart';
|
|
||||||
import 'package:infinite_lazy_2d_grid/utils/styles.dart';
|
|
||||||
|
|
||||||
// ------------------------------ Public Types -------------------------------
|
|
||||||
|
|
||||||
typedef WidgetBuilder = Widget Function();
|
|
||||||
|
|
||||||
// ------------------------------ Private Types ------------------------------
|
|
||||||
|
|
||||||
// some simple exceptions
|
|
||||||
// ignore: non_constant_identifier_names
|
|
||||||
final _ChildNotFoundException = Exception('Child with the given ID does not exist');
|
|
||||||
|
|
||||||
class _ChildInfo {
|
|
||||||
Offset gsPosition;
|
|
||||||
final WidgetBuilder builder;
|
|
||||||
|
|
||||||
_ChildInfo({required this.gsPosition, required this.builder});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChildInfo {
|
|
||||||
Offset gsPosition;
|
|
||||||
Offset ssPosition;
|
|
||||||
Widget child;
|
|
||||||
ChildInfo({required this.gsPosition, required this.ssPosition, required this.child});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------ Controller ------------------------------
|
|
||||||
|
|
||||||
/// Every canvas view needs one and this handles the positioning logic
|
|
||||||
class CanvasController with ChangeNotifier {
|
|
||||||
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
|
||||||
Offset _gsTopLeftOffset = Offset.zero;
|
|
||||||
double _baseScale, _scale;
|
|
||||||
late Size _canvasSize;
|
|
||||||
final Map<int, _ChildInfo> _children = {}; // int for IDs
|
|
||||||
bool debug;
|
|
||||||
|
|
||||||
CanvasController({this.debug = false, double initialScale = 1})
|
|
||||||
: _scale = initialScale,
|
|
||||||
_baseScale = initialScale,
|
|
||||||
assert(initialScale > 0, 'Initial scale must be greater than 0');
|
|
||||||
|
|
||||||
// -------------------- getters --------------------
|
|
||||||
Offset get offset => _gsTopLeftOffset;
|
|
||||||
double get scale => _scale;
|
|
||||||
Size get canvasSize => _canvasSize;
|
|
||||||
// -------------------- public functions --------------------
|
|
||||||
|
|
||||||
void onCanvasSizeChange(Size size) {
|
|
||||||
_canvasSize = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- child manip --------
|
|
||||||
|
|
||||||
int addChild(Offset position, WidgetBuilder builder) {
|
|
||||||
_children[_nextId] = _ChildInfo(gsPosition: position, builder: builder);
|
|
||||||
return _nextId++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeChild(int id) {
|
|
||||||
_children.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
int updatePosition(int id, Offset newPosition) {
|
|
||||||
if (_children.containsKey(id)) {
|
|
||||||
_children[id]!.gsPosition = newPosition;
|
|
||||||
return id;
|
|
||||||
} else {
|
|
||||||
throw _ChildNotFoundException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Offset getPosition(int id) {
|
|
||||||
if (_children.containsKey(id)) {
|
|
||||||
return _children[id]!.gsPosition;
|
|
||||||
} else {
|
|
||||||
throw _ChildNotFoundException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onScaleStart(ScaleStartDetails details) {
|
|
||||||
_baseScale = _scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- gesture handling --------
|
|
||||||
void onScaleUpdate(ScaleUpdateDetails details) {
|
|
||||||
// uses usual display conventions and final vector postion - initial vector position
|
|
||||||
// convention is that if I drag from right to left, dx is negative
|
|
||||||
// for top to bottom, dy is postive
|
|
||||||
|
|
||||||
// scale + offset is => scale then offset
|
|
||||||
|
|
||||||
if (details.scale != 1) {
|
|
||||||
final newScale = _baseScale * details.scale;
|
|
||||||
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, details.localFocalPoint, _scale, newScale);
|
|
||||||
_scale = newScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.focalPointDelta != Offset.zero) {
|
|
||||||
// if ss distnace is x, and zoom is 2x, gs only moves by x/2
|
|
||||||
_gsTopLeftOffset -= details.focalPointDelta / _scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateScalebyDelta(double delta) {
|
|
||||||
final focalPoint = Offset(canvasSize.width / 2, canvasSize.height / 2);
|
|
||||||
final newScale = _scale + delta;
|
|
||||||
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, focalPoint, _scale, newScale);
|
|
||||||
_scale = newScale;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChildInfo> widgetsWithScreenPositions() {
|
|
||||||
return _children.entries.map((entry) {
|
|
||||||
final item = entry.value;
|
|
||||||
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset, _scale);
|
|
||||||
var child = item.builder();
|
|
||||||
if (debug) child = _Debug(id: entry.key, gs: item.gsPosition, ss: ssPosition, child: child);
|
|
||||||
return ChildInfo(gsPosition: item.gsPosition, ssPosition: ssPosition, child: child);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Debug extends StatelessWidget {
|
|
||||||
final int id;
|
|
||||||
final Offset gs, ss;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const _Debug({required this.id, required this.gs, required this.ss, required this.child});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
child,
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
bottom: -60,
|
|
||||||
child: Text(
|
|
||||||
'ID: $id\nGS:(${gs.dx.toInt()},${gs.dy.toInt()})\nSS:(${ss.dx.toInt()},${ss.dy.toInt()})',
|
|
||||||
style: monospaceStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:infinite_lazy_2d_grid/core/spatial_hashing.dart';
|
||||||
|
import 'package:infinite_lazy_2d_grid/utils/offset_extensions.dart';
|
||||||
|
import '../../utils/conversions.dart';
|
||||||
|
import '../../utils/styles.dart';
|
||||||
|
|
||||||
|
part 'debug.dart';
|
||||||
|
part 'types.dart';
|
||||||
|
|
||||||
|
/// Every canvas view needs one and this handles the positioning logic
|
||||||
|
class CanvasController with ChangeNotifier {
|
||||||
|
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
||||||
|
Offset _gsTopLeftOffset = Offset.zero;
|
||||||
|
double _baseScale, _scale;
|
||||||
|
late Size _canvasSize;
|
||||||
|
final Map<int, _ChildInfo> _children = {}; // int for IDs
|
||||||
|
final Offset? buildCacheExtent;
|
||||||
|
late final Offset _buildExtent;
|
||||||
|
final Offset _initialBuildExtent;
|
||||||
|
final Size _hashCellSize;
|
||||||
|
bool _init = false;
|
||||||
|
late final SpatialHashing<int> _spatialHash;
|
||||||
|
|
||||||
|
bool debug;
|
||||||
|
|
||||||
|
CanvasController({
|
||||||
|
double initialScale = 1,
|
||||||
|
this.debug = false,
|
||||||
|
this.buildCacheExtent,
|
||||||
|
Size hashCellSize = const Size(200, 200),
|
||||||
|
Offset initialBuildExtent = const Offset(1000, 1000),
|
||||||
|
}) : _scale = initialScale,
|
||||||
|
_baseScale = initialScale,
|
||||||
|
_hashCellSize = hashCellSize,
|
||||||
|
_initialBuildExtent = initialBuildExtent,
|
||||||
|
assert(initialScale > 0, 'Initial scale must be greater than 0') {
|
||||||
|
_spatialHash = SpatialHashing<int>(cellSize: _hashCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Getters ====================
|
||||||
|
Offset get offset => _gsTopLeftOffset;
|
||||||
|
double get scale => _scale;
|
||||||
|
Size get canvasSize => _canvasSize;
|
||||||
|
Offset get _ssCenter => Offset(_canvasSize.width / 2, _canvasSize.height / 2);
|
||||||
|
Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale);
|
||||||
|
|
||||||
|
// ==================== Public Functions ====================
|
||||||
|
|
||||||
|
/// Update the canvas size when the widget size changes.
|
||||||
|
void onCanvasSizeChange(Size size) {
|
||||||
|
if (_init && size == _canvasSize) return;
|
||||||
|
|
||||||
|
_canvasSize = size; // allow resize due to canvas resize
|
||||||
|
if (!_init) {
|
||||||
|
_buildExtent =
|
||||||
|
Offset(size.width, size.height) + (buildCacheExtent ?? Offset(size.width * 0.5, size.height * 0.5));
|
||||||
|
_init = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Child Management ====================
|
||||||
|
|
||||||
|
/// Add a child at a given position with a builder. Returns the child ID.
|
||||||
|
int addChild(Offset position, WidgetBuilder builder) {
|
||||||
|
_children[_nextId] = _ChildInfo(gsPosition: position, builder: builder);
|
||||||
|
_spatialHash.add(position.toPoint(), _nextId); // add to spatial hash
|
||||||
|
return _nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a child by its ID.
|
||||||
|
void removeChild(int id) {
|
||||||
|
if (!_children.containsKey(id)) {
|
||||||
|
throw _ChildNotFoundException;
|
||||||
|
}
|
||||||
|
_spatialHash.remove(_children[id]!.gsPosition.toPoint());
|
||||||
|
_children.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the position of a child by its ID.
|
||||||
|
int updatePosition(int id, Offset newPosition) {
|
||||||
|
if (_children.containsKey(id)) {
|
||||||
|
_children[id]!.gsPosition = newPosition;
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
throw _ChildNotFoundException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position of a child by its ID.
|
||||||
|
Offset getPosition(int id) {
|
||||||
|
if (_children.containsKey(id)) {
|
||||||
|
return _children[id]!.gsPosition;
|
||||||
|
} else {
|
||||||
|
throw _ChildNotFoundException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Gesture Handling ====================
|
||||||
|
|
||||||
|
/// Called when a scale gesture starts.
|
||||||
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
|
_baseScale = _scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a scale gesture updates.
|
||||||
|
/// Usually you would not want to override this
|
||||||
|
void onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
|
// uses usual display conventions and final vector postion - initial vector position
|
||||||
|
// convention is that if I drag from right to left, dx is negative
|
||||||
|
// for top to bottom, dy is postive
|
||||||
|
|
||||||
|
// scale + offset => scale then offset
|
||||||
|
|
||||||
|
if (details.scale != 1) {
|
||||||
|
final newScale = _baseScale * details.scale;
|
||||||
|
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, details.localFocalPoint, _scale, newScale);
|
||||||
|
_scale = newScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.focalPointDelta != Offset.zero) {
|
||||||
|
// if ss distnace is x, and zoom is 2x, gs only moves by x/2
|
||||||
|
_gsTopLeftOffset -= details.focalPointDelta / _scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment or decrement the scale by a delta value.
|
||||||
|
void updateScalebyDelta(double delta) {
|
||||||
|
final focalPoint = Offset(canvasSize.width / 2, canvasSize.height / 2);
|
||||||
|
final newScale = _scale + delta;
|
||||||
|
_gsTopLeftOffset = newGsTopLeftOnScaling(_gsTopLeftOffset, focalPoint, _scale, newScale);
|
||||||
|
_scale = newScale;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Positioning Logic ====================
|
||||||
|
|
||||||
|
/// Currently rendered widgets with their position info
|
||||||
|
List<ChildInfo> widgetsWithScreenPositions() {
|
||||||
|
final idsToBuild = _init
|
||||||
|
? _childrenWithinBuildArea(_gsCenter, _buildExtent)
|
||||||
|
: _childrenWithinBuildArea(Offset.zero, _initialBuildExtent);
|
||||||
|
|
||||||
|
return idsToBuild.map((id) {
|
||||||
|
final item = _children[id]!;
|
||||||
|
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset, _scale);
|
||||||
|
var child = item.builder();
|
||||||
|
if (debug) child = _Debug(id: id, gs: item.gsPosition, ss: ssPosition, child: child);
|
||||||
|
return ChildInfo(gsPosition: item.gsPosition, ssPosition: ssPosition, child: child);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _childrenWithinBuildArea(Offset center, Offset extent) {
|
||||||
|
final items = _spatialHash.getPointsAround(center.toPoint(), extent);
|
||||||
|
return items.map((item) => item.data).toList(); // data is the child id here
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
part of 'controller.dart';
|
||||||
|
|
||||||
|
class _Debug extends StatelessWidget {
|
||||||
|
final int id;
|
||||||
|
final Offset gs, ss;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _Debug({required this.id, required this.gs, required this.ss, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
bottom: -60,
|
||||||
|
child: Text(
|
||||||
|
'ID: $id\nGS:(${gs.dx.toInt()},${gs.dy.toInt()})\nSS:(${ss.dx.toInt()},${ss.dy.toInt()})',
|
||||||
|
style: monospaceStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
part of 'controller.dart';
|
||||||
|
|
||||||
|
typedef WidgetBuilder = Widget Function();
|
||||||
|
|
||||||
|
// ------------------------------ Private Types ------------------------------
|
||||||
|
|
||||||
|
// some simple exceptions
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final _ChildNotFoundException = Exception('Child with the given ID does not exist');
|
||||||
|
|
||||||
|
class _ChildInfo {
|
||||||
|
Offset gsPosition;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
_ChildInfo({required this.gsPosition, required this.builder});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildInfo {
|
||||||
|
Offset gsPosition;
|
||||||
|
Offset ssPosition;
|
||||||
|
Widget child;
|
||||||
|
ChildInfo({required this.gsPosition, required this.ssPosition, required this.child});
|
||||||
|
}
|
||||||
+38
-39
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/core/controller.dart';
|
import 'package:infinite_lazy_2d_grid/core/controller/controller.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/utils/offset_extensions.dart';
|
import 'package:infinite_lazy_2d_grid/utils/offset_extensions.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/utils/styles.dart';
|
import 'package:infinite_lazy_2d_grid/utils/styles.dart';
|
||||||
|
|
||||||
@@ -15,45 +15,42 @@ class CanvasView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return GestureDetector(
|
||||||
color: canvasBackground.bgColor,
|
behavior: HitTestBehavior.translucent,
|
||||||
child: GestureDetector(
|
onScaleUpdate: controller.onScaleUpdate,
|
||||||
behavior: HitTestBehavior.translucent,
|
onScaleStart: controller.onScaleStart,
|
||||||
onScaleUpdate: controller.onScaleUpdate,
|
child: ListenableBuilder(
|
||||||
onScaleStart: controller.onScaleStart,
|
listenable: controller,
|
||||||
child: ListenableBuilder(
|
builder: (_, _) {
|
||||||
listenable: controller,
|
final childrenWithPositions = controller.widgetsWithScreenPositions();
|
||||||
builder: (_, _) {
|
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
||||||
final childrenWithPositions = controller.widgetsWithScreenPositions();
|
final children = childrenWithPositions.map((e) => e.child).toList();
|
||||||
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
final canvas = _CanvasRenderObject(
|
||||||
final children = childrenWithPositions.map((e) => e.child).toList();
|
canvasBackground: canvasBackground,
|
||||||
final canvas = _CanvasRenderObject(
|
ssPositions: ssPositions,
|
||||||
canvasBackground: canvasBackground,
|
scale: controller.scale,
|
||||||
ssPositions: ssPositions,
|
onCanvasSizeChange: controller.onCanvasSizeChange,
|
||||||
scale: controller.scale,
|
children: children,
|
||||||
onCanvasSizeChange: controller.onCanvasSizeChange,
|
);
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (controller.debug) {
|
if (controller.debug) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
canvas,
|
canvas,
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Offset: ${controller.offset.coord()}\nScale: ${controller.scale.toStringAsFixed(1)}',
|
'Offset: ${controller.offset.coord()}\nScale: ${controller.scale.toStringAsFixed(1)}',
|
||||||
style: monospaceStyle,
|
style: monospaceStyle,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
} else {
|
);
|
||||||
return canvas;
|
} else {
|
||||||
}
|
return canvas;
|
||||||
},
|
}
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,7 +119,9 @@ class _CanvasRenderBox extends RenderBox
|
|||||||
}
|
}
|
||||||
|
|
||||||
set ssPositions(List<Offset> ssPositions) {
|
set ssPositions(List<Offset> ssPositions) {
|
||||||
assert(ssPositions.length == childCount);
|
// at this point childCount may not be equal to ssPositions.length
|
||||||
|
// but since we are only marking for layout this is fine
|
||||||
|
// at the actual performLayout this will be asserted
|
||||||
if (_ssPositions != ssPositions) {
|
if (_ssPositions != ssPositions) {
|
||||||
_ssPositions = ssPositions;
|
_ssPositions = ssPositions;
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef _CellKey = Point<int>;
|
||||||
|
|
||||||
|
class PointData<T> {
|
||||||
|
final Point point;
|
||||||
|
final T data;
|
||||||
|
|
||||||
|
PointData(this.point, this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpatialHashing<T> {
|
||||||
|
final Size cellSize;
|
||||||
|
final HashMap<Point, T> _pointData = HashMap<Point, T>();
|
||||||
|
final HashMap<_CellKey, List<Point>> _cellMap = HashMap<_CellKey, List<Point>>();
|
||||||
|
|
||||||
|
SpatialHashing({required this.cellSize});
|
||||||
|
|
||||||
|
_CellKey _cellKey(Point point) {
|
||||||
|
int x = (point.x / cellSize.width).floor();
|
||||||
|
int y = (point.y / cellSize.height).floor();
|
||||||
|
return Point(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(Point point, T data) {
|
||||||
|
_CellKey cellKey = _cellKey(point);
|
||||||
|
_pointData[point] = data;
|
||||||
|
|
||||||
|
if (!_cellMap.containsKey(cellKey)) {
|
||||||
|
_cellMap[cellKey] = [];
|
||||||
|
}
|
||||||
|
_cellMap[cellKey]!.add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(Point point) {
|
||||||
|
final key = _cellKey(point);
|
||||||
|
if (_pointData.containsKey(point)) {
|
||||||
|
_pointData.remove(point); // remove the data
|
||||||
|
|
||||||
|
// cell map must have key then
|
||||||
|
_cellMap[key]!.remove(point); // remove from spatial hash
|
||||||
|
if (_cellMap[key]!.isEmpty) {
|
||||||
|
_cellMap.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PointData<T>> getPointsAround(Point point, Offset offset) {
|
||||||
|
_CellKey cellKey = _cellKey(point);
|
||||||
|
List<PointData<T>> results = [];
|
||||||
|
|
||||||
|
// Calculate the range of cells to check
|
||||||
|
int startX = cellKey.x - (offset.dx / cellSize.width).floor();
|
||||||
|
int startY = cellKey.y - (offset.dy / cellSize.height).floor();
|
||||||
|
int endX = cellKey.x + (offset.dx / cellSize.width).ceil();
|
||||||
|
int endY = cellKey.y + (offset.dy / cellSize.height).ceil();
|
||||||
|
|
||||||
|
for (int x = startX; x <= endX; x++) {
|
||||||
|
for (int y = startY; y <= endY; y++) {
|
||||||
|
Point currentCellKey = Point(x, y);
|
||||||
|
if (_cellMap.containsKey(currentCellKey)) {
|
||||||
|
for (Point p in _cellMap[currentCellKey]!) {
|
||||||
|
results.add(PointData(p, _pointData[p] as T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export "./core/background.dart";
|
export "core/background.dart";
|
||||||
export "./core/render.dart";
|
export "core/render.dart";
|
||||||
|
export 'core/controller/controller.dart';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math';
|
||||||
import 'dart:ui' show Offset;
|
import 'dart:ui' show Offset;
|
||||||
|
|
||||||
extension OffsetCartesionOps on Offset {
|
extension OffsetCartesionOps on Offset {
|
||||||
@@ -11,6 +12,10 @@ extension OffsetCartesionOps on Offset {
|
|||||||
return Offset(dx - other.dx, dy - other.dy);
|
return Offset(dx - other.dx, dy - other.dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Point toPoint() {
|
||||||
|
return Point(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
String coord() {
|
String coord() {
|
||||||
return '(${dx.toStringAsFixed(2)}, ${dy.toStringAsFixed(2)})';
|
return '(${dx.toStringAsFixed(2)}, ${dy.toStringAsFixed(2)})';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:infinite_lazy_2d_grid/core/spatial_hashing.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SpatialHashing', () {
|
||||||
|
late SpatialHashing<String> spatialHashing;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
spatialHashing = SpatialHashing<String>(cellSize: const Size(10, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add and retrieve single point', () {
|
||||||
|
final point = Point(5, 5);
|
||||||
|
spatialHashing.add(point, 'A');
|
||||||
|
final results = spatialHashing.getPointsAround(point, const Offset(0, 0));
|
||||||
|
expect(results.length, 1);
|
||||||
|
expect(results.first, (point, 'A'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remove point', () {
|
||||||
|
final point = Point(5, 5);
|
||||||
|
spatialHashing.add(point, 'A');
|
||||||
|
spatialHashing.remove(point);
|
||||||
|
final results = spatialHashing.getPointsAround(point, const Offset(0, 0));
|
||||||
|
expect(results, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add multiple points in same cell', () {
|
||||||
|
final p1 = Point(2, 2);
|
||||||
|
final p2 = Point(7, 7);
|
||||||
|
spatialHashing.add(p1, 'A');
|
||||||
|
spatialHashing.add(p2, 'B');
|
||||||
|
final results = spatialHashing.getPointsAround(p1, const Offset(0, 0));
|
||||||
|
expect(results.length, 2);
|
||||||
|
expect(results, contains((p1, 'A')));
|
||||||
|
expect(results, contains((p2, 'B')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add points in different cells and query with offset', () {
|
||||||
|
final p1 = Point(5, 5);
|
||||||
|
final p2 = Point(15, 15);
|
||||||
|
spatialHashing.add(p1, 'A');
|
||||||
|
spatialHashing.add(p2, 'B');
|
||||||
|
// Query with offset to include both cells
|
||||||
|
final results = spatialHashing.getPointsAround(p1, const Offset(10, 10));
|
||||||
|
expect(results.length, 2);
|
||||||
|
expect(results, contains((p1, 'A')));
|
||||||
|
expect(results, contains((p2, 'B')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removing one of multiple points in a cell', () {
|
||||||
|
final p1 = Point(2, 2);
|
||||||
|
final p2 = Point(7, 7);
|
||||||
|
spatialHashing.add(p1, 'A');
|
||||||
|
spatialHashing.add(p2, 'B');
|
||||||
|
spatialHashing.remove(p1);
|
||||||
|
final results = spatialHashing.getPointsAround(p2, const Offset(0, 0));
|
||||||
|
expect(results.length, 1);
|
||||||
|
expect(results.first, (p2, 'B'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user