nbody_sim_flutter 0.1.2
nbody_sim_flutter: ^0.1.2 copied to clipboard
Flutter UI wrapper widgets and theme primitives for nbody_sim_core.
nbody_sim_flutter #
Reusable Flutter UI wrapper for nbody_sim_core.
High-iteration N-body vibes, production-ready wrapper APIs.
Features #
SimulationCanvasfor interactive 2D N-body rendering.CameraControlsfor quick fit/zoom actions.SimulatorVisualThemeandbuildSimulatorTheme(...)for visual tokens.
Install #
dependencies:
nbody_sim_flutter: ^0.1.2
nbody_sim_core: ^0.1.2
What This Package Owns #
- Canvas rendering and gestures.
- Camera interaction plumbing (tap select, pan, zoom, drag selected body).
- Visual theme tokens and default Material theme setup.
What This Package Does Not Own #
- Physics engine execution (owned by
nbody_sim_core). - App workflows (play/pause/reset, scenario import/export panels, inspector forms).
- Product-specific layout/routes.
Quick Start #
import 'package:nbody_sim_flutter/nbody_sim_flutter.dart';
import 'package:nbody_sim_core/models.dart';
SimulationCanvas(
bodies: const [],
config: SimulationConfig.scientificDefault,
selectedBodyId: null,
cameraMode: CameraMode.fit,
cameraCenter: Vec2.zero,
cameraZoom: 1,
visualTheme: SimulatorVisualTheme.fromColorScheme(
Theme.of(context).colorScheme,
),
renderOptions: const RenderOptions(),
trails: const {},
onSelectBody: (_) {},
onPanCamera: (_) {},
onZoomCamera: (_) {},
onMoveSelectedBody: (_) {},
);
Full Integration Pattern #
Typical pattern:
- Run physics using
SimulationEnginefromnbody_sim_core. - Hold state in your own controller/bloc/provider.
- Feed state into
SimulationCanvas. - Wire callbacks back into your controller (
select,pan,zoom,move selected body).
Theme Integration #
MaterialApp(
theme: buildSimulatorTheme(Brightness.light),
darkTheme: buildSimulatorTheme(Brightness.dark),
themeMode: ThemeMode.system,
home: const YourSimulationPage(),
);
Theme Configuration #
You can fully configure visuals in two ways:
- Build directly from your app
ColorScheme:
final visualTheme = SimulatorVisualTheme.fromColorScheme(
Theme.of(context).colorScheme,
);
- Override specific tokens:
final customVisualTheme = SimulatorVisualTheme.fromColorScheme(
Theme.of(context).colorScheme,
).copyWith(
gridColor: Colors.white.withValues(alpha: 0.12),
vectorColor: Colors.cyanAccent.withValues(alpha: 0.45),
);
Token groups available on SimulatorVisualTheme:
- Canvas gradients (
canvasGradientStart,canvasGradientMid,canvasGradientEnd) - Panel/chip colors (
hudSurface,hudBorder,hudChipSurface,hudChipBorder,hudOnChip) - Simulation overlays (
gridColor,vectorColor,selectionColor,fieldOverlayColor,lensingColor)
Camera Controls Example #
CameraControls(
onFit: fitCamera,
onZoomIn: () => zoomBy(1.1),
onZoomOut: () => zoomBy(0.9),
);
Camera Mode Semantics #
SimulationCanvas supports these modes from nbody_sim_core:
CameraMode.fit- Auto-fits alive bodies in view.
- Ignores manual
cameraCenter/cameraZoomwhile active.
CameraMode.free- Uses your
cameraCenterandcameraZoom. - Intended for user-driven panning/zooming.
- Uses your
CameraMode.followSelected- Tracks the currently selected body when available.
- Falls back to
cameraCenterwhen no selected body exists.
Interaction Contract of SimulationCanvas #
SimulationCanvas expects:
bodies,config,renderOptions, camera state, trails, visual theme.- Selection and camera callbacks:
onSelectBody(String? id)onPanCamera(Vec2 delta)onZoomCamera(double factor)onMoveSelectedBody(Vec2 worldPosition)
Built-in behavior:
- Tap near body => selects nearest body.
- Wheel/pinch => emits zoom factors.
- Drag empty space => emits pan deltas.
- Drag selected body => emits world position updates.
State ownership note:
SimulationCanvasemits interaction intents; your app owns state/engine.- You can use any state management approach (
ChangeNotifier, Riverpod, Bloc, etc.). - The package does not require
setState; rebuild strategy is host-app choice.
Engine Tick Loop Integration (nbody_sim_core) #
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:nbody_sim_core/engine.dart';
import 'package:nbody_sim_core/models.dart';
class SimulationRuntime extends ChangeNotifier {
SimulationRuntime({
SimulationEngine? engine,
this.ticksPerFrame = 1,
}) : _engine = engine ?? IsolateSimulationEngine();
final SimulationEngine _engine;
final int ticksPerFrame;
Timer? _timer;
bool _isStepping = false;
List<SimulationBody> bodies = const [];
String? selectedId;
Vec2 cameraCenter = Vec2.zero;
double cameraZoom = 1;
CameraMode cameraMode = CameraMode.fit;
RenderOptions renderOptions = const RenderOptions();
final Map<String, List<Vec2>> trails = <String, List<Vec2>>{};
Future<void> initialize(List<SimulationBody> initialBodies) async {
await _engine.initialize(
config: SimulationConfig.scientificDefault,
bodies: initialBodies,
);
bodies = _engine.getState().bodies;
_appendTrailPoints();
notifyListeners();
}
void start() {
_timer ??= Timer.periodic(const Duration(milliseconds: 16), (_) async {
if (_isStepping) {
return;
}
_isStepping = true;
try {
await _engine.step(ticksPerFrame);
bodies = _engine.getState().bodies;
_appendTrailPoints();
notifyListeners();
} finally {
_isStepping = false;
}
});
}
void stop() {
_timer?.cancel();
_timer = null;
}
Future<void> disposeRuntime() async {
stop();
await _engine.dispose();
}
void _appendTrailPoints() {
final maxPoints = renderOptions.trailLength;
for (final body in bodies.where((b) => b.alive)) {
final history = trails.putIfAbsent(body.id, () => <Vec2>[]);
history.add(body.position);
if (history.length > maxPoints) {
history.removeRange(0, history.length - maxPoints);
}
}
}
}
Minimal Listenable Example (Recommended) #
class SimulationViewModel extends ChangeNotifier {
String? selectedId;
Vec2 cameraCenter = Vec2.zero;
double cameraZoom = 1;
List<SimulationBody> bodies = const [];
void selectBody(String? id) {
selectedId = id;
notifyListeners();
}
void pan(Vec2 delta) {
cameraCenter = Vec2(cameraCenter.x + delta.x, cameraCenter.y + delta.y);
notifyListeners();
}
void zoom(double factor) {
cameraZoom = (cameraZoom * factor).clamp(0.1, 50.0);
notifyListeners();
}
}
class DemoPage extends StatelessWidget {
DemoPage({super.key});
final model = SimulationViewModel();
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: model,
builder: (_, __) => SimulationCanvas(
bodies: model.bodies,
config: SimulationConfig.scientificDefault,
selectedBodyId: model.selectedId,
cameraMode: CameraMode.fit,
cameraCenter: model.cameraCenter,
cameraZoom: model.cameraZoom,
visualTheme: SimulatorVisualTheme.fromColorScheme(
Theme.of(context).colorScheme,
),
renderOptions: const RenderOptions(),
trails: const {},
onSelectBody: model.selectBody,
onPanCamera: model.pan,
onZoomCamera: model.zoom,
onMoveSelectedBody: (world) {
// forward to your engine/controller edit pipeline
},
),
);
}
}
Notes #
- This package focuses on reusable rendering/theme primitives only.
- Physics state and engine control come from
nbody_sim_core.
Performance Notes #
- The canvas paint surface is wrapped with
RepaintBoundaryto isolate heavy repaints from surrounding controls/layout. - For dense scenes, keep overlay options selective (
showFieldOverlay,showLensingOverlay,showVelocityVectors) when targeting mobile devices.
Trail Lifecycle Guidance #
- Cap trail history per body (for example,
renderOptions.trailLength). - Remove trail entries for deleted bodies to avoid stale memory use.
- Lower trail length and/or quality on low-end devices for smoother panning.
Troubleshooting #
- Canvas looks static:
- Ensure your app is ticking physics and feeding updated
bodies.
- Ensure your app is ticking physics and feeding updated
- Selection feels off:
- Verify camera state (
cameraCenter,cameraZoom) is updated consistently from callbacks.
- Verify camera state (
- Colors look wrong:
- Pass your own
SimulatorVisualThemeinstead of default derived values.
- Pass your own