@@ -506,96 +506,161 @@ function serializeTexture(texture: Texture): PluginTexture {
506506 } satisfies PluginTexture )
507507}
508508
509- function serializeAnimation ( options : {
509+ function buildLoopMode ( animation : IRenderedAnimation ) : LoopMode {
510+ switch ( animation . loop_mode ) {
511+ case 'loop' :
512+ return { type : 'loop' , loop_delay : String ( animation . loop_delay ?? 0 ) }
513+ case 'hold' :
514+ return { type : 'hold' }
515+ default :
516+ return { type : 'once' }
517+ }
518+ }
519+
520+ function pad3 < T > ( arr : ArrayLike < T > , fill : T ) : [ T , T , T ] {
521+ return [ arr [ 0 ] ?? fill , arr [ 1 ] ?? fill , arr [ 2 ] ?? fill ]
522+ }
523+
524+ function keyframeInterpolation ( kf : _Keyframe ) : TransformationKeyframeInterpolation {
525+ switch ( kf . interpolation ) {
526+ case 'bezier' :
527+ return {
528+ type : 'bezier' ,
529+ left_handle_time : pad3 ( kf . bezier_left_time , 0 ) ,
530+ left_handle_value : pad3 ( kf . bezier_left_value , 0 ) ,
531+ right_handle_time : pad3 ( kf . bezier_right_time , 0 ) ,
532+ right_handle_value : pad3 ( kf . bezier_right_value , 0 ) ,
533+ }
534+ case 'catmullrom' :
535+ return { type : 'catmullrom' }
536+ case 'step' :
537+ return { type : 'step' }
538+ default : {
539+ const out : TransformationKeyframeInterpolation = {
540+ type : 'linear' ,
541+ easing : kf . easing ?? 'linear' ,
542+ }
543+ if ( kf . easingArgs ?. length ) out . easing_arguments = kf . easingArgs . slice ( )
544+ return out
545+ }
546+ }
547+ }
548+
549+ function keyframeDataPoint ( kf : _Keyframe , index : number ) : [ string , string , string ] {
550+ return [
551+ String ( kf . get ( 'x' , index ) ) ,
552+ String ( kf . get ( 'y' , index ) ) ,
553+ String ( kf . get ( 'z' , index ) ) ,
554+ ]
555+ }
556+
557+ function serializeRawAnimation ( options : {
510558 animation : IRenderedAnimation
511559 nodeUuidToId : Map < string , string >
512560 paletteIds : string [ ]
513561} ) : PluginAnimation {
514- const { animation, nodeUuidToId } = options
562+ const { animation, nodeUuidToId, paletteIds } = options
563+ const bbAnimation = Blockbench . Animation . all . find ( a => a . uuid === animation . uuid )
564+ if ( ! bbAnimation ) {
565+ throw new IntentionalExportError (
566+ `Could not locate source animation for <code>${ animation . name } </code>.`
567+ )
568+ }
515569
516570 // eslint-disable-next-line @typescript-eslint/naming-convention
517- const loop_mode : LoopMode =
518- animation . loop_mode === 'loop'
519- ? { type : 'loop' , loop_delay : String ( animation . loop_delay ?? 0 ) }
520- : animation . loop_mode === 'hold'
521- ? { type : 'hold' }
522- : { type : 'once' }
571+ const node_keyframes : NonNullable < PluginAnimation [ 'node_keyframes' ] > = { }
572+ // eslint-disable-next-line @typescript-eslint/naming-convention
573+ let global_keyframes : PluginAnimation [ 'global_keyframes' ]
574+
575+ for ( const [ animatorUuid , animator ] of Object . entries ( bbAnimation . animators ) ) {
576+ // @ts -expect-error - broken bb types
577+ const keyframes : _Keyframe [ ] | undefined = animator ?. keyframes
578+ if ( ! keyframes ?. length ) continue
579+
580+ const nodeId = nodeUuidToId . get ( animatorUuid )
581+
582+ for ( const kf of keyframes ) {
583+ const timeKey = formatTimestamp ( kf . time )
584+
585+ if ( kf . channel === 'position' || kf . channel === 'rotation' || kf . channel === 'scale' ) {
586+ if ( ! nodeId ) continue
587+ const channels = ( node_keyframes [ nodeId ] ??= { } )
588+ const bucket = ( channels [ kf . channel ] ??= { } )
589+ const entry : TransformationKeyframe = {
590+ value : keyframeDataPoint ( kf , 0 ) ,
591+ interpolation : keyframeInterpolation ( kf ) ,
592+ }
593+ if ( kf . data_points . length === 2 ) entry . post = keyframeDataPoint ( kf , 1 )
594+ bucket [ timeKey ] = entry
595+ } else if ( kf . channel === 'variant' && paletteIds . length && kf . variant ) {
596+ global_keyframes ??= { }
597+ const texture = ( global_keyframes . texture ??= { } )
598+ const slot = ( texture [ timeKey ] ??= { } )
599+ for ( const paletteId of paletteIds ) slot [ paletteId ] = kf . variant . name
600+ }
601+ }
602+ }
523603
524- const maxTime = animation . frames . at ( - 1 ) ?. time ?? 0
604+ return scrubUndefined ( {
605+ loop_mode : buildLoopMode ( animation ) ,
606+ blend_weight : '1' ,
607+ start_delay : '0' ,
608+ length : bbAnimation . length ,
609+ global_keyframes,
610+ node_keyframes,
611+ } satisfies PluginAnimation )
612+ }
613+
614+ function serializeBakedAnimation ( options : {
615+ animation : IRenderedAnimation
616+ nodeUuidToId : Map < string , string >
617+ paletteIds : string [ ]
618+ } ) : PluginAnimation {
619+ const { animation, nodeUuidToId, paletteIds } = options
525620
526621 // eslint-disable-next-line @typescript-eslint/naming-convention
527622 const node_keyframes : NonNullable < PluginAnimation [ 'node_keyframes' ] > = { }
623+ // eslint-disable-next-line @typescript-eslint/naming-convention
624+ let global_keyframes : PluginAnimation [ 'global_keyframes' ]
528625
529626 for ( const frame of animation . frames ) {
530627 const timeKey = formatTimestamp ( frame . time )
628+
531629 for ( const [ uuid , transform ] of Object . entries ( frame . node_transforms ) ) {
532630 const nodeId = nodeUuidToId . get ( uuid )
533631 if ( ! nodeId ) continue
534- node_keyframes [ nodeId ] ??= { }
535632
536- const createInterpolation = ( ) : TransformationKeyframeInterpolation =>
633+ const interpolation : TransformationKeyframeInterpolation =
537634 transform . interpolation === 'step' || transform . interpolation === 'pre-post'
538635 ? { type : 'step' }
539636 : { type : 'linear' , easing : 'linear' }
540637
541- node_keyframes [ nodeId ] . position ??= { }
542- node_keyframes [ nodeId ] . rotation ??= { }
543- node_keyframes [ nodeId ] . scale ??= { }
544-
545- node_keyframes [ nodeId ] . position ! [ timeKey ] = {
546- value : [
547- toMolangNumber ( transform . pos [ 0 ] ) ,
548- toMolangNumber ( transform . pos [ 1 ] ) ,
549- toMolangNumber ( transform . pos [ 2 ] ) ,
550- ] ,
551- interpolation : createInterpolation ( ) ,
552- }
553- node_keyframes [ nodeId ] . rotation ! [ timeKey ] = {
554- value : [
555- toMolangNumber ( transform . rot [ 0 ] ) ,
556- toMolangNumber ( transform . rot [ 1 ] ) ,
557- toMolangNumber ( transform . rot [ 2 ] ) ,
558- ] ,
559- interpolation : createInterpolation ( ) ,
560- }
561- node_keyframes [ nodeId ] . scale ! [ timeKey ] = {
562- value : [
563- toMolangNumber ( transform . scale [ 0 ] ) ,
564- toMolangNumber ( transform . scale [ 1 ] ) ,
565- toMolangNumber ( transform . scale [ 2 ] ) ,
566- ] ,
567- interpolation : createInterpolation ( ) ,
568- }
569- }
570- }
638+ const channels = ( node_keyframes [ nodeId ] ??= { } )
639+ const position = ( channels . position ??= { } )
640+ const rotation = ( channels . rotation ??= { } )
641+ const scale = ( channels . scale ??= { } )
571642
572- // eslint-disable-next-line @typescript-eslint/naming-convention
573- let global_keyframes : NonNullable < PluginAnimation [ 'global_keyframes' ] > | undefined
643+ position [ timeKey ] = { value : transform . pos . map ( toMolangNumber ) as [ string , string , string ] , interpolation }
644+ rotation [ timeKey ] = { value : transform . rot . map ( toMolangNumber ) as [ string , string , string ] , interpolation }
645+ scale [ timeKey ] = { value : transform . scale . map ( toMolangNumber ) as [ string , string , string ] , interpolation }
646+ }
574647
575- // map the baked variant for each frame into the texture keyframes
576- if ( options . paletteIds . length ) {
577- const textureKeyframes : Record < string , Record < string , string > > = { }
578- for ( const frame of animation . frames ) {
579- if ( ! frame . variants ?. length ) continue
648+ if ( paletteIds . length && frame . variants ?. length ) {
580649 const variant = Variant . getByUUID ( frame . variants [ 0 ] )
581- if ( ! variant ) continue
582- const timeKey = formatTimestamp ( frame . time )
583- textureKeyframes [ timeKey ] ??= { }
584- for ( const paletteId of options . paletteIds ) {
585- textureKeyframes [ timeKey ] [ paletteId ] = variant . name
650+ if ( variant ) {
651+ global_keyframes ??= { }
652+ const texture = ( global_keyframes . texture ??= { } )
653+ const slot = ( texture [ timeKey ] ??= { } )
654+ for ( const paletteId of paletteIds ) slot [ paletteId ] = variant . name
586655 }
587656 }
588- if ( Object . keys ( textureKeyframes ) . length ) {
589- global_keyframes ??= { }
590- global_keyframes . texture = textureKeyframes
591- }
592657 }
593658
594659 return scrubUndefined ( {
595- loop_mode,
660+ loop_mode : buildLoopMode ( animation ) ,
596661 blend_weight : '1' ,
597662 start_delay : '0' ,
598- length : maxTime ,
663+ length : animation . frames . at ( - 1 ) ?. time ?? 0 ,
599664 global_keyframes,
600665 node_keyframes,
601666 } satisfies PluginAnimation )
@@ -642,13 +707,11 @@ export function exportPluginBlueprint(options: {
642707 }
643708
644709 const animations : Record < string , PluginAnimation > = { }
710+ const usedAnimationKeys = new Set < string > ( )
711+ const serialize = aj . baked_animations ? serializeBakedAnimation : serializeRawAnimation
645712 for ( const animation of options . animations ) {
646- const key = ensureUniqueKey ( animation . storage_name , new Set ( Object . keys ( animations ) ) )
647- animations [ key ] = serializeAnimation ( {
648- animation,
649- nodeUuidToId,
650- paletteIds,
651- } )
713+ const key = ensureUniqueKey ( animation . storage_name , usedAnimationKeys )
714+ animations [ key ] = serialize ( { animation, nodeUuidToId, paletteIds } )
652715 }
653716
654717 const blueprint : PluginBlueprintJson = scrubUndefined ( {
0 commit comments