Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/managers/ar_object_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,22 @@ class ARObjectManager {
removeNode(ARNode node) {
_channel.invokeMethod<String>('removeNode', {'name': node.name});
}

/// Pushes the given node's current transformation to the underlying AR scene.
/// Call this after programmatically changing a node's [ARNode.transform],
/// [ARNode.position], [ARNode.rotation], [ARNode.scale] or [ARNode.eulerAngles]
/// to synchronize the change with the native AR engine.
///
/// Fire-and-forget (like [removeNode]): it is intentionally not awaitable
/// because the Android side only sends a reply when pan/rotation handling is
/// enabled, so awaiting would hang when gestures are disabled. On Android the
/// transform is applied only when `handlePans` or `handleRotation` was enabled
/// in [ARSessionManager.onInitialize].
void updateNode(ARNode node) {
_channel.invokeMethod<void>('transformationChanged', {
'name': node.name,
'transformation':
const MatrixValueNotifierConverter().toJson(node.transformNotifier),
});
}
}
8 changes: 4 additions & 4 deletions lib/managers/ar_session_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class ARSessionManager {
final PlaneDetectionConfig planeDetectionConfig;

/// Receives hit results from user taps with tracked planes or feature points
late ARHitResultHandler onPlaneOrPointTap;
ARHitResultHandler? onPlaneOrPointTap;

/// Receives total number of Planes when a plane is detected and added to the view
late ARPlaneResultHandler onPlaneDetected;
ARPlaneResultHandler? onPlaneDetected;

/// Callback that is triggered once error is triggered
ErrorHandler? onError;
Expand Down Expand Up @@ -158,13 +158,13 @@ class ARSessionManager {
final hitTestResults = serializedHitTestResults.map((e) {
return ARHitTestResult.fromJson(e);
}).toList();
onPlaneOrPointTap(hitTestResults);
onPlaneOrPointTap?.call(hitTestResults);
}
break;
case 'onPlaneDetected':
if (onPlaneDetected != null) {
final planeCountResult = call.arguments as int;
onPlaneDetected(planeCountResult);
onPlaneDetected?.call(planeCountResult);
}
break;
case 'dispose':
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ version: 0.0.3
repository: https://github.com/hlefe/ar_flutter_plugin_2

environment:
sdk: ">=2.16.1 <3.0.0"
sdk: ">=2.16.1 <4.0.0"
flutter: ">=1.20.0"

dependencies:
flutter:
sdk: flutter
permission_handler: ^11.3.1
permission_handler: ">=11.3.1 <13.0.0"
vector_math: ^2.1.1
json_annotation: ^4.9.0
geolocator: ^12.0.0
Expand Down
53 changes: 53 additions & 0 deletions test/ar_flutter_plugin_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ar_flutter_plugin_2/ar_flutter_plugin.dart';
import 'package:ar_flutter_plugin_2/managers/ar_session_manager.dart';
import 'package:ar_flutter_plugin_2/managers/ar_object_manager.dart';
import 'package:ar_flutter_plugin_2/datatypes/config_planedetection.dart';
import 'package:ar_flutter_plugin_2/datatypes/node_types.dart';
import 'package:ar_flutter_plugin_2/models/ar_node.dart';

void main() {
const MethodChannel channel = MethodChannel('ar_flutter_plugin_2');
Expand All @@ -20,4 +26,51 @@ void main() {
test('getPlatformVersion', () async {
expect(await ArFlutterPlugin.platformVersion, '42');
});

// Regression test for upstream issue #8: the session event handlers used to
// be declared `late` and non-nullable, so reading them before the app
// assigned one threw `LateInitializationError: Field 'onPlaneDetected' has
// not been initialized.` They must now be nullable and default to null.
testWidgets('session event handlers are null-safe when unset (issue #8)',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
final context = tester.element(find.byType(SizedBox));

final sessionManager =
ARSessionManager(0, context, PlaneDetectionConfig.none);

expect(sessionManager.onPlaneOrPointTap, isNull);
expect(sessionManager.onPlaneDetected, isNull);
});

// Regression test for upstream issue #6: `updateNode` did not exist even
// though it was the documented way to sync transform changes. It must push a
// `transformationChanged` call (the method the native side already handles).
test('updateNode pushes transformationChanged to the object channel (issue #6)',
() async {
const objectChannel = MethodChannel('arobjects_0');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
final calls = <MethodCall>[];
messenger.setMockMethodCallHandler(objectChannel, (call) async {
calls.add(call);
return null;
});
addTearDown(() => messenger.setMockMethodCallHandler(objectChannel, null));

final objectManager = ARObjectManager(0);
final node = ARNode(
type: NodeType.localGLTF2, uri: 'model.gltf', name: 'myNode');

// updateNode is fire-and-forget; flush the event queue so the mock handler
// records the dispatched platform call before asserting.
objectManager.updateNode(node);
await pumpEventQueue();

expect(calls.single.method, 'transformationChanged');
final args = calls.single.arguments as Map;
expect(args['name'], 'myNode');
expect(args['transformation'], isA<List<dynamic>>());
expect((args['transformation'] as List).length, 16);
});
}