From 05f3fbc2d0c892b05b4a91e6b8c908fc5b244e7e Mon Sep 17 00:00:00 2001 From: dvmatyun Date: Sat, 8 Feb 2025 14:17:56 +0400 Subject: [PATCH 1/3] started dev for collisions example --- example/lib/src/common/widget/routes.dart | 6 + example/lib/src/feature/home/home_screen.dart | 4 + .../src/feature/quadtree/quadtree_camera.dart | 112 +++ .../quadtree/quadtree_collision_screen.dart | 692 ++++++++++++++++++ .../src/feature/quadtree/quadtree_screen.dart | 185 +---- lib/src/collisions/quadtree.dart | 126 ++++ 6 files changed, 974 insertions(+), 151 deletions(-) create mode 100644 example/lib/src/feature/quadtree/quadtree_camera.dart create mode 100644 example/lib/src/feature/quadtree/quadtree_collision_screen.dart diff --git a/example/lib/src/common/widget/routes.dart b/example/lib/src/common/widget/routes.dart index e146ab8..28da0ee 100644 --- a/example/lib/src/common/widget/routes.dart +++ b/example/lib/src/common/widget/routes.dart @@ -3,6 +3,7 @@ import 'package:repaintexample/src/feature/clock/clock_screen.dart'; import 'package:repaintexample/src/feature/fps/fps_screen.dart'; import 'package:repaintexample/src/feature/home/home_screen.dart'; import 'package:repaintexample/src/feature/performance_overlay/performance_overlay_screen.dart'; +import 'package:repaintexample/src/feature/quadtree/quadtree_collision_screen.dart'; import 'package:repaintexample/src/feature/quadtree/quadtree_screen.dart'; import 'package:repaintexample/src/feature/shaders/fragment_shaders_screen.dart'; import 'package:repaintexample/src/feature/sunflower/sunflower_screen.dart'; @@ -45,4 +46,9 @@ final Map Function(Map?)> $routes = child: const QuadTreeScreen(), arguments: arguments, ), + 'quadtree-collisions': (arguments) => MaterialPage( + name: 'quadtree-collisions', + child: const QuadTreeCollisionScreen(), + arguments: arguments, + ), }; diff --git a/example/lib/src/feature/home/home_screen.dart b/example/lib/src/feature/home/home_screen.dart index 4ac4b24..a3e074b 100644 --- a/example/lib/src/feature/home/home_screen.dart +++ b/example/lib/src/feature/home/home_screen.dart @@ -48,6 +48,10 @@ class HomeScreen extends StatelessWidget { title: 'QuadTree', page: 'quadtree', ), + HomeTile( + title: 'QuadTree collisions', + page: 'quadtree-collisions', + ), ], ), ), diff --git a/example/lib/src/feature/quadtree/quadtree_camera.dart b/example/lib/src/feature/quadtree/quadtree_camera.dart new file mode 100644 index 0000000..2c23b7d --- /dev/null +++ b/example/lib/src/feature/quadtree/quadtree_camera.dart @@ -0,0 +1,112 @@ +import 'dart:ui'; + +import 'package:repaint/repaint.dart'; + +mixin QuadTreeCameraMixin { + final QuadTree quadTree = QuadTree( + boundary: const Rect.fromLTWH(0, 0, 100000, 100000), + capacity: 18, + ); + + final _camera = QuadTreeCamera( + boundary: Rect.zero, + ); + Rect get cameraBoundary => _camera.boundary; + Size size = Size.zero; + + bool needsPaintQt = false; + + void mountQtCamera() { + _camera.set(Rect.fromCenter( + center: quadTree.boundary.center, + width: size.width, + height: size.height, + )); + } + + void updateCameraBoundary(Size newSize) { + size = newSize; + _camera.set(Rect.fromCenter( + center: _camera.boundary.center, + width: size.width, + height: size.height, + )); + needsPaintQt = true; + } + + void moveQtCamera(Offset offset) { + if (offset == Offset.zero) return; + needsPaintQt = true; + _camera.move(offset); + // Ensure the camera stays within the quadtree boundary. + if (_camera.boundary.width > quadTree.boundary.width || + _camera.boundary.height > quadTree.boundary.height) { + final canvasAspectRatio = size.width / size.height; + final quadTreeAspectRatio = + quadTree.boundary.width / quadTree.boundary.height; + if (canvasAspectRatio > quadTreeAspectRatio) { + _camera.set(Rect.fromCenter( + center: _camera.boundary.center, + width: quadTree.boundary.width, + height: quadTree.boundary.width / canvasAspectRatio, + )); + } else { + _camera.set(Rect.fromCenter( + center: _camera.boundary.center, + width: quadTree.boundary.height * canvasAspectRatio, + height: quadTree.boundary.height, + )); + } + } + if (_camera.boundary.left < quadTree.boundary.left) { + _camera.set(Rect.fromLTWH( + 0, + _camera.boundary.top, + _camera.boundary.width, + _camera.boundary.height, + )); + } else if (_camera.boundary.right > quadTree.boundary.right) { + _camera.set(Rect.fromLTWH( + quadTree.boundary.right - _camera.boundary.width, + _camera.boundary.top, + _camera.boundary.width, + _camera.boundary.height, + )); + } + if (_camera.boundary.top < quadTree.boundary.top) { + _camera.set(Rect.fromLTWH( + _camera.boundary.left, + 0, + _camera.boundary.width, + _camera.boundary.height, + )); + } else if (_camera.boundary.bottom > quadTree.boundary.bottom) { + _camera.set(Rect.fromLTWH( + _camera.boundary.left, + quadTree.boundary.bottom - _camera.boundary.height, + _camera.boundary.width, + _camera.boundary.height, + )); + } + } + + void unmountQtCamera() { + quadTree.clear(); + } +} + +class QuadTreeCamera { + QuadTreeCamera({ + required Rect boundary, + }) : _boundary = boundary; + + /// The boundary of the camera. + Rect get boundary => _boundary; + Rect _boundary; + + /// Move the camera by the given offset. + void move(Offset offset) => _boundary = _boundary.shift(offset); + + /// Set the camera to the given boundary. + void set(Rect boundary) => _boundary = boundary; +} diff --git a/example/lib/src/feature/quadtree/quadtree_collision_screen.dart b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart new file mode 100644 index 0000000..e56c76b --- /dev/null +++ b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart @@ -0,0 +1,692 @@ +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:repaint/repaint.dart'; +import 'package:repaintexample/src/common/widget/app.dart'; +import 'package:repaintexample/src/feature/quadtree/quadtree_camera.dart'; + +/// {@template quadtree_screen} +/// QuadTreeCollisionScreen widget. +/// {@endtemplate} +class QuadTreeCollisionScreen extends StatefulWidget { + /// {@macro quadtree_screen} + const QuadTreeCollisionScreen({ + super.key, // ignore: unused_element + }); + + @override + State createState() => + _QuadTreeCollisionScreenState(); +} + +/// State for widget QuadTreeCollisionScreen. +class _QuadTreeCollisionScreenState extends State { + final QuadTreeCollisionPainter painter = QuadTreeCollisionPainter(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('QuadTreeCollision'), + leading: BackButton( + onPressed: () => App.pop(context), + ), + ), + body: SafeArea( + child: RePaint( + painter: painter, + ), + ), + ); +} + +class CollisionRectObject { + CollisionRectObject({ + required this.id, + required this.rect, + required this.velocity, + }); + + final int id; + Rect rect; + Vector2d velocity; + + // Only small rects can be moved + bool get canBeMoved => rect.width < 128 && rect.height < 128; + + Iterable get vertices { + return [ + Vector2d(rect.left, rect.bottom), + Vector2d(rect.right, rect.bottom), + Vector2d(rect.right, rect.top), + Vector2d(rect.left, rect.top), + ]; + } + + Vector2d get absoluteCenter => Vector2d(rect.center.dx, rect.center.dy); + + @override + int get hashCode { + const int prime1 = 73856093; + const int prime2 = 19349663; + const int prime3 = 83492791; + return ((id * prime1) ^ + (rect.left * prime2).round() ^ + (rect.top * prime3).round()); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CollisionRectObject && + id == other.id && + rect == other.rect && + velocity == other.velocity; +} + +class Vector2d { + const Vector2d(this.x, this.y); + + Vector2d.zero() + : x = 0, + y = 0; + + final double x; + final double y; + + /// Negate. + Vector2d operator -() => Vector2d(-x, -y); + + /// Subtract two vectors. + Vector2d operator -(Vector2d other) => Vector2d(x - other.x, y - other.y); + + /// Add two vectors. + Vector2d operator +(Vector2d other) => Vector2d(x + other.x, y + other.y); + + /// Scale. + Vector2d operator /(double scale) => Vector2d(x / scale, y / scale); + + /// Scale. + Vector2d operator *(double scale) => Vector2d(x * scale, y * scale); + + /// Length. + double get length => sqrt(length2); + + /// Length squared. + double get length2 { + double sum; + sum = x * x; + sum += y * y; + return sum; + } + + /// Normalize this. + Vector2d normalized() { + final l = length; + if (l == 0.0) { + return Vector2d.zero(); + } + final d = 1.0 / l; + return Vector2d(x * d, y * d); + } + + /// Inner product. + double dot(Vector2d other) { + double sum; + sum = x * other.x; + sum += y * other.y; + return sum; + } + + Vector2d perpendicular() => Vector2d(-y, x); + + @override + String toString() => 'Vector2d($x, $y)'; +} + +class CollisionUtil { + static ({Vector2d normal, double depth})? intercectRects( + CollisionRectObject a, CollisionRectObject b) { + double minOverlap = double.infinity; + Vector2d bestNormal = const Vector2d(0, 0); + + for (var poly in [a, b]) { + final vertices = poly.vertices.toList(); + //print('verticies: [${vertices.join(', ')}]'); + for (int i = 0; i < vertices.length; i++) { + Vector2d p1 = vertices[i]; + Vector2d p2 = vertices[(i + 1) % vertices.length]; + Vector2d edge = p2 - p1; + Vector2d axis = edge.perpendicular().normalized(); + + // Project both polygons onto the axis + double minA = double.infinity, maxA = -double.infinity; + double minB = double.infinity, maxB = -double.infinity; + + for (Vector2d v in a.vertices) { + double projection = v.dot(axis); + minA = min(minA, projection); + maxA = max(maxA, projection); + } + + for (Vector2d v in b.vertices) { + double projection = v.dot(axis); + minB = min(minB, projection); + maxB = max(maxB, projection); + } + + // Check for gap + if (maxA < minB || maxB < minA) { + return null; // No collision + } + + // Compute overlap + double overlap = min(maxA, maxB) - max(minA, minB); + if (overlap < minOverlap) { + minOverlap = overlap; + bestNormal = axis; + } + } + } + + return (normal: bestNormal, depth: minOverlap); + } + + static ({Vector2d normal, double depth}) getNormalAndDepth( + List verticesA, + List verticesB, { + bool insverted = false, + }) { + var normal = Vector2d.zero(); + double depth = double.maxFinite; + for (int i = 0; i < verticesA.length; i++) { + Vector2d va = verticesA[i]; + Vector2d vb = verticesA[(i + 1) % verticesA.length]; + + Vector2d edge = vb - va; + Vector2d axis = Vector2d(-edge.y, edge.x); + axis = axis.normalized(); + + final pA = projectVertices(insverted ? verticesB : verticesA, axis); + final pB = projectVertices(insverted ? verticesA : verticesB, axis); + + double axisDepth = min(pB.max - pA.min, pA.max - pB.min); + if (axisDepth < depth) { + depth = axisDepth; + normal = axis; + } + } + return (normal: normal, depth: depth); + } + + static ({double min, double max}) projectVertices( + List vertices, + Vector2d axis, + ) { + double min = double.maxFinite; + double max = -double.maxFinite; + for (var v in vertices) { + double proj = v.dot(axis); + + if (proj < min) { + min = proj; + } + if (proj > max) { + max = proj; + } + } + return (min: min, max: max); + } +} + +class CollidingPair { + T _a; + T _b; + + T get a => _a; + T get b => _b; + + int get hash => _hash; + int _hash; + + CollidingPair(this._a, this._b) : _hash = _a.hashCode ^ _b.hashCode; + + /// Sets the prospect to contain [a] and [b] instead of what it previously + /// contained. + void set(T a, T b) { + _a = a; + _b = b; + _hash = a.hashCode ^ b.hashCode; + } + + /// Sets the prospect to contain the content of [other]. + void setFrom(CollidingPair other) { + _a = other._a; + _b = other._b; + _hash = other._hash; + } + + /// Creates a new prospect object with the same content. + CollidingPair clone() => CollidingPair(_a, _b); +} + +mixin CollisionObjectsMixin on QuadTreeCameraMixin { + final collisionObjects = {}; + final Paint _rectPaint = Paint() + ..color = Colors.yellow.withAlpha(150) + ..style = PaintingStyle.fill + ..strokeWidth = 2; + final Paint _collidingPaint = Paint() + ..color = Colors.red.withAlpha(150) + ..style = PaintingStyle.fill + ..strokeWidth = 2; + + @override + // ignore: unnecessary_overrides + void mountQtCamera() { + super.mountQtCamera(); + + // addSideBounds(); + } + + void addSideBounds() { + // Adding bounds to the screen box: + const double boundWidth = 64; + const double rectDimensions = 1024; + final boundLeft = Rect.fromLTWH( + cameraBoundary.center.dx - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy - rectDimensions / 2, + boundWidth, + rectDimensions, + ); + final boundRight = Rect.fromLTWH( + cameraBoundary.center.dx + rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy - rectDimensions / 2 - boundWidth, + boundWidth, + rectDimensions, + ); + final boundTop = Rect.fromLTWH( + cameraBoundary.center.dx - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy - rectDimensions / 2 - boundWidth, + rectDimensions, + boundWidth, + ); + final boundBottom = Rect.fromLTWH( + cameraBoundary.center.dx - rectDimensions / 2, + cameraBoundary.center.dy + rectDimensions / 2 - boundWidth, + rectDimensions, + boundWidth, + ); + + quadTree.insert(boundLeft); + quadTree.insert(boundRight); + quadTree.insert(boundTop); + quadTree.insert(boundBottom); + } + + /// Draw points on the canvas. + void drawCollisionObjects(Size size, Canvas canvas) { + canvas.save(); + canvas.translate(-cameraBoundary.left, -cameraBoundary.top); + for (final e in collisionObjects.values.toList()) { + if (_collidingObjectIds.contains(e.id)) { + canvas.drawRect(e.rect, _collidingPaint); + } else { + canvas.drawRect(e.rect, _rectPaint); + } + } + canvas.restore(); + } + + void updateCollisionObjects(double delta) { + final dt = delta / 1000; + // Checking collisions: + _collidingPairHashes.clear(); + _collidingObjectIds.clear(); + quadTree.forEach((id, left, top, width, height) { + final obj = collisionObjects[id]; + if (obj == null) { + collisionObjects[id] = CollisionRectObject( + id: id, + rect: Rect.fromLTWH(left, top, width, height), + velocity: Vector2d.zero(), + ); + } else { + obj.rect = Rect.fromLTWH(left, top, width, height); + } + _checkCollideWith(collisionObjects[id]!); + return true; + }); + + // Manipulating colliding pairs: + for (final pair in _collidingPairHashes.values) { + final a = pair.a; + final b = pair.b; + if (!a.canBeMoved && !b.canBeMoved) { + continue; + } + final colisionResult = CollisionUtil.intercectRects(a, b); + if (colisionResult == null) { + continue; + } + //if (a.canBeMoved || b.canBeMoved) { + // print( + // '> colisionResult= ${colisionResult.normal}, depth=${colisionResult.depth} (delta=$dt)'); + //} + final depth = max(1.0, colisionResult.depth); + if (a.canBeMoved) { + //final newVelocity = colisionResult.normal * colisionResult.depth; + a.velocity = colisionResult.normal * depth; + } + if (b.canBeMoved) { + b.velocity = colisionResult.normal * (-1) * depth; + } + } + for (final e in collisionObjects.values) { + quadTree.move(e.id, e.rect.left + e.velocity.x * dt, + e.rect.top + e.velocity.y * dt); + } + needsPaintQt = true; + } + + final _collidingPairHashes = >{}; + final _collidingObjectIds = {}; + + CollidingPair? _checkCollideWith( + CollisionRectObject object, + ) { + for (final potential in quadTree.queryRectsIterable(object.rect)) { + if (potential.id == object.id) continue; + final other = collisionObjects[potential.id]; + if (other == null) continue; + final pair = CollidingPair(object, other); + if (_collidingPairHashes.containsKey(pair.hash)) continue; + _collidingPairHashes[pair.hash] = pair; + _collidingObjectIds.add(object.id); + _collidingObjectIds.add(other.id); + return pair; + } + + return null; + } +} + +class QuadTreeCollisionPainter extends RePainterBase + with QuadTreeCameraMixin, CollisionObjectsMixin { + final HardwareKeyboard _keyboardManager = HardwareKeyboard.instance; + bool _spacebarPressed = false; + + @override + bool get needsPaint => needsPaintQt; + + @override + void mount(RePaintBox box, PipelineOwner owner) { + size = box.size; + mountQtCamera(); + + _keyboardManager.addHandler(onKeyboardEvent); + _spacebarPressed = + HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.space); + needsPaintQt = true; + } + + @override + void unmount() { + _keyboardManager.removeHandler(onKeyboardEvent); + unmountQtCamera(); + _spacebarPressed = false; + needsPaintQt = false; + } + + bool onKeyboardEvent(KeyEvent event) { + bool? isKeyDown = switch (event) { + KeyDownEvent _ => true, + KeyRepeatEvent _ => true, + KeyUpEvent _ => false, + _ => null, + }; + if (isKeyDown == null) return false; // Not a key press event. + switch (event.logicalKey) { + case LogicalKeyboardKey.keyW when isKeyDown: + case LogicalKeyboardKey.arrowUp when isKeyDown: + moveQtCamera(const Offset(0, -10)); + case LogicalKeyboardKey.keyS when isKeyDown: + case LogicalKeyboardKey.arrowDown when isKeyDown: + moveQtCamera(const Offset(0, 10)); + case LogicalKeyboardKey.keyA when isKeyDown: + case LogicalKeyboardKey.arrowLeft when isKeyDown: + moveQtCamera(const Offset(-10, 0)); + case LogicalKeyboardKey.keyD when isKeyDown: + case LogicalKeyboardKey.arrowRight when isKeyDown: + moveQtCamera(const Offset(10, 0)); + case LogicalKeyboardKey.space: + _spacebarPressed = isKeyDown; + default: + return false; // Not a key we care about. + } + return true; + } + + @override + void onPointerEvent(PointerEvent event) { + switch (event) { + case PointerHoverEvent hover: + if (!_spacebarPressed) return; + moveQtCamera(-hover.localDelta); + case PointerMoveEvent move: + if (!move.down) return; + _onClick(move.localPosition); + case PointerPanZoomUpdateEvent pan: + moveQtCamera(pan.panDelta); + case PointerDownEvent click: + _onClick(click.localPosition); + } + } + + bool _wasAdded = false; + + void _onClick(Offset point) { + if (_wasAdded) { + return; + } + _wasAdded = true; + Future.delayed(const Duration(milliseconds: 500), () { + _wasAdded = false; + }); + // Calculate the dot offset from the camera. + final cameraCenter = cameraBoundary.center; + const double dimension = 64; + final dot = Rect.fromCenter( + center: Offset( + point.dx + cameraCenter.dx - size.width / 2, + point.dy + cameraCenter.dy - size.height / 2, + ), + width: dimension, + height: dimension, + ); + quadTree.insert(dot); + needsPaintQt = true; + } + + @override + void update(RePaintBox box, Duration elapsed, double delta) { + // If the size of the box has changed, update the camera too. + if (box.size != size) { + updateCameraBoundary(box.size); + } + updateCollisionObjects(delta); + + if (!needsPaintQt) return; // No need to update the points and repaint. + + /* + final boundary = cameraBoundary; + final result = quadTree.query(boundary); + if (result.isEmpty) { + _points = Float32List(0); + } else { + _points = Float32List(result.length * 2); + var counter = 0; + result.forEach(( + int id, + double left, + double top, + double width, + double height, + ) { + _points + ..[counter] = left + width / 2 - boundary.left + ..[counter + 1] = top + height / 2 - boundary.top; + counter += 2; + return true; + }); + //_sortByY(_points); + } + */ + } + + final TextPainter _textPainter = TextPainter( + textAlign: TextAlign.left, + textDirection: TextDirection.ltr, + ); + final Paint _bgPaint = Paint()..color = Colors.lightBlue; + + int _spinnerIndex = 0; + static const List _spinnerSymbols = [ + '⠋', + '⠙', + '⠹', + '⠸', + '⠼', + '⠴', + '⠦', + '⠧', + '⠇', + '⠏', + ]; + + /// Draw current status. + void _drawStatus(Size size, Canvas canvas) { + _spinnerIndex++; + final nbsp = String.fromCharCode(160); + final status = StringBuffer() + ..write(_spinnerSymbols[_spinnerIndex = + _spinnerIndex % _spinnerSymbols.length]) + ..write(' | ') + ..write('World:') + ..write(nbsp) + ..write(quadTree.boundary.width.toStringAsFixed(0)) + ..write('x') + ..write(quadTree.boundary.height.toStringAsFixed(0)) + ..write(' | ') + ..write('Screen:') + ..write(nbsp) + ..write(size.width.toStringAsFixed(0)) + ..write('x') + ..write(size.height.toStringAsFixed(0)) + ..write(' | ') + ..write('Position:') + ..write(nbsp) + ..write(cameraBoundary.left.toStringAsFixed(0)) + ..write('x') + ..write(cameraBoundary.top.toStringAsFixed(0)) + ..write(' | ') + ..write('Points:') + ..write(nbsp) + ..write(collisionObjects.length) + ..write('/') + ..write(quadTree.length) + ..write(' | ') + ..write('Nodes:') + ..write(nbsp) + ..write(quadTree.nodes); + _textPainter + ..text = TextSpan( + text: status.toString(), + style: const TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'RobotoMono', + ), + ) + ..layout(maxWidth: size.width - 32); + final textSize = _textPainter.size; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH( + 8, + size.height - textSize.height - 16 - 8, + size.width - 16, + textSize.height + 16, + ), + const Radius.circular(8), + ), + Paint()..color = Colors.white54, + ); + _textPainter.paint( + canvas, + Offset(16, size.height - textSize.height - 16), + ); + } + + final Paint _nodePaint = Paint() + ..color = Colors.white + ..strokeWidth = 0.1 + ..style = PaintingStyle.stroke; + + /// Draw the quadtree nodes on the canvas. + void _drawQuadTree(Size size, Canvas canvas) { + final cam = cameraBoundary; + final queue = Queue()..add(quadTree.root); + while (queue.isNotEmpty) { + final node = queue.removeFirst(); + if (node == null) continue; + if (!node.boundary.overlaps(cam)) continue; + if (node.subdivided) { + // Parent node is subdivided, add children to the queue. + queue + ..add(node.northWest) + ..add(node.northEast) + ..add(node.southWest) + ..add(node.southEast); + } else { + // Draw the leaf node. + final nodeBounds = Rect.fromLTWH( + node.boundary.left - cam.left, + node.boundary.top - cam.top, + node.boundary.width, + node.boundary.height, + ); + + // Check if any vertices are visible. + if (nodeBounds.right < 0 || + nodeBounds.left > size.width || + nodeBounds.bottom < 0 || + nodeBounds.top > size.height) { + continue; + } + final k = 1.0 / (node.depth + 1); + canvas.drawRect( + nodeBounds, + _nodePaint + ..strokeWidth = k * 2 + ..color = Colors.white.withValues(alpha: 1 - k), + ); + } + } + } + + @override + void paint(RePaintBox box, PaintingContext context) { + final size = box.size; + final canvas = context.canvas; + + canvas.drawRect(Offset.zero & size, _bgPaint); // Draw background + _drawQuadTree(size, canvas); // Draw quadtree nodes + drawCollisionObjects(size, canvas); // Draw points + _drawStatus(size, canvas); // Draw status + needsPaintQt = false; // Reset the flag. + } +} diff --git a/example/lib/src/feature/quadtree/quadtree_screen.dart b/example/lib/src/feature/quadtree/quadtree_screen.dart index 634c9bb..86c4784 100644 --- a/example/lib/src/feature/quadtree/quadtree_screen.dart +++ b/example/lib/src/feature/quadtree/quadtree_screen.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:repaint/repaint.dart'; import 'package:repaintexample/src/common/widget/app.dart'; +import 'package:repaintexample/src/feature/quadtree/quadtree_camera.dart'; /// {@template quadtree_screen} /// QuadTreeScreen widget. @@ -25,32 +26,6 @@ class QuadTreeScreen extends StatefulWidget { /// State for widget QuadTreeScreen. class _QuadTreeScreenState extends State { final QuadTreePainter painter = QuadTreePainter(); - /* #region Lifecycle */ - @override - void initState() { - super.initState(); - // Initial state initialization - } - - @override - void didUpdateWidget(covariant QuadTreeScreen oldWidget) { - super.didUpdateWidget(oldWidget); - // Widget configuration changed - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // The configuration of InheritedWidgets has changed - // Also called after initState but before build - } - - @override - void dispose() { - // Permanent removal of a tree stent - super.dispose(); - } - /* #endregion */ @override Widget build(BuildContext context) => Scaffold( @@ -68,46 +43,32 @@ class _QuadTreeScreenState extends State { ); } -class QuadTreePainter extends RePainterBase { - final QuadTree _quadTree = QuadTree( - boundary: const Rect.fromLTWH(0, 0, 100000, 100000), - capacity: 18, - ); - - final _camera = QuadTreeCamera( - boundary: Rect.zero, - ); - +class QuadTreePainter extends RePainterBase with QuadTreeCameraMixin { final HardwareKeyboard _keyboardManager = HardwareKeyboard.instance; bool _spacebarPressed = false; Float32List _points = Float32List(0); - Size _size = Size.zero; @override - bool get needsPaint => _needsPaint; - bool _needsPaint = false; + bool get needsPaint => needsPaintQt; @override void mount(RePaintBox box, PipelineOwner owner) { - _size = box.size; - _camera.set(Rect.fromCenter( - center: _quadTree.boundary.center, - width: _size.width, - height: _size.height, - )); + size = box.size; + mountQtCamera(); + _keyboardManager.addHandler(onKeyboardEvent); _spacebarPressed = HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.space); - _needsPaint = true; + needsPaintQt = true; } @override void unmount() { _keyboardManager.removeHandler(onKeyboardEvent); - _quadTree.clear(); + unmountQtCamera(); _spacebarPressed = false; - _needsPaint = false; + needsPaintQt = false; } bool onKeyboardEvent(KeyEvent event) { @@ -121,16 +82,16 @@ class QuadTreePainter extends RePainterBase { switch (event.logicalKey) { case LogicalKeyboardKey.keyW when isKeyDown: case LogicalKeyboardKey.arrowUp when isKeyDown: - _moveCamera(const Offset(0, -10)); + moveQtCamera(const Offset(0, -10)); case LogicalKeyboardKey.keyS when isKeyDown: case LogicalKeyboardKey.arrowDown when isKeyDown: - _moveCamera(const Offset(0, 10)); + moveQtCamera(const Offset(0, 10)); case LogicalKeyboardKey.keyA when isKeyDown: case LogicalKeyboardKey.arrowLeft when isKeyDown: - _moveCamera(const Offset(-10, 0)); + moveQtCamera(const Offset(-10, 0)); case LogicalKeyboardKey.keyD when isKeyDown: case LogicalKeyboardKey.arrowRight when isKeyDown: - _moveCamera(const Offset(10, 0)); + moveQtCamera(const Offset(10, 0)); case LogicalKeyboardKey.space: _spacebarPressed = isKeyDown; default: @@ -144,105 +105,43 @@ class QuadTreePainter extends RePainterBase { switch (event) { case PointerHoverEvent hover: if (!_spacebarPressed) return; - _moveCamera(-hover.localDelta); + moveQtCamera(-hover.localDelta); case PointerMoveEvent move: if (!move.down) return; _onClick(move.localPosition); case PointerPanZoomUpdateEvent pan: - _moveCamera(pan.panDelta); + moveQtCamera(pan.panDelta); case PointerDownEvent click: _onClick(click.localPosition); } } - void _moveCamera(Offset offset) { - if (offset == Offset.zero) return; - _needsPaint = true; - _camera.move(offset); - // Ensure the camera stays within the quadtree boundary. - if (_camera.boundary.width > _quadTree.boundary.width || - _camera.boundary.height > _quadTree.boundary.height) { - final canvasAspectRatio = _size.width / _size.height; - final quadTreeAspectRatio = - _quadTree.boundary.width / _quadTree.boundary.height; - if (canvasAspectRatio > quadTreeAspectRatio) { - _camera.set(Rect.fromCenter( - center: _camera.boundary.center, - width: _quadTree.boundary.width, - height: _quadTree.boundary.width / canvasAspectRatio, - )); - } else { - _camera.set(Rect.fromCenter( - center: _camera.boundary.center, - width: _quadTree.boundary.height * canvasAspectRatio, - height: _quadTree.boundary.height, - )); - } - } - if (_camera.boundary.left < _quadTree.boundary.left) { - _camera.set(Rect.fromLTWH( - 0, - _camera.boundary.top, - _camera.boundary.width, - _camera.boundary.height, - )); - } else if (_camera.boundary.right > _quadTree.boundary.right) { - _camera.set(Rect.fromLTWH( - _quadTree.boundary.right - _camera.boundary.width, - _camera.boundary.top, - _camera.boundary.width, - _camera.boundary.height, - )); - } - if (_camera.boundary.top < _quadTree.boundary.top) { - _camera.set(Rect.fromLTWH( - _camera.boundary.left, - 0, - _camera.boundary.width, - _camera.boundary.height, - )); - } else if (_camera.boundary.bottom > _quadTree.boundary.bottom) { - _camera.set(Rect.fromLTWH( - _camera.boundary.left, - _quadTree.boundary.bottom - _camera.boundary.height, - _camera.boundary.width, - _camera.boundary.height, - )); - } - } - void _onClick(Offset point) { // Calculate the dot offset from the camera. - final cameraCenter = _camera.boundary.center; + final cameraCenter = cameraBoundary.center; final dot = Rect.fromCenter( center: Offset( - point.dx + cameraCenter.dx - _size.width / 2, - point.dy + cameraCenter.dy - _size.height / 2, + point.dx + cameraCenter.dx - size.width / 2, + point.dy + cameraCenter.dy - size.height / 2, ), width: 2, height: 2, ); - _quadTree.insert(dot); - _needsPaint = true; + quadTree.insert(dot); + needsPaintQt = true; } @override void update(RePaintBox box, Duration elapsed, double delta) { // If the size of the box has changed, update the camera too. - if (box.size != _size) { - _size = box.size; - _camera.set(Rect.fromCenter( - center: _camera.boundary.center, - width: _size.width, - height: _size.height, - )); - _needsPaint = true; + if (box.size != size) { + updateCameraBoundary(box.size); } - if (!_needsPaint) return; // No need to update the points and repaint. + if (!needsPaintQt) return; // No need to update the points and repaint. - final boundary = _camera.boundary; - final result = _quadTree.query(boundary); + final boundary = cameraBoundary; + final result = quadTree.query(boundary); if (result.isEmpty) { _points = Float32List(0); } else { @@ -299,9 +198,9 @@ class QuadTreePainter extends RePainterBase { ..write(' | ') ..write('World:') ..write(nbsp) - ..write(_quadTree.boundary.width.toStringAsFixed(0)) + ..write(quadTree.boundary.width.toStringAsFixed(0)) ..write('x') - ..write(_quadTree.boundary.height.toStringAsFixed(0)) + ..write(quadTree.boundary.height.toStringAsFixed(0)) ..write(' | ') ..write('Screen:') ..write(nbsp) @@ -311,19 +210,19 @@ class QuadTreePainter extends RePainterBase { ..write(' | ') ..write('Position:') ..write(nbsp) - ..write(_camera.boundary.left.toStringAsFixed(0)) + ..write(cameraBoundary.left.toStringAsFixed(0)) ..write('x') - ..write(_camera.boundary.top.toStringAsFixed(0)) + ..write(cameraBoundary.top.toStringAsFixed(0)) ..write(' | ') ..write('Points:') ..write(nbsp) ..write(_points.length ~/ 2) ..write('/') - ..write(_quadTree.length) + ..write(quadTree.length) ..write(' | ') ..write('Nodes:') ..write(nbsp) - ..write(_quadTree.nodes); + ..write(quadTree.nodes); _textPainter ..text = TextSpan( text: status.toString(), @@ -370,8 +269,8 @@ class QuadTreePainter extends RePainterBase { /// Draw the quadtree nodes on the canvas. void _drawQuadTree(Size size, Canvas canvas) { - final cam = _camera.boundary; - final queue = Queue()..add(_quadTree.root); + final cam = cameraBoundary; + final queue = Queue()..add(quadTree.root); while (queue.isNotEmpty) { final node = queue.removeFirst(); if (node == null) continue; @@ -419,26 +318,10 @@ class QuadTreePainter extends RePainterBase { _drawQuadTree(size, canvas); // Draw quadtree nodes _drawPoints(size, canvas); // Draw points _drawStatus(size, canvas); // Draw status - _needsPaint = false; // Reset the flag. + needsPaintQt = false; // Reset the flag. } } -class QuadTreeCamera { - QuadTreeCamera({ - required Rect boundary, - }) : _boundary = boundary; - - /// The boundary of the camera. - Rect get boundary => _boundary; - Rect _boundary; - - /// Move the camera by the given offset. - void move(Offset offset) => _boundary = _boundary.shift(offset); - - /// Set the camera to the given boundary. - void set(Rect boundary) => _boundary = boundary; -} - /// Sort the list of points by the y-coordinate. // ignore: unused_element void _sortByY(Float32List list) { diff --git a/lib/src/collisions/quadtree.dart b/lib/src/collisions/quadtree.dart index 54fa0bc..d0cf3ba 100644 --- a/lib/src/collisions/quadtree.dart +++ b/lib/src/collisions/quadtree.dart @@ -75,6 +75,33 @@ extension type QuadTree$QueryResult._(Float32List _bytes) { } } +/// Simple representation of a rectangle with an identifier. +class QuadTreeRect { + /// Creates a new rectangle with an identifier. + const QuadTreeRect({ + required this.id, + required this.left, + required this.top, + required this.right, + required this.bottom, + }); + + /// The identifier of the rectangle. + final int id; + + /// left x coordinate of the rectangle. + final double left; + + /// top y coordinate of the rectangle. + final double top; + + /// right x of the rectangle. + final double right; + + /// bottom y the rectangle. + final double bottom; +} + /// {@template quadtree} /// A Quadtree data structure that subdivides a 2D space into four quadrants /// to speed up collision detection and spatial queries. @@ -734,6 +761,105 @@ final class QuadTree { return QuadTree$QueryResult._(results.sublist(0, $length)); } + /// Returns the iterable of Rects that intersect with the given [rect]. + Iterable queryRectsIterable(ui.Rect rect) sync* { + //if (rect.isEmpty) return QuadTree$QueryResult._(Float32List(0)); + + final root = _root; + if (root == null || isEmpty) return; + + final objects = _objects; + var offset = 0; + + // If the query rectangle fully contains the QuadTree boundary. + // Return all objects in the QuadTree. + if (rect.left <= boundary.left && + rect.top <= boundary.top && + rect.right >= boundary.right && + rect.bottom >= boundary.bottom) { + if (root._subdivided) { + for (var i = 0; i < _nextObjectId; i++) { + if (_id2node[i] == 0) continue; + offset = i * _objectSize; + yield QuadTreeRect( + id: i, + left: objects[offset + 0], + top: objects[offset + 1], + right: objects[offset + 0] + objects[offset + 2], + bottom: objects[offset + 1] + objects[offset + 3], + ); + } + } else { + final rootIds = root._ids; + for (final id in rootIds) { + offset = id * _objectSize; + yield QuadTreeRect( + id: id, + left: objects[offset + 0], + top: objects[offset + 1], + right: objects[offset + 0] + objects[offset + 2], + bottom: objects[offset + 1] + objects[offset + 3], + ); + } + } + return; + } + + final subdivided = Queue()..add(root); + final leafs = []; + + // Find all leaf nodes from the subdivided nodes + while (subdivided.isNotEmpty) { + final node = subdivided.removeFirst(); + if (node.isEmpty) continue; + if (!_overlaps(node.boundary, rect)) continue; + if (node.subdivided) { + subdivided + ..add(node._northWest!) + ..add(node._northEast!) + ..add(node._southWest!) + ..add(node._southEast!); + } else { + leafs.add(node); + } + } + + // Find all objects in the leaf nodes + // hat intersect with the query rectangle + /* var j = 0; + for (var i = 0; i < leafs.length; i++) { + final node = leafs[i]; + if (!_overlaps(node.boundary, rect)) continue; + if (i != j) leafs[j] = leafs[i]; + j++; + } + leafs.length = j; */ + + // No leaf nodes found + if (leafs.isEmpty) return; + + // Fill the results with the objects from the leaf nodes + for (final node in leafs) { + for (final id in node._ids) { + offset = id * _objectSize; + final left = objects[offset + 0], + top = objects[offset + 1], + width = objects[offset + 2], + height = objects[offset + 3]; + if (!_overlapsLTWH(rect, left, top, width, height)) continue; + + yield QuadTreeRect( + id: id, + left: left, + top: top, + right: left + width, + bottom: top + height, + ); + } + } + return; + } + /// Call this on the root to try merging all possible child nodes. /// Recursively merges subtrees that have fewer than [capacity] /// objects in total. From 24aa51efb072e7bb5ef6d88db823ef0893af791b Mon Sep 17 00:00:00 2001 From: dvmatyun Date: Sat, 8 Feb 2025 16:10:18 +0400 Subject: [PATCH 2/3] collisions adjusted --- .../quadtree/quadtree_collision_screen.dart | 131 +++++++++--------- 1 file changed, 68 insertions(+), 63 deletions(-) diff --git a/example/lib/src/feature/quadtree/quadtree_collision_screen.dart b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart index e56c76b..c149fd8 100644 --- a/example/lib/src/feature/quadtree/quadtree_collision_screen.dart +++ b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'dart:developer'; import 'dart:math'; import 'package:flutter/gestures.dart'; @@ -57,6 +58,9 @@ class CollisionRectObject { // Only small rects can be moved bool get canBeMoved => rect.width < 128 && rect.height < 128; + double get diagonalLength => + sqrt(rect.width * rect.width + rect.height * rect.height); + Iterable get vertices { return [ Vector2d(rect.left, rect.bottom), @@ -73,11 +77,15 @@ class CollisionRectObject { const int prime1 = 73856093; const int prime2 = 19349663; const int prime3 = 83492791; - return ((id * prime1) ^ - (rect.left * prime2).round() ^ - (rect.top * prime3).round()); + return _hashCode ??= (id * 37 + + (id * prime1) + + ((rect.left * prime2).round() << 8) + + ((rect.top * prime3).round()) << + 4); } + int? _hashCode; + @override bool operator ==(Object other) => identical(this, other) || @@ -148,51 +156,42 @@ class Vector2d { } class CollisionUtil { + // Object a inside object b static ({Vector2d normal, double depth})? intercectRects( CollisionRectObject a, CollisionRectObject b) { - double minOverlap = double.infinity; - Vector2d bestNormal = const Vector2d(0, 0); - - for (var poly in [a, b]) { - final vertices = poly.vertices.toList(); - //print('verticies: [${vertices.join(', ')}]'); - for (int i = 0; i < vertices.length; i++) { - Vector2d p1 = vertices[i]; - Vector2d p2 = vertices[(i + 1) % vertices.length]; - Vector2d edge = p2 - p1; - Vector2d axis = edge.perpendicular().normalized(); - - // Project both polygons onto the axis - double minA = double.infinity, maxA = -double.infinity; - double minB = double.infinity, maxB = -double.infinity; - - for (Vector2d v in a.vertices) { - double projection = v.dot(axis); - minA = min(minA, projection); - maxA = max(maxA, projection); - } + Vector2d normal = Vector2d.zero(); + double depth = double.maxFinite; - for (Vector2d v in b.vertices) { - double projection = v.dot(axis); - minB = min(minB, projection); - maxB = max(maxB, projection); - } + List verticesA = a.vertices.toList(); + List verticesB = b.vertices.toList(); - // Check for gap - if (maxA < minB || maxB < minA) { - return null; // No collision - } + var normalAndDepthA = CollisionUtil.getNormalAndDepth( + verticesA, + verticesB, + ); - // Compute overlap - double overlap = min(maxA, maxB) - max(minA, minB); - if (overlap < minOverlap) { - minOverlap = overlap; - bestNormal = axis; - } - } + if (normalAndDepthA.depth < depth) { + depth = normalAndDepthA.depth; + normal = normalAndDepthA.normal; + } + var normalAndDepthB = CollisionUtil.getNormalAndDepth( + verticesB, + verticesA, + insverted: true, + ); + + if (normalAndDepthB.depth < depth) { + depth = normalAndDepthB.depth; + normal = normalAndDepthB.normal; } - return (normal: bestNormal, depth: minOverlap); + Vector2d direction = a.absoluteCenter - b.absoluteCenter; + + if (direction.dot(normal) < 0) { + normal = -normal; + } + + return (normal: normal, depth: depth); } static ({Vector2d normal, double depth}) getNormalAndDepth( @@ -285,39 +284,39 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { ..strokeWidth = 2; @override - // ignore: unnecessary_overrides void mountQtCamera() { super.mountQtCamera(); - // addSideBounds(); + addSideBounds(); } + // Bounds that will restrict the area where objects can move. void addSideBounds() { // Adding bounds to the screen box: const double boundWidth = 64; const double rectDimensions = 1024; final boundLeft = Rect.fromLTWH( cameraBoundary.center.dx - rectDimensions / 2 - boundWidth, - cameraBoundary.center.dy - rectDimensions / 2, + cameraBoundary.center.dy - rectDimensions, boundWidth, - rectDimensions, + rectDimensions * 3, ); final boundRight = Rect.fromLTWH( cameraBoundary.center.dx + rectDimensions / 2 - boundWidth, - cameraBoundary.center.dy - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy - rectDimensions - boundWidth, boundWidth, - rectDimensions, + rectDimensions * 3, ); final boundTop = Rect.fromLTWH( - cameraBoundary.center.dx - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dx - rectDimensions - boundWidth, cameraBoundary.center.dy - rectDimensions / 2 - boundWidth, - rectDimensions, + rectDimensions * 3, boundWidth, ); final boundBottom = Rect.fromLTWH( - cameraBoundary.center.dx - rectDimensions / 2, + cameraBoundary.center.dx - rectDimensions, cameraBoundary.center.dy + rectDimensions / 2 - boundWidth, - rectDimensions, + rectDimensions * 3, boundWidth, ); @@ -372,19 +371,20 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { if (colisionResult == null) { continue; } - //if (a.canBeMoved || b.canBeMoved) { - // print( - // '> colisionResult= ${colisionResult.normal}, depth=${colisionResult.depth} (delta=$dt)'); - //} + + // Changing velocity depending on how deep objects intercet. final depth = max(1.0, colisionResult.depth); + final velocityChange = colisionResult.normal * depth * dt * 64; if (a.canBeMoved) { - //final newVelocity = colisionResult.normal * colisionResult.depth; - a.velocity = colisionResult.normal * depth; + a.velocity = a.velocity + velocityChange; } + // both objects gain same velocity change, but in opposite directions. if (b.canBeMoved) { - b.velocity = colisionResult.normal * (-1) * depth; + b.velocity = b.velocity + velocityChange * -1; } } + + // Moving objects depending on their velocities: for (final e in collisionObjects.values) { quadTree.move(e.id, e.rect.left + e.velocity.x * dt, e.rect.top + e.velocity.y * dt); @@ -395,7 +395,7 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { final _collidingPairHashes = >{}; final _collidingObjectIds = {}; - CollidingPair? _checkCollideWith( + void _checkCollideWith( CollisionRectObject object, ) { for (final potential in quadTree.queryRectsIterable(object.rect)) { @@ -403,14 +403,19 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { final other = collisionObjects[potential.id]; if (other == null) continue; final pair = CollidingPair(object, other); - if (_collidingPairHashes.containsKey(pair.hash)) continue; + if (_collidingPairHashes.containsKey(pair.hash)) { + // just checking that hash function is correct. + final existing = _collidingPairHashes[pair.hash]!; + debugger( + when: !((existing.a.id == pair.a.id && + existing.b.id == pair.b.id) || + (existing.a.id == pair.b.id && existing.b.id == pair.a.id))); + continue; + } _collidingPairHashes[pair.hash] = pair; _collidingObjectIds.add(object.id); _collidingObjectIds.add(other.id); - return pair; } - - return null; } } From b8d3c5025cb4f5de71e7a801518c5e4387d9e6d9 Mon Sep 17 00:00:00 2001 From: dvmatyun Date: Sat, 8 Feb 2025 17:07:07 +0400 Subject: [PATCH 3/3] collision with immovable wall --- .../quadtree/quadtree_collision_screen.dart | 66 ++++----- lib/src/collisions/quadtree.dart | 126 ------------------ 2 files changed, 29 insertions(+), 163 deletions(-) diff --git a/example/lib/src/feature/quadtree/quadtree_collision_screen.dart b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart index c149fd8..a2440e6 100644 --- a/example/lib/src/feature/quadtree/quadtree_collision_screen.dart +++ b/example/lib/src/feature/quadtree/quadtree_collision_screen.dart @@ -293,29 +293,29 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { // Bounds that will restrict the area where objects can move. void addSideBounds() { // Adding bounds to the screen box: - const double boundWidth = 64; + const double boundWidth = 128; const double rectDimensions = 1024; final boundLeft = Rect.fromLTWH( - cameraBoundary.center.dx - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dx - rectDimensions / 2 - boundWidth / 2, cameraBoundary.center.dy - rectDimensions, boundWidth, rectDimensions * 3, ); final boundRight = Rect.fromLTWH( - cameraBoundary.center.dx + rectDimensions / 2 - boundWidth, + cameraBoundary.center.dx + rectDimensions / 2 - boundWidth / 2, cameraBoundary.center.dy - rectDimensions - boundWidth, boundWidth, rectDimensions * 3, ); final boundTop = Rect.fromLTWH( cameraBoundary.center.dx - rectDimensions - boundWidth, - cameraBoundary.center.dy - rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy - rectDimensions / 2 - boundWidth / 2, rectDimensions * 3, boundWidth, ); final boundBottom = Rect.fromLTWH( cameraBoundary.center.dx - rectDimensions, - cameraBoundary.center.dy + rectDimensions / 2 - boundWidth, + cameraBoundary.center.dy + rectDimensions / 2 - boundWidth / 2, rectDimensions * 3, boundWidth, ); @@ -375,12 +375,30 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { // Changing velocity depending on how deep objects intercet. final depth = max(1.0, colisionResult.depth); final velocityChange = colisionResult.normal * depth * dt * 64; + + Vector2d changeVelocityOnImmovableCollide( + Vector2d velocity, Vector2d change) { + return Vector2d( + (velocity.x.sign == change.x.sign) ? velocity.x : -(velocity.x), + (velocity.y.sign == change.y.sign) ? velocity.y : -(velocity.y), + ); + } + if (a.canBeMoved) { + if (!b.canBeMoved) { + a.velocity = + changeVelocityOnImmovableCollide(a.velocity, velocityChange); + } a.velocity = a.velocity + velocityChange; } // both objects gain same velocity change, but in opposite directions. if (b.canBeMoved) { - b.velocity = b.velocity + velocityChange * -1; + final velocityChangeB = velocityChange * -1; + if (!a.canBeMoved) { + b.velocity = + changeVelocityOnImmovableCollide(b.velocity, velocityChangeB); + } + b.velocity = b.velocity + velocityChangeB; } } @@ -398,9 +416,10 @@ mixin CollisionObjectsMixin on QuadTreeCameraMixin { void _checkCollideWith( CollisionRectObject object, ) { - for (final potential in quadTree.queryRectsIterable(object.rect)) { - if (potential.id == object.id) continue; - final other = collisionObjects[potential.id]; + final queryResult = quadTree.query(object.rect); + for (final potentialId in queryResult.ids) { + if (potentialId == object.id) continue; + final other = collisionObjects[potentialId]; if (other == null) continue; final pair = CollidingPair(object, other); if (_collidingPairHashes.containsKey(pair.hash)) { @@ -498,7 +517,7 @@ class QuadTreeCollisionPainter extends RePainterBase return; } _wasAdded = true; - Future.delayed(const Duration(milliseconds: 500), () { + Future.delayed(const Duration(milliseconds: 150), () { _wasAdded = false; }); // Calculate the dot offset from the camera. @@ -523,33 +542,6 @@ class QuadTreeCollisionPainter extends RePainterBase updateCameraBoundary(box.size); } updateCollisionObjects(delta); - - if (!needsPaintQt) return; // No need to update the points and repaint. - - /* - final boundary = cameraBoundary; - final result = quadTree.query(boundary); - if (result.isEmpty) { - _points = Float32List(0); - } else { - _points = Float32List(result.length * 2); - var counter = 0; - result.forEach(( - int id, - double left, - double top, - double width, - double height, - ) { - _points - ..[counter] = left + width / 2 - boundary.left - ..[counter + 1] = top + height / 2 - boundary.top; - counter += 2; - return true; - }); - //_sortByY(_points); - } - */ } final TextPainter _textPainter = TextPainter( diff --git a/lib/src/collisions/quadtree.dart b/lib/src/collisions/quadtree.dart index d0cf3ba..54fa0bc 100644 --- a/lib/src/collisions/quadtree.dart +++ b/lib/src/collisions/quadtree.dart @@ -75,33 +75,6 @@ extension type QuadTree$QueryResult._(Float32List _bytes) { } } -/// Simple representation of a rectangle with an identifier. -class QuadTreeRect { - /// Creates a new rectangle with an identifier. - const QuadTreeRect({ - required this.id, - required this.left, - required this.top, - required this.right, - required this.bottom, - }); - - /// The identifier of the rectangle. - final int id; - - /// left x coordinate of the rectangle. - final double left; - - /// top y coordinate of the rectangle. - final double top; - - /// right x of the rectangle. - final double right; - - /// bottom y the rectangle. - final double bottom; -} - /// {@template quadtree} /// A Quadtree data structure that subdivides a 2D space into four quadrants /// to speed up collision detection and spatial queries. @@ -761,105 +734,6 @@ final class QuadTree { return QuadTree$QueryResult._(results.sublist(0, $length)); } - /// Returns the iterable of Rects that intersect with the given [rect]. - Iterable queryRectsIterable(ui.Rect rect) sync* { - //if (rect.isEmpty) return QuadTree$QueryResult._(Float32List(0)); - - final root = _root; - if (root == null || isEmpty) return; - - final objects = _objects; - var offset = 0; - - // If the query rectangle fully contains the QuadTree boundary. - // Return all objects in the QuadTree. - if (rect.left <= boundary.left && - rect.top <= boundary.top && - rect.right >= boundary.right && - rect.bottom >= boundary.bottom) { - if (root._subdivided) { - for (var i = 0; i < _nextObjectId; i++) { - if (_id2node[i] == 0) continue; - offset = i * _objectSize; - yield QuadTreeRect( - id: i, - left: objects[offset + 0], - top: objects[offset + 1], - right: objects[offset + 0] + objects[offset + 2], - bottom: objects[offset + 1] + objects[offset + 3], - ); - } - } else { - final rootIds = root._ids; - for (final id in rootIds) { - offset = id * _objectSize; - yield QuadTreeRect( - id: id, - left: objects[offset + 0], - top: objects[offset + 1], - right: objects[offset + 0] + objects[offset + 2], - bottom: objects[offset + 1] + objects[offset + 3], - ); - } - } - return; - } - - final subdivided = Queue()..add(root); - final leafs = []; - - // Find all leaf nodes from the subdivided nodes - while (subdivided.isNotEmpty) { - final node = subdivided.removeFirst(); - if (node.isEmpty) continue; - if (!_overlaps(node.boundary, rect)) continue; - if (node.subdivided) { - subdivided - ..add(node._northWest!) - ..add(node._northEast!) - ..add(node._southWest!) - ..add(node._southEast!); - } else { - leafs.add(node); - } - } - - // Find all objects in the leaf nodes - // hat intersect with the query rectangle - /* var j = 0; - for (var i = 0; i < leafs.length; i++) { - final node = leafs[i]; - if (!_overlaps(node.boundary, rect)) continue; - if (i != j) leafs[j] = leafs[i]; - j++; - } - leafs.length = j; */ - - // No leaf nodes found - if (leafs.isEmpty) return; - - // Fill the results with the objects from the leaf nodes - for (final node in leafs) { - for (final id in node._ids) { - offset = id * _objectSize; - final left = objects[offset + 0], - top = objects[offset + 1], - width = objects[offset + 2], - height = objects[offset + 3]; - if (!_overlapsLTWH(rect, left, top, width, height)) continue; - - yield QuadTreeRect( - id: id, - left: left, - top: top, - right: left + width, - bottom: top + height, - ); - } - } - return; - } - /// Call this on the root to try merging all possible child nodes. /// Recursively merges subtrees that have fewer than [capacity] /// objects in total.