add method
Decodes and instantiates entity's asset if it names a model; null
and no effect otherwise, PropVisuals.add's own "not every entity is
mine" contract. Awaited by the caller, not fired and forgotten the way
FixtureVisuals._addModel is — a lesson's steps read a node's rest
position the moment the level opens, so the model has to already be in
the scene by the time LevelReady/LessonReady is emitted, not two
frames later.
Implementation
Future<SceneNode?> add(EntityDef entity) async {
if (entity.type != entityType) return null;
final path = entity.string('asset');
if (path == null) {
onIssue(Issue('$entityType "${entity.name ?? '?'}" names no asset'));
return null;
}
final generation = _generation;
final ModelAsset asset;
try {
// Whichever kind of path the level wrote: an `assets_src/` source goes
// to the file the build hook converted it into, anything else is read
// as it stands — see [loadModelByPath].
final document = await loadModelByPath(path, bundleSource: _source);
asset = await ModelAsset.fromDocument(
document,
device: device,
name: path,
);
} catch (error) {
onIssue(
Issue(
'$entityType "${entity.name ?? '?'}" could not load "$path": $error',
),
);
return null;
}
// [dispose] ran while the file was being read: the upload is released
// here, since no list [dispose] walks will ever hold it, and nothing goes
// into a scene nobody draws any more.
if (generation != _generation) {
asset.release(device);
return null;
}
_assets.add(asset);
final instance = asset.instantiate(scene, name: entity.name);
instance.root.setPositionFrom(entity.position);
instance.root.setRotationYawPitchRoll(entity.yaw, 0.0, 0.0);
_instances.add(instance);
final entityName = entity.name;
if (entityName != null) {
_nodes[entityName] = instance.root;
for (var i = 0; i < asset.nodes.length; i++) {
final nodeName = asset.nodes[i].name;
if (nodeName != null) {
_nodes['$entityName#$nodeName'] = instance.nodes[i];
}
}
}
return instance.root;
}