Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View File

@@ -0,0 +1,489 @@
import 'package:flame/components.dart';
import '../models/game_diff.dart';
import '../models/runtime_event.dart';
import '../models/runtime_node.dart';
import '../protocol/runtime_protocol.dart';
import '../resources/game_resource_manager.dart';
import 'runtime_component.dart';
class RenderTreeController {
RenderTreeController({
required Component root,
required GameResourceManager resources,
required void Function(RuntimeEvent event) eventSink,
this.onScopeRemoved,
}) : _root = root,
_resources = resources,
_eventSink = eventSink;
final Component _root;
final GameResourceManager _resources;
final void Function(RuntimeEvent event) _eventSink;
final Map<String, RuntimeComponent> _components = {};
final Map<String, int> _epochs = {};
final Map<String, Vector2> _scrollVelocities = {};
void Function(String id)? onScopeRemoved;
RuntimeComponent? componentById(String id) => _components[id];
bool contains(String id) => _components.containsKey(id);
String? listViewAt(Vector2 canvasPosition) {
final hits =
_components.values
.where(
(component) =>
component.node.type == RuntimeNodeType.listView &&
component.containsVisualPoint(canvasPosition),
)
.toList(growable: false)
..sort((a, b) => b.priority.compareTo(a.priority));
return hits.isEmpty ? null : hits.first.node.id;
}
bool scrollListViewAt(
Vector2 canvasPosition, {
double deltaX = 0,
double deltaY = 0,
String source = 'wheel',
}) {
final id = listViewAt(canvasPosition);
if (id == null) {
return false;
}
return scrollListView(id, deltaX: deltaX, deltaY: deltaY, source: source);
}
bool scrollListView(
String id, {
double deltaX = 0,
double deltaY = 0,
String source = 'program',
}) {
final component = _components[id];
if (component == null || component.node.type != RuntimeNodeType.listView) {
return false;
}
final node = component.node;
final viewport = _listViewContentViewport(node);
final nextScrollX = _clampScroll(
node.scrollX + deltaX,
viewportExtent: viewport.x,
contentExtent: node.contentWidth,
);
final nextScrollY = _clampScroll(
node.scrollY + deltaY,
viewportExtent: viewport.y,
contentExtent: node.contentHeight,
);
if (nextScrollX == node.scrollX && nextScrollY == node.scrollY) {
return false;
}
component.updateNode(
node.copyWithProps({'scrollX': nextScrollX, 'scrollY': nextScrollY}),
);
_reattachChildrenOf(id);
_emitScrollEvent(component, source: source);
return true;
}
void setListViewVelocity(String id, Vector2 velocity) {
final component = _components[id];
if (component == null || component.node.type != RuntimeNodeType.listView) {
return;
}
if (!component.node.inertia) {
_scrollVelocities.remove(id);
return;
}
if (velocity.length2 < 1) {
_scrollVelocities.remove(id);
return;
}
_scrollVelocities[id] = velocity;
}
void stopListViewVelocity(String id) {
_scrollVelocities.remove(id);
}
void updateListViewInertia(double dt) {
if (_scrollVelocities.isEmpty || dt <= 0) {
return;
}
for (final entry in _scrollVelocities.entries.toList(growable: false)) {
final id = entry.key;
final velocity = entry.value;
final consumed = scrollListView(
id,
deltaX: velocity.x * dt,
deltaY: velocity.y * dt,
source: 'inertia',
);
final next = velocity * 0.88;
if (!consumed || next.length2 < 25) {
_scrollVelocities.remove(id);
} else {
_scrollVelocities[id] = next;
}
}
}
int epochOf(String id) => _epochs[id] ?? 0;
bool isNodeEpochAlive(String id, int epoch) {
return _components.containsKey(id) && epochOf(id) == epoch;
}
void clear() {
final ids = _components.keys.toList(growable: false);
for (final component in _components.values) {
component.removeFromParent();
}
_components.clear();
for (final id in ids) {
_bumpEpoch(id);
onScopeRemoved?.call(id);
}
}
void removeById(String id) {
final removedIds = <String>[];
for (final childId in _descendantIdsOf(id)) {
final child = _components.remove(childId);
if (child != null) {
removedIds.add(childId);
_bumpEpoch(childId);
child.removeFromParent();
}
}
final component = _components.remove(id);
if (component != null) {
removedIds.add(id);
_bumpEpoch(id);
component.removeFromParent();
}
for (final removedId in removedIds) {
onScopeRemoved?.call(removedId);
}
}
void apply(NodeDiff diff) {
_validateDiff(diff);
for (final remove in diff.removes) {
removeById(remove.id);
}
for (final create in diff.creates) {
_createOrReplace(create);
}
for (final update in diff.updates) {
final component = _components[update.id];
if (component == null) {
continue;
}
final nextNode = component.node.copyWithProps(update.props);
component.updateNode(nextNode);
_attachToParent(component);
_reattachChildrenOf(component.node.id);
}
}
void _validateDiff(NodeDiff diff) {
final nodes = <String, RuntimeNode>{
for (final entry in _components.entries) entry.key: entry.value.node,
};
for (final remove in diff.removes) {
_removeNodeSnapshot(nodes, remove.id);
}
for (final create in diff.creates) {
nodes[create.id] = create;
}
for (final update in diff.updates) {
final current = nodes[update.id];
if (current == null) {
continue;
}
nodes[update.id] = current.copyWithProps(update.props);
}
_validateParentGraph(nodes);
}
void _removeNodeSnapshot(Map<String, RuntimeNode> nodes, String id) {
final descendants = _descendantIdsOfSnapshot(nodes, id);
for (final childId in descendants) {
nodes.remove(childId);
}
nodes.remove(id);
}
List<String> _descendantIdsOfSnapshot(
Map<String, RuntimeNode> nodes,
String parentId,
) {
final descendants = <String>[];
for (final node in nodes.values) {
if (node.parent == parentId) {
descendants.add(node.id);
descendants.addAll(_descendantIdsOfSnapshot(nodes, node.id));
}
}
return descendants;
}
void _validateParentGraph(Map<String, RuntimeNode> nodes) {
for (final node in nodes.values) {
final parentId = node.parent;
if (parentId == null) {
continue;
}
if (parentId == node.id) {
throw const FormatException(
'RuntimeNode.parent cannot reference itself',
);
}
final seen = <String>{node.id};
var currentId = parentId;
while (true) {
if (!seen.add(currentId)) {
throw FormatException(
'RuntimeNode.parent would create a cycle: ${node.id} -> $parentId',
);
}
final parent = nodes[currentId];
final nextId = parent?.parent;
if (nextId == null) {
break;
}
currentId = nextId;
}
}
}
void _createOrReplace(RuntimeNode node) {
_validateParent(node);
final existing = _components.remove(node.id);
existing?.removeFromParent();
final epoch = _bumpEpoch(node.id);
late final RuntimeComponent component;
component = RuntimeComponent(
node: node,
resources: _resources,
onNodeTap: (tappedNode, localPosition) {
if (_components[tappedNode.id] != component) {
return;
}
_eventSink(
RuntimeEvent(
type: RuntimeEventType.tap,
target: tappedNode.id,
handler: tappedNode.onTap,
x: localPosition.x,
y: localPosition.y,
scope: tappedNode.id,
targetEpoch: epoch,
scopeEpoch: epoch,
),
);
},
);
_components[node.id] = component;
_attachToParent(component);
_reattachChildrenOf(node.id);
}
int _bumpEpoch(String id) {
final next = (_epochs[id] ?? 0) + 1;
_epochs[id] = next;
return next;
}
void _attachToParent(RuntimeComponent component) {
final parentId = component.node.parent;
final parent = parentId == null ? null : _components[parentId];
final target = parent ?? _root;
final parentIsListView = parent?.node.type == RuntimeNodeType.listView;
final parentScrollX = parentIsListView ? parent!.node.scrollX : 0.0;
final parentScrollY = parentIsListView ? parent!.node.scrollY : 0.0;
final parentContentOffset = parentIsListView
? parent!.listViewContentOffset()
: Vector2.zero();
component.setParentScroll(
x: parentScrollX,
y: parentScrollY,
contentOffsetX: parentContentOffset.x,
contentOffsetY: parentContentOffset.y,
);
component.setViewportCulled(_isCulledByParentListView(component, parent));
if (component.parent == target) {
return;
}
component.removeFromParent();
target.add(component);
}
void _validateParent(RuntimeNode node) {
final parentId = node.parent;
if (parentId != null && _wouldCreateCycle(node.id, parentId)) {
throw FormatException(
'RuntimeNode.parent would create a cycle: ${node.id} -> $parentId',
);
}
}
void _reattachChildrenOf(String parentId) {
for (final component in _components.values.toList(growable: false)) {
if (component.node.parent == parentId) {
_attachToParent(component);
}
}
}
List<String> _descendantIdsOf(String parentId) {
final descendants = <String>[];
for (final component in _components.values) {
if (component.node.parent == parentId) {
descendants.add(component.node.id);
descendants.addAll(_descendantIdsOf(component.node.id));
}
}
return descendants;
}
bool _isCulledByParentListView(
RuntimeComponent component,
RuntimeComponent? parent,
) {
final parentNode = parent?.node;
if (parentNode == null ||
parentNode.type != RuntimeNodeType.listView ||
!parentNode.virtualized) {
return false;
}
final cache = parentNode.cacheExtent;
final viewportLeft = parentNode.scrollX - cache;
final viewportTop = parentNode.scrollY - cache;
final viewport = parent?.listViewContentViewport() ?? Vector2.zero();
final viewportRight = parentNode.scrollX + viewport.x + cache;
final viewportBottom = parentNode.scrollY + viewport.y + cache;
final node = component.node;
final childLeft = node.x;
final childTop = node.y;
final childRight = node.x + (node.width ?? 0);
final childBottom = node.y + (node.height ?? 0);
return childRight < viewportLeft ||
childLeft > viewportRight ||
childBottom < viewportTop ||
childTop > viewportBottom;
}
Vector2 _listViewContentViewport(RuntimeNode node) {
final width = node.width ?? 0;
final height = node.height ?? 0;
final left = node.paddingLeft.clamp(0.0, width).toDouble();
final top = node.paddingTop.clamp(0.0, height).toDouble();
if (!node.scrollbarVisible) {
return Vector2(
(width - left - node.paddingRight).clamp(0.0, width).toDouble(),
(height - top - node.paddingBottom).clamp(0.0, height).toDouble(),
);
}
final thickness = (node.scrollbarThickness ?? 5).clamp(1.0, 16.0);
final gutter = thickness + 8;
var vertical = (node.contentHeight ?? 0) > height && height > 0;
var horizontal = (node.contentWidth ?? 0) > width && width > 0;
for (var i = 0; i < 2; i += 1) {
final viewportWidth =
width - left - node.paddingRight - (vertical ? gutter : 0);
final viewportHeight =
height - top - node.paddingBottom - (horizontal ? gutter : 0);
vertical =
(node.contentHeight ?? 0) > viewportHeight && viewportHeight > 0;
horizontal =
(node.contentWidth ?? 0) > viewportWidth && viewportWidth > 0;
}
return Vector2(
(width - left - node.paddingRight - (vertical ? gutter : 0))
.clamp(0.0, width)
.toDouble(),
(height - top - node.paddingBottom - (horizontal ? gutter : 0))
.clamp(0.0, height)
.toDouble(),
);
}
double _clampScroll(
double value, {
required double? viewportExtent,
required double? contentExtent,
}) {
final maxScroll = (contentExtent ?? 0) - (viewportExtent ?? 0);
if (maxScroll <= 0) {
return 0;
}
return value.clamp(0, maxScroll).toDouble();
}
void _emitScrollEvent(RuntimeComponent component, {required String source}) {
final node = component.node;
final handler = node.onScroll;
if (handler == null || handler.isEmpty) {
return;
}
final id = node.id;
_eventSink(
RuntimeEvent(
type: RuntimeEventType.scroll,
target: id,
handler: handler,
scope: id,
targetEpoch: epochOf(id),
scopeEpoch: epochOf(id),
data: {
'scrollX': node.scrollX,
'scrollY': node.scrollY,
'maxScrollX': _clampScroll(
double.infinity,
viewportExtent: node.width,
contentExtent: node.contentWidth,
),
'maxScrollY': _clampScroll(
double.infinity,
viewportExtent: node.height,
contentExtent: node.contentHeight,
),
'source': source,
},
),
);
}
bool _wouldCreateCycle(String nodeId, String parentId) {
var currentId = parentId;
while (true) {
if (currentId == nodeId) {
return true;
}
final parent = _components[currentId];
final nextId = parent?.node.parent;
if (nextId == null) {
return false;
}
currentId = nextId;
}
}
}

File diff suppressed because it is too large Load Diff