diff --git a/lib/classes/gsf/header2/chunks/bone.dart b/lib/classes/gsf/header2/chunks/bone.dart index c3c5ede..794a22f 100644 --- a/lib/classes/gsf/header2/chunks/bone.dart +++ b/lib/classes/gsf/header2/chunks/bone.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:paraworld_gsf_viewer/classes/gsf/header2/affine_matrix.dart'; import 'package:paraworld_gsf_viewer/classes/gsf_data.dart'; +import 'package:vector_math/vector_math.dart'; class Bone extends GsfPart { late final Standard4BytesData guid; @@ -22,6 +23,17 @@ class Bone extends GsfPart { AffineTransformation? bindPose; + Vector3 get translation => Vector3(posX.value, posY.value, posZ.value); + Quaternion get rotation => Quaternion( + quaternionX.value, + quaternionY.value, + quaternionZ.value, + quaternionW.value, + ); + Vector3 get scale => Vector3(scaleX.value, scaleY.value, scaleZ.value); + + Matrix4 get localTransform => Matrix4.compose(translation, rotation, scale); + @override String get label => 'Bone 0x${guid.value.toRadixString(16)}'; diff --git a/lib/classes/gsf/header2/chunks/mesh.dart b/lib/classes/gsf/header2/chunks/mesh.dart index 5a7cc9c..8b12d54 100644 --- a/lib/classes/gsf/header2/chunks/mesh.dart +++ b/lib/classes/gsf/header2/chunks/mesh.dart @@ -86,7 +86,7 @@ mixin MeshToModelInterface on Chunk { ], cloth: [], boundingBox: globalBB, - skeleton: [], + skeletons: [], ); } } diff --git a/lib/classes/gsf/header2/chunks/skeleton.dart b/lib/classes/gsf/header2/chunks/skeleton.dart index 398e64e..c7e1f4e 100644 --- a/lib/classes/gsf/header2/chunks/skeleton.dart +++ b/lib/classes/gsf/header2/chunks/skeleton.dart @@ -96,7 +96,6 @@ class SkeletonChunk extends Chunk { } _getChildOfBone(boneTree, bones, nextBone); } - print(boneTree); bindPoses = []; for (var i = 0; i < allBonesCount.value; i++) { final bindPose = AffineTransformation.fromBytes( @@ -179,33 +178,17 @@ class SkeletonChunk extends Chunk { } _createJointsBranch( - List jointsBranch, - Map treatedBones, + List jointVertices, List boneIds, Matrix4 parentWorldTransform, ) { for (final boneId in boneIds) { final data = boneTree[boneId]!; final bone = data.bone; - treatedBones[bone.guid.value] = true; - final translation = Vector3( - bone.posX.value, - bone.posY.value, - bone.posZ.value, - ); - - final Quaternion rotation = Quaternion( - bone.quaternionX.value, - bone.quaternionY.value, - bone.quaternionZ.value, - bone.quaternionW.value, - ); - final scale = - Vector3(bone.scaleX.value, bone.scaleY.value, bone.scaleZ.value); - Matrix4 localTransform = Matrix4.compose(translation, rotation, scale); + Matrix4 localTransform = bone.localTransform; localTransform *= bone.bindPose!.matrix; final Matrix4 worldTranform = parentWorldTransform * localTransform; - jointsBranch.add( + jointVertices.add( ModelVertex( worldTranform.getTranslation(), box: BoundingBoxModel.zero(), @@ -215,47 +198,31 @@ class SkeletonChunk extends Chunk { ); _createJointsBranch( - jointsBranch, - treatedBones, + jointVertices, data.children, worldTranform, ); } } - List> toModelVertices() { - final List> vertices = []; - final Map treatedBones = {}; - for (final bone in bones) { - final branch = []; - if (treatedBones.containsKey(bone.guid.value)) { - continue; - } - treatedBones[bone.guid.value] = true; - var translation = - Vector3(bone.posX.value, bone.posY.value, bone.posZ.value); - final scale = - Vector3(bone.scaleX.value, bone.scaleY.value, bone.scaleZ.value); - final Quaternion rotation = Quaternion( - bone.quaternionX.value, - bone.quaternionY.value, - bone.quaternionZ.value, - bone.quaternionW.value, - ); - Matrix4 localTransform = Matrix4.compose(translation, rotation, scale); - localTransform = localTransform * bone.bindPose!.matrix; + List toModelVertices() { + final List vertices = []; + final rootBone = bones.first; - branch.add( - ModelVertex( - localTransform.getTranslation(), - box: BoundingBoxModel.zero(), - positionOffset: Vector3.zero(), - ), - ); - _createJointsBranch(branch, treatedBones, boneTree[bone.guid.value]!.children, - localTransform); - vertices.add(branch); - } + Matrix4 localTransform = rootBone.localTransform; + localTransform = localTransform * rootBone.bindPose!.matrix; + + vertices.add( + ModelVertex( + localTransform.getTranslation(), + box: BoundingBoxModel.zero(), + positionOffset: Vector3.zero(), + ), + ); + _createJointsBranch( + vertices, boneTree[rootBone.guid.value]!.children, localTransform); + assert(vertices.length == allBonesCount.value, + "There is more than one root bone in skeleton"); return vertices; } } diff --git a/lib/classes/gsf/header2/model_settings.dart b/lib/classes/gsf/header2/model_settings.dart index 93b3984..cf2f404 100644 --- a/lib/classes/gsf/header2/model_settings.dart +++ b/lib/classes/gsf/header2/model_settings.dart @@ -239,12 +239,12 @@ class ModelSettings extends GsfPart { ) { final List meshes = []; final List cloths = []; - List> skeleton = []; + List> skeletons = []; final List materialIndices = fallbackTable?.usedMaterialIndexes.map((e) => e.value).toList() ?? []; for (final chunk in chunksTable?.chunks ?? []) { if (chunk.type == ChunkType.skeleton) { - skeleton = (chunk as SkeletonChunk).toModelVertices(); + skeletons.add((chunk as SkeletonChunk).toModelVertices()); } else if (chunk.type.isMeshLike()) { meshes.add((chunk as MeshChunk).toModelMesh( ChunkAttributes.fromValue(type, chunk.attributes.value), @@ -263,13 +263,17 @@ class ModelSettings extends GsfPart { )); } } + assert( + skeletons.length == skeletonChunksCount.value, + 'Skeletons count does not match', + ); return Model( name: objectName.label, type: type, meshes: meshes, cloth: cloths, boundingBox: boundingBox.toModelBox(), - skeleton: skeleton, + skeletons: skeletons, ); } diff --git a/lib/classes/model.dart b/lib/classes/model.dart index 9fcdf14..7d88794 100644 --- a/lib/classes/model.dart +++ b/lib/classes/model.dart @@ -28,7 +28,7 @@ class Model { required this.meshes, required this.cloth, required this.boundingBox, - required this.skeleton, + required this.skeletons, }); final ModelType type; @@ -36,7 +36,7 @@ class Model { final List meshes; final List cloth; final BoundingBoxModel boundingBox; - final List> skeleton; + final List> skeletons; // todo skeleton // todo position links @@ -238,14 +238,14 @@ class Model { : (_paint..color = meshColor.withOpacity(0.3)), ); - if (showSkeleton && skeleton.isNotEmpty) { - final List points = []; + if (showSkeleton && skeletons.isNotEmpty) { final skeletonPaint = Paint() ..color = meshColor ..strokeWidth = 5 ..strokeCap = StrokeCap.round; - for (final branch in skeleton) { - for (final joint in branch) { + for (final skeleton in skeletons) { + final List points = []; + for (final joint in skeleton) { final coords = joint.project( widthOffset: projectionData.widthOffset, heightOffset: projectionData.heightOffset, @@ -255,15 +255,15 @@ class Model { ); points.addAll([coords.pointProjection.x, coords.pointProjection.y]); + final pos = Float32List.fromList(points); + canvas.drawRawPoints(ui.PointMode.polygon, pos, skeletonPaint); + canvas.drawRawPoints( + ui.PointMode.points, + pos, + Paint() + ..color = Colors.pink + ..strokeWidth = 8); } - final pos = Float32List.fromList(points); - canvas.drawRawPoints(ui.PointMode.polygon, pos, skeletonPaint); - canvas.drawRawPoints( - ui.PointMode.points, - pos, - Paint() - ..color = Colors.pink - ..strokeWidth = 8); } } } diff --git a/lib/widgets/header2/display.dart b/lib/widgets/header2/display.dart index 34d4212..5ee9892 100644 --- a/lib/widgets/header2/display.dart +++ b/lib/widgets/header2/display.dart @@ -212,7 +212,15 @@ List getChunkWidgetByType( ], withSkeleton: (data) => [ SkeletonDisplay(skeleton: data.skeleton), - if (data.bone != null) BoneDisplay(bone: data.bone!), + Flexible( + child: Column( + children: [ + BoneTreeDisplay( + boneTree: data.skeleton.boneTree, bones: data.skeleton.bones), + if (data.bone != null) BoneDisplay(bone: data.bone!) + ], + ), + ), ], withLink: (data) => [LinkDisplay(link: data.linkChunk)], orElse: () => [const SizedBox.shrink()], diff --git a/lib/widgets/header2/widgets/chunks/skeleton.dart b/lib/widgets/header2/widgets/chunks/skeleton.dart index 48ccb9c..15be774 100644 --- a/lib/widgets/header2/widgets/chunks/skeleton.dart +++ b/lib/widgets/header2/widgets/chunks/skeleton.dart @@ -1,12 +1,13 @@ +import 'package:animated_tree_view/animated_tree_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:paraworld_gsf_viewer/classes/gsf/header2/chunks/bind_pose.dart'; import 'package:paraworld_gsf_viewer/classes/gsf/header2/chunks/bone.dart'; import 'package:paraworld_gsf_viewer/classes/gsf/header2/chunks/skeleton.dart'; import 'package:paraworld_gsf_viewer/widgets/header2/providers.dart'; import 'package:paraworld_gsf_viewer/widgets/header2/widgets/affine_transformation.dart'; import 'package:paraworld_gsf_viewer/widgets/header2/widgets/chunks/attributes/chunk_attributes.dart'; import 'package:paraworld_gsf_viewer/widgets/utils/data_display.dart'; +import 'package:paraworld_gsf_viewer/widgets/utils/label.dart'; class SkeletonDisplay extends ConsumerWidget { const SkeletonDisplay({super.key, required this.skeleton}); @@ -73,37 +74,88 @@ class BoneDisplay extends StatelessWidget { } } -class _BindPoseDisplay extends StatelessWidget { - const _BindPoseDisplay({ +class BoneTreeDisplay extends ConsumerStatefulWidget { + const BoneTreeDisplay({ super.key, - required this.bindPose, + required this.boneTree, + required this.bones, }); - final BindPose bindPose; + final BoneTree boneTree; + final List bones; + + @override + ConsumerState createState() => _BoneTreeDisplayState(); +} + +class _BoneTreeDisplayState extends ConsumerState { + late final TreeNode _computedBoneTree = TreeNode.root( + data: widget.bones.first, + ); + + createBranchFromBone(Bone bone, TreeNode node) { + final children = widget.boneTree[bone.guid.value]!.children; + for (final child in children) { + final chilBone = widget.boneTree[child]!.bone; + final childNode = TreeNode( + key: child.toString(), + data: widget.boneTree[child]!.bone, + ); + node.add(childNode); + createBranchFromBone(chilBone, childNode); + } + } + + @override + void initState() { + createBranchFromBone(widget.bones.first, _computedBoneTree); + super.initState(); + } @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GsfDataTile(label: "a1_1", data: bindPose.float1To1), - GsfDataTile(label: "a1_2", data: bindPose.float1To2), - GsfDataTile(label: "a1_3", data: bindPose.float1To3), - GsfDataTile(label: "a1_4", data: bindPose.float1To4), - GsfDataTile(label: "a2_1", data: bindPose.float2To1), - GsfDataTile(label: "a2_2", data: bindPose.float2To2), - GsfDataTile(label: "a2_3", data: bindPose.float2To3), - GsfDataTile(label: "a2_4", data: bindPose.float2To4), - GsfDataTile(label: "a3_1", data: bindPose.float3To1), - GsfDataTile(label: "a3_2", data: bindPose.float3To2), - GsfDataTile(label: "a3_3", data: bindPose.float3To3), - GsfDataTile(label: "a3_4", data: bindPose.float3To4), - GsfDataTile(label: "a4_1", data: bindPose.float4To1), - GsfDataTile(label: "a4_2", data: bindPose.float4To2), - GsfDataTile(label: "a4_3", data: bindPose.float4To3), - GsfDataTile(label: "a4_4", data: bindPose.float4To4), - ], + final theme = Theme.of(context); + final selectedBone = ref.watch(header2StateNotifierProvider).mapOrNull( + withModelSettings: (state) => state.selectedChunkState?.mapOrNull( + withSkeleton: (skeleton) => skeleton.bone, + ), + ); + return Flexible( + child: ListViewWrapper( + rightPadding: 0, + maxHeight: double.infinity, + child: TreeView.simpleTyped>( + shrinkWrap: true, + onTreeReady: (controller) { + controller.expandAllChildren(_computedBoneTree); + }, + builder: (context, node) { + return ListTile( + dense: true, + visualDensity: const VisualDensity( + horizontal: 0, + vertical: VisualDensity.minimumDensity, + ), + shape: Border( + top: BorderSide(color: theme.colorScheme.onBackground), + left: BorderSide(color: theme.colorScheme.onBackground), + ), + selected: selectedBone != null && + selectedBone.guid.value == node.data?.guid.value, + selectedTileColor: theme.colorScheme.secondary, + title: Label.regular(node.data?.label ?? ""), + onTap: () { + if (node.data != null) { + ref + .read(header2StateNotifierProvider.notifier) + .setBone(node.data!); + } + }, + ); + }, + tree: _computedBoneTree, + ), + ), ); } } diff --git a/lib/widgets/header2/widgets/chunks/submesh.dart b/lib/widgets/header2/widgets/chunks/submesh.dart index 7d685f1..56d4476 100644 --- a/lib/widgets/header2/widgets/chunks/submesh.dart +++ b/lib/widgets/header2/widgets/chunks/submesh.dart @@ -43,7 +43,7 @@ class SubmeshDisplay extends StatelessWidget { ], cloth: [], boundingBox: modelData.box, - skeleton: [], + skeletons: [], ); return Flexible( child: Column( diff --git a/pubspec.lock b/pubspec.lock index f026a33..6ab775e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.3" + animated_tree_view: + dependency: "direct main" + description: + name: animated_tree_view + sha256: "577a59e2c9dfca433b7c3bb4e516a3834ea11edb49827f7cfb19ec72dcccf991" + url: "https://pub.dev" + source: hosted + version: "2.2.0" archive: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -193,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + diffutil_dart: + dependency: transitive + description: + name: diffutil_dart + sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81" + url: "https://pub.dev" + source: hosted + version: "4.0.1" equatable: dependency: "direct main" description: @@ -229,10 +253,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "8.0.3" fixnum: dependency: transitive description: @@ -250,10 +274,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -405,10 +429,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" logging: dependency: transitive description: @@ -601,6 +625,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" shelf: dependency: transitive description: @@ -638,6 +670,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -710,6 +750,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" vector_math: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d8b5816..872e486 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: vector_math: ^2.1.4 equatable: ^2.0.5 path_provider: ^2.1.2 - file_picker: ^6.1.1 + file_picker: ^8.0.3 flutter_riverpod: ^2.4.10 freezed_annotation: ^2.4.1 gap: ^3.0.1 @@ -46,6 +46,7 @@ dependencies: git: url: http://github.com/arceusVen1/image-with-dds ref: efef5ae3 + animated_tree_view: ^2.2.0 dev_dependencies: flutter_test: sdk: flutter @@ -55,7 +56,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.2 riverpod_generator: ^2.3.10 build_runner: ^2.4.8 freezed: ^2.4.7