Initial flame_lua_runtime package
This commit is contained in:
173
lib/runtime/models/game_diff.dart
Normal file
173
lib/runtime/models/game_diff.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'runtime_command.dart';
|
||||
import 'runtime_node.dart';
|
||||
|
||||
import '../protocol/runtime_protocol.dart';
|
||||
|
||||
class NodeUpdate {
|
||||
const NodeUpdate({required this.id, required this.props});
|
||||
|
||||
final String id;
|
||||
final Map<String, Object?> props;
|
||||
|
||||
static NodeUpdate fromMap(Map<String, Object?> map) {
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
map,
|
||||
allowed: RuntimeProtocolSchema.nodeUpdateFields,
|
||||
context: 'NodeUpdate',
|
||||
);
|
||||
final id = map[RuntimeProtocolField.id];
|
||||
if (id is! String || id.isEmpty) {
|
||||
throw const FormatException('NodeUpdate.id must be a string');
|
||||
}
|
||||
|
||||
final props = map[RuntimeProtocolField.props];
|
||||
if (props is! Map) {
|
||||
throw const FormatException('NodeUpdate.props must be a map');
|
||||
}
|
||||
|
||||
final typedProps = Map<String, Object?>.from(props);
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
typedProps,
|
||||
allowed: RuntimeProtocolSchema.nodePropsFields,
|
||||
context: 'RuntimeNode.props',
|
||||
);
|
||||
|
||||
return NodeUpdate(id: id, props: typedProps);
|
||||
}
|
||||
}
|
||||
|
||||
class NodeRemove {
|
||||
const NodeRemove({required this.id});
|
||||
|
||||
final String id;
|
||||
|
||||
static NodeRemove fromValue(Object? value) {
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return NodeRemove(id: value);
|
||||
}
|
||||
if (value is Map) {
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
value,
|
||||
allowed: RuntimeProtocolSchema.nodeRemoveFields,
|
||||
context: 'NodeRemove',
|
||||
);
|
||||
final id = value[RuntimeProtocolField.id];
|
||||
if (id is String && id.isNotEmpty) {
|
||||
return NodeRemove(id: id);
|
||||
}
|
||||
}
|
||||
throw const FormatException('NodeRemove must be an id string or {id}');
|
||||
}
|
||||
}
|
||||
|
||||
class NodeDiff {
|
||||
const NodeDiff({
|
||||
this.creates = const [],
|
||||
this.updates = const [],
|
||||
this.removes = const [],
|
||||
});
|
||||
|
||||
final List<RuntimeNode> creates;
|
||||
final List<NodeUpdate> updates;
|
||||
final List<NodeRemove> removes;
|
||||
|
||||
static NodeDiff empty = const NodeDiff();
|
||||
|
||||
static NodeDiff fromMap(Object? value) {
|
||||
if (value == null) {
|
||||
return NodeDiff.empty;
|
||||
}
|
||||
if (value is! Map) {
|
||||
throw const FormatException('NodeDiff must be a map');
|
||||
}
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
value,
|
||||
allowed: RuntimeProtocolSchema.nodeDiffFields,
|
||||
context: 'NodeDiff',
|
||||
);
|
||||
|
||||
return NodeDiff(
|
||||
creates: _readList(
|
||||
value[RuntimeProtocolField.creates],
|
||||
(item) => RuntimeNode.fromMap(Map<String, Object?>.from(item as Map)),
|
||||
),
|
||||
updates: _readList(
|
||||
value[RuntimeProtocolField.updates],
|
||||
(item) => NodeUpdate.fromMap(Map<String, Object?>.from(item as Map)),
|
||||
),
|
||||
removes: _readList(
|
||||
value[RuntimeProtocolField.removes],
|
||||
NodeRemove.fromValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<T> _readList<T>(Object? value, T Function(Object? value) mapper) {
|
||||
if (value == null) {
|
||||
return const [];
|
||||
}
|
||||
if (value is List) {
|
||||
return value.map(mapper).toList(growable: false);
|
||||
}
|
||||
if (value is Map && value.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
if (value is Map && value.keys.every(_isPositiveIntegerKey)) {
|
||||
final entries = value.entries.toList()
|
||||
..sort(
|
||||
(a, b) => int.parse(
|
||||
a.key.toString(),
|
||||
).compareTo(int.parse(b.key.toString())),
|
||||
);
|
||||
return entries
|
||||
.map((entry) => mapper(entry.value))
|
||||
.toList(growable: false);
|
||||
}
|
||||
throw const FormatException('Diff field must be a list');
|
||||
}
|
||||
|
||||
static bool _isPositiveIntegerKey(Object? key) {
|
||||
final value = int.tryParse(key.toString());
|
||||
return value != null && value > 0;
|
||||
}
|
||||
}
|
||||
|
||||
class GameDiff {
|
||||
const GameDiff({
|
||||
required this.render,
|
||||
required this.ui,
|
||||
required this.commands,
|
||||
});
|
||||
|
||||
final NodeDiff render;
|
||||
final NodeDiff ui;
|
||||
final List<RuntimeCommand> commands;
|
||||
|
||||
static const empty = GameDiff(
|
||||
render: NodeDiff(),
|
||||
ui: NodeDiff(),
|
||||
commands: [],
|
||||
);
|
||||
|
||||
static GameDiff fromMap(Map<String, Object?> map) {
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
map,
|
||||
allowed: RuntimeProtocolSchema.gameDiffFields,
|
||||
context: 'GameDiff',
|
||||
);
|
||||
final commandsValue = map[RuntimeProtocolField.commands];
|
||||
final commands = commandsValue == null
|
||||
? const <RuntimeCommand>[]
|
||||
: NodeDiff._readList(
|
||||
commandsValue,
|
||||
(item) =>
|
||||
RuntimeCommand.fromMap(Map<String, Object?>.from(item as Map)),
|
||||
);
|
||||
|
||||
return GameDiff(
|
||||
render: NodeDiff.fromMap(map[RuntimeProtocolField.render]),
|
||||
ui: NodeDiff.fromMap(map[RuntimeProtocolField.ui]),
|
||||
commands: commands,
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/runtime/models/runtime_command.dart
Normal file
43
lib/runtime/models/runtime_command.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import '../protocol/runtime_protocol.dart';
|
||||
|
||||
class RuntimeCommand {
|
||||
const RuntimeCommand({
|
||||
required this.type,
|
||||
this.target,
|
||||
this.payload = const {},
|
||||
});
|
||||
|
||||
final String type;
|
||||
final String? target;
|
||||
final Map<String, Object?> payload;
|
||||
|
||||
static RuntimeCommand fromMap(Map<String, Object?> map) {
|
||||
final type = map[RuntimeProtocolField.type];
|
||||
if (type is! String || type.isEmpty) {
|
||||
throw const FormatException('RuntimeCommand.type must be a string');
|
||||
}
|
||||
|
||||
if (!RuntimeCommandType.isSupported(type)) {
|
||||
throw FormatException('RuntimeCommand.type is unsupported: $type');
|
||||
}
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
map,
|
||||
allowed: RuntimeProtocolSchema.allowedCommandFields(type),
|
||||
context: 'RuntimeCommand.$type',
|
||||
);
|
||||
|
||||
final targetValue = map[RuntimeProtocolField.target];
|
||||
if (targetValue != null && targetValue is! String) {
|
||||
throw const FormatException('RuntimeCommand.target must be a string');
|
||||
}
|
||||
|
||||
final payload = Map<String, Object?>.from(map)
|
||||
..remove(RuntimeProtocolField.type)
|
||||
..remove(RuntimeProtocolField.target);
|
||||
return RuntimeCommand(
|
||||
type: type,
|
||||
target: targetValue as String?,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/runtime/models/runtime_event.dart
Normal file
64
lib/runtime/models/runtime_event.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
class RuntimeEvent {
|
||||
const RuntimeEvent({
|
||||
required this.type,
|
||||
this.target,
|
||||
this.handler,
|
||||
this.x,
|
||||
this.y,
|
||||
this.data = const {},
|
||||
this.sessionId,
|
||||
this.scope,
|
||||
this.targetEpoch,
|
||||
this.scopeEpoch,
|
||||
});
|
||||
|
||||
final String type;
|
||||
final String? target;
|
||||
final String? handler;
|
||||
final double? x;
|
||||
final double? y;
|
||||
final Map<String, Object?> data;
|
||||
|
||||
/// Runtime-internal lifecycle session. Not exposed to Lua.
|
||||
final int? sessionId;
|
||||
|
||||
/// Runtime-internal lifecycle scope. Not exposed to Lua.
|
||||
final String? scope;
|
||||
|
||||
/// Runtime-internal target node epoch. Not exposed to Lua.
|
||||
final int? targetEpoch;
|
||||
|
||||
/// Runtime-internal scope node epoch. Not exposed to Lua.
|
||||
final int? scopeEpoch;
|
||||
|
||||
RuntimeEvent withLifecycle({
|
||||
int? sessionId,
|
||||
String? scope,
|
||||
int? targetEpoch,
|
||||
int? scopeEpoch,
|
||||
}) {
|
||||
return RuntimeEvent(
|
||||
type: type,
|
||||
target: target,
|
||||
handler: handler,
|
||||
x: x,
|
||||
y: y,
|
||||
data: data,
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
scope: scope ?? this.scope,
|
||||
targetEpoch: targetEpoch ?? this.targetEpoch,
|
||||
scopeEpoch: scopeEpoch ?? this.scopeEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
return {
|
||||
'type': type,
|
||||
if (target != null) 'target': target,
|
||||
if (handler != null) 'handler': handler,
|
||||
if (x != null) 'x': x,
|
||||
if (y != null) 'y': y,
|
||||
if (data.isNotEmpty) 'data': data,
|
||||
};
|
||||
}
|
||||
}
|
||||
572
lib/runtime/models/runtime_node.dart
Normal file
572
lib/runtime/models/runtime_node.dart
Normal file
@@ -0,0 +1,572 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../protocol/runtime_protocol.dart';
|
||||
|
||||
class RuntimeNode {
|
||||
const RuntimeNode({
|
||||
required this.id,
|
||||
required this.type,
|
||||
this.parent,
|
||||
this.asset,
|
||||
this.pressedAsset,
|
||||
this.disabledAsset,
|
||||
this.animation,
|
||||
this.skin,
|
||||
this.loop = true,
|
||||
this.text,
|
||||
this.x = 0,
|
||||
this.y = 0,
|
||||
this.width,
|
||||
this.height,
|
||||
this.paddingLeft = 0,
|
||||
this.paddingTop = 0,
|
||||
this.paddingRight = 0,
|
||||
this.paddingBottom = 0,
|
||||
this.anchor = RuntimeAnchorValue.topLeft,
|
||||
this.layer = 0,
|
||||
this.visible = true,
|
||||
this.alpha = 1,
|
||||
this.scale = 1,
|
||||
this.rotation = 0,
|
||||
this.color,
|
||||
this.fontSize,
|
||||
this.textAlign = RuntimeTextAlignValue.center,
|
||||
this.radius,
|
||||
this.strokeWidth,
|
||||
this.value,
|
||||
this.scrollX = 0,
|
||||
this.scrollY = 0,
|
||||
this.contentWidth,
|
||||
this.contentHeight,
|
||||
this.virtualized = false,
|
||||
this.cacheExtent = 0,
|
||||
this.inertia = true,
|
||||
this.scrollbarThumbColor,
|
||||
this.scrollbarTrackColor,
|
||||
this.scrollbarThickness,
|
||||
this.scrollbarVisible = true,
|
||||
this.interactive = false,
|
||||
this.onTap,
|
||||
this.onScroll,
|
||||
this.preset,
|
||||
this.count,
|
||||
this.duration,
|
||||
this.speedMin,
|
||||
this.speedMax,
|
||||
this.gravityX,
|
||||
this.gravityY,
|
||||
this.spread,
|
||||
this.colorTo,
|
||||
this.radiusTo,
|
||||
this.autoRemove = true,
|
||||
this.fadeOut = true,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String? parent;
|
||||
final String? asset;
|
||||
final String? pressedAsset;
|
||||
final String? disabledAsset;
|
||||
final String? animation;
|
||||
final String? skin;
|
||||
final bool loop;
|
||||
final String? text;
|
||||
final double x;
|
||||
final double y;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double paddingLeft;
|
||||
final double paddingTop;
|
||||
final double paddingRight;
|
||||
final double paddingBottom;
|
||||
final String anchor;
|
||||
final int layer;
|
||||
final bool visible;
|
||||
final double alpha;
|
||||
final double scale;
|
||||
final double rotation;
|
||||
final Color? color;
|
||||
final double? fontSize;
|
||||
final String textAlign;
|
||||
final double? radius;
|
||||
final double? strokeWidth;
|
||||
final double? value;
|
||||
final double scrollX;
|
||||
final double scrollY;
|
||||
final double? contentWidth;
|
||||
final double? contentHeight;
|
||||
final bool virtualized;
|
||||
final double cacheExtent;
|
||||
final bool inertia;
|
||||
final Color? scrollbarThumbColor;
|
||||
final Color? scrollbarTrackColor;
|
||||
final double? scrollbarThickness;
|
||||
final bool scrollbarVisible;
|
||||
final bool interactive;
|
||||
final String? onTap;
|
||||
final String? onScroll;
|
||||
final String? preset;
|
||||
final int? count;
|
||||
final double? duration;
|
||||
final double? speedMin;
|
||||
final double? speedMax;
|
||||
final double? gravityX;
|
||||
final double? gravityY;
|
||||
final double? spread;
|
||||
final Color? colorTo;
|
||||
final double? radiusTo;
|
||||
final bool autoRemove;
|
||||
final bool fadeOut;
|
||||
|
||||
RuntimeNode copyWithProps(Map<String, Object?> props) {
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
props,
|
||||
allowed: RuntimeProtocolSchema.nodePropsFields,
|
||||
context: 'RuntimeNode.props',
|
||||
);
|
||||
final nextType = _stringProp(props, RuntimeProtocolField.type) ?? type;
|
||||
if (!RuntimeNodeType.isSupported(nextType)) {
|
||||
throw FormatException('RuntimeNode.type is unsupported: $nextType');
|
||||
}
|
||||
final nextAnchor =
|
||||
_stringProp(props, RuntimeProtocolField.anchor) ?? anchor;
|
||||
if (!RuntimeAnchorValue.isSupported(nextAnchor)) {
|
||||
throw FormatException('RuntimeNode.anchor is unsupported: $nextAnchor');
|
||||
}
|
||||
final nextTextAlign =
|
||||
_stringProp(props, RuntimeProtocolField.textAlign) ?? textAlign;
|
||||
if (!RuntimeTextAlignValue.isSupported(nextTextAlign)) {
|
||||
throw FormatException(
|
||||
'RuntimeNode.textAlign is unsupported: $nextTextAlign',
|
||||
);
|
||||
}
|
||||
|
||||
final nextPreset =
|
||||
_stringProp(props, RuntimeProtocolField.preset) ?? preset;
|
||||
_validateParticlePreset(nextPreset);
|
||||
|
||||
final nextWidth = _doubleProp(props, RuntimeProtocolField.width) ?? width;
|
||||
final nextHeight =
|
||||
_doubleProp(props, RuntimeProtocolField.height) ?? height;
|
||||
final nextContentWidth =
|
||||
_doubleProp(props, RuntimeProtocolField.contentWidth) ?? contentWidth;
|
||||
final nextContentHeight =
|
||||
_doubleProp(props, RuntimeProtocolField.contentHeight) ?? contentHeight;
|
||||
final nextPaddingLeft =
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingLeft) ??
|
||||
paddingLeft;
|
||||
final nextPaddingTop =
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingTop) ??
|
||||
paddingTop;
|
||||
final nextPaddingRight =
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingRight) ??
|
||||
paddingRight;
|
||||
final nextPaddingBottom =
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingBottom) ??
|
||||
paddingBottom;
|
||||
final nextViewportWidth = nextWidth == null
|
||||
? null
|
||||
: (nextWidth - nextPaddingLeft - nextPaddingRight)
|
||||
.clamp(0.0, nextWidth)
|
||||
.toDouble();
|
||||
final nextViewportHeight = nextHeight == null
|
||||
? null
|
||||
: (nextHeight - nextPaddingTop - nextPaddingBottom)
|
||||
.clamp(0.0, nextHeight)
|
||||
.toDouble();
|
||||
final nextScrollX = props.containsKey(RuntimeProtocolField.scrollX)
|
||||
? _scrollProp(
|
||||
props,
|
||||
RuntimeProtocolField.scrollX,
|
||||
contentExtent: nextContentWidth,
|
||||
viewportExtent: nextViewportWidth,
|
||||
)!
|
||||
: _clampScroll(
|
||||
scrollX,
|
||||
contentExtent: nextContentWidth,
|
||||
viewportExtent: nextViewportWidth,
|
||||
);
|
||||
final nextScrollY = props.containsKey(RuntimeProtocolField.scrollY)
|
||||
? _scrollProp(
|
||||
props,
|
||||
RuntimeProtocolField.scrollY,
|
||||
contentExtent: nextContentHeight,
|
||||
viewportExtent: nextViewportHeight,
|
||||
)!
|
||||
: _clampScroll(
|
||||
scrollY,
|
||||
contentExtent: nextContentHeight,
|
||||
viewportExtent: nextViewportHeight,
|
||||
);
|
||||
|
||||
return RuntimeNode(
|
||||
id: id,
|
||||
type: nextType,
|
||||
parent: _parentProp(props, currentParent: parent, nodeId: id),
|
||||
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
|
||||
pressedAsset:
|
||||
_stringProp(props, RuntimeProtocolField.pressedAsset) ?? pressedAsset,
|
||||
disabledAsset:
|
||||
_stringProp(props, RuntimeProtocolField.disabledAsset) ??
|
||||
disabledAsset,
|
||||
animation:
|
||||
_stringProp(props, RuntimeProtocolField.animation) ?? animation,
|
||||
skin: _stringProp(props, RuntimeProtocolField.skin) ?? skin,
|
||||
loop: _boolProp(props, RuntimeProtocolField.loop) ?? loop,
|
||||
text: _stringProp(props, RuntimeProtocolField.text) ?? text,
|
||||
x: _doubleProp(props, RuntimeProtocolField.x) ?? x,
|
||||
y: _doubleProp(props, RuntimeProtocolField.y) ?? y,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
paddingLeft: nextPaddingLeft,
|
||||
paddingTop: nextPaddingTop,
|
||||
paddingRight: nextPaddingRight,
|
||||
paddingBottom: nextPaddingBottom,
|
||||
anchor: nextAnchor,
|
||||
layer: _intProp(props, RuntimeProtocolField.layer) ?? layer,
|
||||
visible: _boolProp(props, RuntimeProtocolField.visible) ?? visible,
|
||||
alpha: _doubleProp(props, RuntimeProtocolField.alpha) ?? alpha,
|
||||
scale: _doubleProp(props, RuntimeProtocolField.scale) ?? scale,
|
||||
rotation: _doubleProp(props, RuntimeProtocolField.rotation) ?? rotation,
|
||||
color: _colorProp(props, RuntimeProtocolField.color) ?? color,
|
||||
fontSize: _doubleProp(props, RuntimeProtocolField.fontSize) ?? fontSize,
|
||||
textAlign: nextTextAlign,
|
||||
radius: _doubleProp(props, RuntimeProtocolField.radius) ?? radius,
|
||||
strokeWidth:
|
||||
_doubleProp(props, RuntimeProtocolField.strokeWidth) ?? strokeWidth,
|
||||
value: _normalizedValueProp(props, RuntimeProtocolField.value) ?? value,
|
||||
scrollX: nextScrollX,
|
||||
scrollY: nextScrollY,
|
||||
contentWidth: nextContentWidth,
|
||||
contentHeight: nextContentHeight,
|
||||
virtualized:
|
||||
_boolProp(props, RuntimeProtocolField.virtualized) ?? virtualized,
|
||||
cacheExtent:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.cacheExtent) ??
|
||||
cacheExtent,
|
||||
inertia: _boolProp(props, RuntimeProtocolField.inertia) ?? inertia,
|
||||
scrollbarThumbColor:
|
||||
_colorProp(props, RuntimeProtocolField.scrollbarThumbColor) ??
|
||||
scrollbarThumbColor,
|
||||
scrollbarTrackColor:
|
||||
_colorProp(props, RuntimeProtocolField.scrollbarTrackColor) ??
|
||||
scrollbarTrackColor,
|
||||
scrollbarThickness:
|
||||
_nonNegativeDoubleProp(
|
||||
props,
|
||||
RuntimeProtocolField.scrollbarThickness,
|
||||
) ??
|
||||
scrollbarThickness,
|
||||
scrollbarVisible:
|
||||
_boolProp(props, RuntimeProtocolField.scrollbarVisible) ??
|
||||
scrollbarVisible,
|
||||
interactive:
|
||||
_boolProp(props, RuntimeProtocolField.interactive) ?? interactive,
|
||||
onTap: _stringProp(props, RuntimeProtocolField.onTap) ?? onTap,
|
||||
onScroll: _stringProp(props, RuntimeProtocolField.onScroll) ?? onScroll,
|
||||
preset: nextPreset,
|
||||
count: _positiveIntProp(props, RuntimeProtocolField.count) ?? count,
|
||||
duration:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.duration) ??
|
||||
duration,
|
||||
speedMin:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.speedMin) ??
|
||||
speedMin,
|
||||
speedMax:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.speedMax) ??
|
||||
speedMax,
|
||||
gravityX: _doubleProp(props, RuntimeProtocolField.gravityX) ?? gravityX,
|
||||
gravityY: _doubleProp(props, RuntimeProtocolField.gravityY) ?? gravityY,
|
||||
spread:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.spread) ?? spread,
|
||||
colorTo: _colorProp(props, RuntimeProtocolField.colorTo) ?? colorTo,
|
||||
radiusTo:
|
||||
_nonNegativeDoubleProp(props, RuntimeProtocolField.radiusTo) ??
|
||||
radiusTo,
|
||||
autoRemove:
|
||||
_boolProp(props, RuntimeProtocolField.autoRemove) ?? autoRemove,
|
||||
fadeOut: _boolProp(props, RuntimeProtocolField.fadeOut) ?? fadeOut,
|
||||
);
|
||||
}
|
||||
|
||||
static RuntimeNode fromMap(Map<String, Object?> map) {
|
||||
RuntimeProtocolSchema.ensureKnownKeys(
|
||||
map,
|
||||
allowed: RuntimeProtocolSchema.nodeFields,
|
||||
context: 'RuntimeNode',
|
||||
);
|
||||
final type = _requiredString(map, RuntimeProtocolField.type);
|
||||
if (!RuntimeNodeType.isSupported(type)) {
|
||||
throw FormatException('RuntimeNode.type is unsupported: $type');
|
||||
}
|
||||
final anchor =
|
||||
_stringProp(map, RuntimeProtocolField.anchor) ??
|
||||
RuntimeAnchorValue.topLeft;
|
||||
if (!RuntimeAnchorValue.isSupported(anchor)) {
|
||||
throw FormatException('RuntimeNode.anchor is unsupported: $anchor');
|
||||
}
|
||||
final textAlign =
|
||||
_stringProp(map, RuntimeProtocolField.textAlign) ??
|
||||
RuntimeTextAlignValue.center;
|
||||
if (!RuntimeTextAlignValue.isSupported(textAlign)) {
|
||||
throw FormatException('RuntimeNode.textAlign is unsupported: $textAlign');
|
||||
}
|
||||
|
||||
final preset = _stringProp(map, RuntimeProtocolField.preset);
|
||||
_validateParticlePreset(preset);
|
||||
|
||||
return RuntimeNode(
|
||||
id: _requiredString(map, RuntimeProtocolField.id),
|
||||
type: type,
|
||||
parent: _parentProp(
|
||||
map,
|
||||
currentParent: null,
|
||||
nodeId: _requiredString(map, RuntimeProtocolField.id),
|
||||
),
|
||||
asset: _stringProp(map, RuntimeProtocolField.asset),
|
||||
pressedAsset: _stringProp(map, RuntimeProtocolField.pressedAsset),
|
||||
disabledAsset: _stringProp(map, RuntimeProtocolField.disabledAsset),
|
||||
animation: _stringProp(map, RuntimeProtocolField.animation),
|
||||
skin: _stringProp(map, RuntimeProtocolField.skin),
|
||||
loop: _boolProp(map, RuntimeProtocolField.loop) ?? true,
|
||||
text: _stringProp(map, RuntimeProtocolField.text),
|
||||
x: _doubleProp(map, RuntimeProtocolField.x) ?? 0,
|
||||
y: _doubleProp(map, RuntimeProtocolField.y) ?? 0,
|
||||
width: _doubleProp(map, RuntimeProtocolField.width),
|
||||
height: _doubleProp(map, RuntimeProtocolField.height),
|
||||
paddingLeft:
|
||||
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingLeft) ?? 0,
|
||||
paddingTop:
|
||||
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingTop) ?? 0,
|
||||
paddingRight:
|
||||
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingRight) ?? 0,
|
||||
paddingBottom:
|
||||
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingBottom) ?? 0,
|
||||
anchor: anchor,
|
||||
layer: _intProp(map, RuntimeProtocolField.layer) ?? 0,
|
||||
visible: _boolProp(map, RuntimeProtocolField.visible) ?? true,
|
||||
alpha: _doubleProp(map, RuntimeProtocolField.alpha) ?? 1,
|
||||
scale: _doubleProp(map, RuntimeProtocolField.scale) ?? 1,
|
||||
rotation: _doubleProp(map, RuntimeProtocolField.rotation) ?? 0,
|
||||
color: _colorProp(map, RuntimeProtocolField.color),
|
||||
fontSize: _doubleProp(map, RuntimeProtocolField.fontSize),
|
||||
textAlign: textAlign,
|
||||
radius: _doubleProp(map, RuntimeProtocolField.radius),
|
||||
strokeWidth: _doubleProp(map, RuntimeProtocolField.strokeWidth),
|
||||
value: _normalizedValueProp(map, RuntimeProtocolField.value),
|
||||
scrollX:
|
||||
_scrollProp(
|
||||
map,
|
||||
RuntimeProtocolField.scrollX,
|
||||
contentExtent: _doubleProp(map, RuntimeProtocolField.contentWidth),
|
||||
viewportExtent: _doubleProp(map, RuntimeProtocolField.width),
|
||||
) ??
|
||||
0,
|
||||
scrollY:
|
||||
_scrollProp(
|
||||
map,
|
||||
RuntimeProtocolField.scrollY,
|
||||
contentExtent: _doubleProp(map, RuntimeProtocolField.contentHeight),
|
||||
viewportExtent: _doubleProp(map, RuntimeProtocolField.height),
|
||||
) ??
|
||||
0,
|
||||
contentWidth: _doubleProp(map, RuntimeProtocolField.contentWidth),
|
||||
contentHeight: _doubleProp(map, RuntimeProtocolField.contentHeight),
|
||||
virtualized: _boolProp(map, RuntimeProtocolField.virtualized) ?? false,
|
||||
cacheExtent:
|
||||
_nonNegativeDoubleProp(map, RuntimeProtocolField.cacheExtent) ?? 0,
|
||||
inertia: _boolProp(map, RuntimeProtocolField.inertia) ?? true,
|
||||
scrollbarThumbColor: _colorProp(
|
||||
map,
|
||||
RuntimeProtocolField.scrollbarThumbColor,
|
||||
),
|
||||
scrollbarTrackColor: _colorProp(
|
||||
map,
|
||||
RuntimeProtocolField.scrollbarTrackColor,
|
||||
),
|
||||
scrollbarThickness: _nonNegativeDoubleProp(
|
||||
map,
|
||||
RuntimeProtocolField.scrollbarThickness,
|
||||
),
|
||||
scrollbarVisible:
|
||||
_boolProp(map, RuntimeProtocolField.scrollbarVisible) ?? true,
|
||||
interactive: _boolProp(map, RuntimeProtocolField.interactive) ?? false,
|
||||
onTap: _stringProp(map, RuntimeProtocolField.onTap),
|
||||
onScroll: _stringProp(map, RuntimeProtocolField.onScroll),
|
||||
preset: preset,
|
||||
count: _positiveIntProp(map, RuntimeProtocolField.count),
|
||||
duration: _nonNegativeDoubleProp(map, RuntimeProtocolField.duration),
|
||||
speedMin: _nonNegativeDoubleProp(map, RuntimeProtocolField.speedMin),
|
||||
speedMax: _nonNegativeDoubleProp(map, RuntimeProtocolField.speedMax),
|
||||
gravityX: _doubleProp(map, RuntimeProtocolField.gravityX),
|
||||
gravityY: _doubleProp(map, RuntimeProtocolField.gravityY),
|
||||
spread: _nonNegativeDoubleProp(map, RuntimeProtocolField.spread),
|
||||
colorTo: _colorProp(map, RuntimeProtocolField.colorTo),
|
||||
radiusTo: _nonNegativeDoubleProp(map, RuntimeProtocolField.radiusTo),
|
||||
autoRemove: _boolProp(map, RuntimeProtocolField.autoRemove) ?? true,
|
||||
fadeOut: _boolProp(map, RuntimeProtocolField.fadeOut) ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
static String _requiredString(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be a non-empty string');
|
||||
}
|
||||
|
||||
static void _validateParticlePreset(String? preset) {
|
||||
if (preset != null && !RuntimeParticlePresetValue.isSupported(preset)) {
|
||||
throw FormatException('RuntimeNode.preset is unsupported: $preset');
|
||||
}
|
||||
}
|
||||
|
||||
static String? _stringProp(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is String) {
|
||||
return value;
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be a string');
|
||||
}
|
||||
|
||||
static String? _parentProp(
|
||||
Map<String, Object?> map, {
|
||||
required String? currentParent,
|
||||
required String nodeId,
|
||||
}) {
|
||||
if (!map.containsKey(RuntimeProtocolField.parent)) {
|
||||
return currentParent;
|
||||
}
|
||||
|
||||
final value = _stringProp(map, RuntimeProtocolField.parent);
|
||||
if (value == null || value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (value == nodeId) {
|
||||
throw const FormatException('RuntimeNode.parent cannot reference itself');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool? _boolProp(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be a boolean');
|
||||
}
|
||||
|
||||
static double? _doubleProp(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toDouble();
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be a number');
|
||||
}
|
||||
|
||||
static double? _normalizedValueProp(Map<String, Object?> map, String key) {
|
||||
final value = _doubleProp(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value < 0 || value > 1) {
|
||||
throw FormatException('RuntimeNode.$key must be between 0 and 1');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static double? _nonNegativeDoubleProp(Map<String, Object?> map, String key) {
|
||||
final value = _doubleProp(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value < 0) {
|
||||
throw FormatException('RuntimeNode.$key must be >= 0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static double? _scrollProp(
|
||||
Map<String, Object?> map,
|
||||
String key, {
|
||||
required double? contentExtent,
|
||||
required double? viewportExtent,
|
||||
}) {
|
||||
final value = _doubleProp(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value < 0) {
|
||||
throw FormatException('RuntimeNode.$key must be >= 0');
|
||||
}
|
||||
return _clampScroll(
|
||||
value,
|
||||
contentExtent: contentExtent,
|
||||
viewportExtent: viewportExtent,
|
||||
);
|
||||
}
|
||||
|
||||
static double _clampScroll(
|
||||
double value, {
|
||||
required double? contentExtent,
|
||||
required double? viewportExtent,
|
||||
}) {
|
||||
final maxScroll = (contentExtent ?? 0) - (viewportExtent ?? 0);
|
||||
if (maxScroll <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return value.clamp(0, maxScroll).toDouble();
|
||||
}
|
||||
|
||||
static int? _positiveIntProp(Map<String, Object?> map, String key) {
|
||||
final value = _intProp(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value <= 0) {
|
||||
throw FormatException('RuntimeNode.$key must be > 0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int? _intProp(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be an integer');
|
||||
}
|
||||
|
||||
static Color? _colorProp(Map<String, Object?> map, String key) {
|
||||
final value = map[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is! String || !value.startsWith('#')) {
|
||||
throw FormatException('RuntimeNode.$key must be a hex color');
|
||||
}
|
||||
|
||||
final hex = value.substring(1);
|
||||
if (hex.length == 6) {
|
||||
return Color(int.parse('ff$hex', radix: 16));
|
||||
}
|
||||
if (hex.length == 8) {
|
||||
return Color(int.parse(hex, radix: 16));
|
||||
}
|
||||
throw FormatException('RuntimeNode.$key must be #RRGGBB or #AARRGGBB');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user