topview-3d-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. topview_3d_cli/__init__.py +3 -0
  2. topview_3d_cli/__main__.py +5 -0
  3. topview_3d_cli/_runtime/builtin-assets/LICENSE +396 -0
  4. topview_3d_cli/_runtime/builtin-assets/NOTICE +24 -0
  5. topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_30ec53c93eae4173946b38639691ebe0.glb +0 -0
  6. topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_461b4f4b76ae4b198f439fa4f6f743b6.glb +0 -0
  7. topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_7d220ac359844c80a8faaf0e25478477.glb +0 -0
  8. topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_de37873a79244fbe8a0660ea08d1b4ab.glb +0 -0
  9. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-jab.webp +0 -0
  10. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-lunge.webp +0 -0
  11. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-rear.webp +0 -0
  12. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-crawling-forward-reaching-with-le.webp +0 -0
  13. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-exaggerated-punch-with-right-hand.webp +0 -0
  14. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-examine-fwd.webp +0 -0
  15. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-holding-bow.webp +0 -0
  16. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-jab-with-right-hand.webp +0 -0
  17. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-on-hands-and-toes-right-leg-forwa.webp +0 -0
  18. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-on-right-leg.webp +0 -0
  19. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-push-up.webp +0 -0
  20. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-backwards.webp +0 -0
  21. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-with-both-hands.webp +0 -0
  22. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-with-left-side-of-body.webp +0 -0
  23. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-sliding.webp +0 -0
  24. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-arms-spread-looking-upwards.webp +0 -0
  25. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-examine-fwd.webp +0 -0
  26. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-lean-fwd-right-hand-supporting.webp +0 -0
  27. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-leaning-against-object-look-l.webp +0 -0
  28. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-back.webp +0 -0
  29. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-down-back.webp +0 -0
  30. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-fwd-f.webp +0 -0
  31. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-fwd-m.webp +0 -0
  32. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-on-knees-arms-up.webp +0 -0
  33. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-sneaking.webp +0 -0
  34. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-l-toe-top-of-right-foot.webp +0 -0
  35. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-spread-lean-l.webp +0 -0
  36. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-spread-lean-r.webp +0 -0
  37. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-x.webp +0 -0
  38. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-foot-arms-extended-backwards.webp +0 -0
  39. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-toe-arms-up.webp +0 -0
  40. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-toe-l-leg-up.webp +0 -0
  41. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-toes-arch.webp +0 -0
  42. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-toes-reach.webp +0 -0
  43. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-bent-forwards.webp +0 -0
  44. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-left-leg-bent-hands-spread.webp +0 -0
  45. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-legs-bent-and-arms-spread.webp +0 -0
  46. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-legs-raised-arms-extended-u.webp +0 -0
  47. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-l-toe-look-l.webp +0 -0
  48. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-leaping-forward-arms-extended-backwa.webp +0 -0
  49. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dynamic-float-arch.webp +0 -0
  50. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dynamic-float-dive.webp +0 -0
  51. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-back-arched-arms-supp.webp +0 -0
  52. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-legs-to-side-arms-up.webp +0 -0
  53. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-legs-x-arms-supp.webp +0 -0
  54. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-left-leg-bent-hands-head.webp +0 -0
  55. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-left-leg-bent.webp +0 -0
  56. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-legs-x-hands-head.webp +0 -0
  57. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-left-side-left-arm-supporting-hea.webp +0 -0
  58. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-left-side-left-arm-supporting.webp +0 -0
  59. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-right-side-right-arm-supporting.webp +0 -0
  60. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-right-side-right-hand-supporting.webp +0 -0
  61. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-stomach-elbows-supporting.webp +0 -0
  62. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-right-arm-supporting-l-arm-up.webp +0 -0
  63. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-right-leg-forward-arms-supp.webp +0 -0
  64. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-run-fwd.webp +0 -0
  65. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-run-reach.webp +0 -0
  66. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-sneaking-forward.webp +0 -0
  67. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-sprinting.webp +0 -0
  68. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-stepping-forward-look-l.webp +0 -0
  69. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walk-fwd-look-r.webp +0 -0
  70. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walk-fwd.webp +0 -0
  71. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking-arms-on-hips.webp +0 -0
  72. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking-right-arm-on-hip.webp +0 -0
  73. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking.webp +0 -0
  74. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-holding-left-knee-reclining.webp +0 -0
  75. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-holding-right-knee.webp +0 -0
  76. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-resting-on-knees.webp +0 -0
  77. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-supp.webp +0 -0
  78. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-up.webp +0 -0
  79. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-elbows-on-legs-lean-fwd.webp +0 -0
  80. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-elbows-on-legs-lean-l.webp +0 -0
  81. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-knees-up-hold.webp +0 -0
  82. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-back-arms-supp.webp +0 -0
  83. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-fwd-elbows-on-knees.webp +0 -0
  84. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-fwd-hands-in-front.webp +0 -0
  85. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-arms-x.webp +0 -0
  86. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-hands-head.webp +0 -0
  87. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-hold.webp +0 -0
  88. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-reclining.webp +0 -0
  89. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-right-arm-over-knee.webp +0 -0
  90. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reaching-hands-towards-toes.webp +0 -0
  91. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reaching-left-hand-towards-right-toe.webp +0 -0
  92. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-facepalm.webp +0 -0
  93. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-hands-head.webp +0 -0
  94. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-left-leg-crossed-hands-head.webp +0 -0
  95. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-left-leg-crossed.webp +0 -0
  96. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-look-down.webp +0 -0
  97. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-right-leg-crossed-hands-hea.webp +0 -0
  98. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-rev-sit-hands-grasping.webp +0 -0
  99. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-rev-sit-hands-relaxed.webp +0 -0
  100. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-behind-back.webp +0 -0
  101. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-down.webp +0 -0
  102. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-slightly-spread.webp +0 -0
  103. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-bending-forward-arms-x-over-knees.webp +0 -0
  104. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-hands-on-hips-look-back.webp +0 -0
  105. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-hip-f.webp +0 -0
  106. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-hip-m.webp +0 -0
  107. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-leg-r-hip-l-arm-up.webp +0 -0
  108. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-lean-back-arms-behind.webp +0 -0
  109. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-lean-back-l-hip.webp +0 -0
  110. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-elbow-resting-obj.webp +0 -0
  111. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-leg-obj-left-hand-on-knee-r-h.webp +0 -0
  112. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-leg-obj-left-hand-on-knee.webp +0 -0
  113. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-arms-up.webp +0 -0
  114. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-l-hip.webp +0 -0
  115. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-leaning-obj.webp +0 -0
  116. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-left-arm-obj-r-hip.webp +0 -0
  117. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-down-r.webp +0 -0
  118. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-fwd.webp +0 -0
  119. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-l.webp +0 -0
  120. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-r.webp +0 -0
  121. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-arm-up-l-hip.webp +0 -0
  122. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-foot-l-leg-up.webp +0 -0
  123. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-foot-up.webp +0 -0
  124. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-hip-l-arm-up.webp +0 -0
  125. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-hip.webp +0 -0
  126. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-hand-behind-head.webp +0 -0
  127. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-leg-obj-arms-x.webp +0 -0
  128. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-leg-obj-l-hip-right-hand-on.webp +0 -0
  129. topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_t-pose.webp +0 -0
  130. topview_3d_cli/_runtime/builtin-assets/manifest.json +2372 -0
  131. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-jab.json +1 -0
  132. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-lunge.json +1 -0
  133. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-rear.json +1 -0
  134. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-crawling-forward-reaching-with-le.json +1 -0
  135. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-exaggerated-punch-with-right-hand.json +1 -0
  136. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-examine-fwd.json +1 -0
  137. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-holding-bow.json +1 -0
  138. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-jab-with-right-hand.json +1 -0
  139. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-on-hands-and-toes-right-leg-forwa.json +1 -0
  140. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-on-right-leg.json +1 -0
  141. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-push-up.json +1 -0
  142. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-backwards.json +1 -0
  143. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-with-both-hands.json +1 -0
  144. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-with-left-side-of-body.json +1 -0
  145. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-sliding.json +1 -0
  146. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-arms-spread-looking-upwards.json +1 -0
  147. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-examine-fwd.json +1 -0
  148. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-lean-fwd-right-hand-supporting.json +1 -0
  149. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-leaning-against-object-look-l.json +1 -0
  150. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-back.json +1 -0
  151. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-down-back.json +1 -0
  152. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-fwd-f.json +1 -0
  153. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-fwd-m.json +1 -0
  154. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-on-knees-arms-up.json +1 -0
  155. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-sneaking.json +1 -0
  156. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-l-toe-top-of-right-foot.json +1 -0
  157. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-spread-lean-l.json +1 -0
  158. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-spread-lean-r.json +1 -0
  159. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-x.json +1 -0
  160. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-foot-arms-extended-backwards.json +1 -0
  161. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-toe-arms-up.json +1 -0
  162. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-toe-l-leg-up.json +1 -0
  163. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-toes-arch.json +1 -0
  164. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-toes-reach.json +1 -0
  165. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-bent-forwards.json +1 -0
  166. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-left-leg-bent-hands-spread.json +1 -0
  167. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-legs-bent-and-arms-spread.json +1 -0
  168. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-legs-raised-arms-extended-u.json +1 -0
  169. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-l-toe-look-l.json +1 -0
  170. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-leaping-forward-arms-extended-backwa.json +1 -0
  171. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dynamic-float-arch.json +1 -0
  172. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dynamic-float-dive.json +1 -0
  173. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-back-arched-arms-supp.json +1 -0
  174. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-legs-to-side-arms-up.json +1 -0
  175. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-legs-x-arms-supp.json +1 -0
  176. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-left-leg-bent-hands-head.json +1 -0
  177. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-left-leg-bent.json +1 -0
  178. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-legs-x-hands-head.json +1 -0
  179. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-left-side-left-arm-supporting-hea.json +1 -0
  180. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-left-side-left-arm-supporting.json +1 -0
  181. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-right-side-right-arm-supporting.json +1 -0
  182. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-right-side-right-hand-supporting.json +1 -0
  183. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-stomach-elbows-supporting.json +1 -0
  184. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-right-arm-supporting-l-arm-up.json +1 -0
  185. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-right-leg-forward-arms-supp.json +1 -0
  186. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-run-fwd.json +1 -0
  187. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-run-reach.json +1 -0
  188. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-sneaking-forward.json +1 -0
  189. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-sprinting.json +1 -0
  190. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-stepping-forward-look-l.json +1 -0
  191. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walk-fwd-look-r.json +1 -0
  192. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walk-fwd.json +1 -0
  193. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking-arms-on-hips.json +1 -0
  194. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking-right-arm-on-hip.json +1 -0
  195. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking.json +1 -0
  196. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-holding-left-knee-reclining.json +1 -0
  197. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-holding-right-knee.json +1 -0
  198. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-resting-on-knees.json +1 -0
  199. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-supp.json +1 -0
  200. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-up.json +1 -0
  201. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-elbows-on-legs-lean-fwd.json +1 -0
  202. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-elbows-on-legs-lean-l.json +1 -0
  203. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-knees-up-hold.json +1 -0
  204. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-back-arms-supp.json +1 -0
  205. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-fwd-elbows-on-knees.json +1 -0
  206. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-fwd-hands-in-front.json +1 -0
  207. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-arms-x.json +1 -0
  208. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-hands-head.json +1 -0
  209. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-hold.json +1 -0
  210. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-reclining.json +1 -0
  211. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-right-arm-over-knee.json +1 -0
  212. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reaching-hands-towards-toes.json +1 -0
  213. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reaching-left-hand-towards-right-toe.json +1 -0
  214. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-facepalm.json +1 -0
  215. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-hands-head.json +1 -0
  216. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-left-leg-crossed-hands-head.json +1 -0
  217. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-left-leg-crossed.json +1 -0
  218. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-look-down.json +1 -0
  219. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-right-leg-crossed-hands-hea.json +1 -0
  220. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-rev-sit-hands-grasping.json +1 -0
  221. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-rev-sit-hands-relaxed.json +1 -0
  222. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-behind-back.json +1 -0
  223. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-down.json +1 -0
  224. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-slightly-spread.json +1 -0
  225. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-bending-forward-arms-x-over-knees.json +1 -0
  226. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-hands-on-hips-look-back.json +1 -0
  227. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-hip-f.json +1 -0
  228. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-hip-m.json +1 -0
  229. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-leg-r-hip-l-arm-up.json +1 -0
  230. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-lean-back-arms-behind.json +1 -0
  231. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-lean-back-l-hip.json +1 -0
  232. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-elbow-resting-obj.json +1 -0
  233. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-leg-obj-left-hand-on-knee-r-h.json +1 -0
  234. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-leg-obj-left-hand-on-knee.json +1 -0
  235. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-arms-up.json +1 -0
  236. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-l-hip.json +1 -0
  237. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-leaning-obj.json +1 -0
  238. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-left-arm-obj-r-hip.json +1 -0
  239. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-down-r.json +1 -0
  240. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-fwd.json +1 -0
  241. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-l.json +1 -0
  242. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-r.json +1 -0
  243. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-arm-up-l-hip.json +1 -0
  244. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-foot-l-leg-up.json +1 -0
  245. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-foot-up.json +1 -0
  246. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-hip-l-arm-up.json +1 -0
  247. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-hip.json +1 -0
  248. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-hand-behind-head.json +1 -0
  249. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-leg-obj-arms-x.json +1 -0
  250. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-leg-obj-l-hip-right-hand-on.json +1 -0
  251. topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_t-pose.json +1 -0
  252. topview_3d_cli/_runtime/director-cli/applyIntent.mjs +35 -0
  253. topview_3d_cli/_runtime/director-cli/browser.mjs +47 -0
  254. topview_3d_cli/_runtime/director-cli/cli.mjs +56 -0
  255. topview_3d_cli/_runtime/director-cli/editSequence.mjs +125 -0
  256. topview_3d_cli/_runtime/director-cli/evaluate.mjs +254 -0
  257. topview_3d_cli/_runtime/director-cli/mediaUrl.mjs +47 -0
  258. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_decoder.js +34 -0
  259. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_decoder.wasm +0 -0
  260. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_wasm_wrapper.js +117 -0
  261. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/engine/index.mjs +22219 -0
  262. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/evaluate/index.mjs +4229 -0
  263. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/headless/index.mjs +22832 -0
  264. topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/package.json +75 -0
  265. topview_3d_cli/_runtime/director-cli/node_modules/three/LICENSE +21 -0
  266. topview_3d_cli/_runtime/director-cli/node_modules/three/build/three.core.js +59732 -0
  267. topview_3d_cli/_runtime/director-cli/node_modules/three/build/three.module.js +19552 -0
  268. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/controls/OrbitControls.js +1963 -0
  269. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/curves/NURBSCurve.js +155 -0
  270. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/curves/NURBSUtils.js +532 -0
  271. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_decoder.js +34 -0
  272. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_decoder.wasm +0 -0
  273. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_wasm_wrapper.js +117 -0
  274. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/fflate.module.js +2672 -0
  275. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineMaterial.js +703 -0
  276. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineSegments2.js +426 -0
  277. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js +298 -0
  278. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/DRACOLoader.js +739 -0
  279. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/FBXLoader.js +4580 -0
  280. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/GLTFLoader.js +4860 -0
  281. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/utils/BufferGeometryUtils.js +1434 -0
  282. topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  283. topview_3d_cli/_runtime/director-cli/node_modules/three/package.json +123 -0
  284. topview_3d_cli/_runtime/director-cli/node_modules/zod/LICENSE +21 -0
  285. topview_3d_cli/_runtime/director-cli/node_modules/zod/index.js +4 -0
  286. topview_3d_cli/_runtime/director-cli/node_modules/zod/package.json +118 -0
  287. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/ZodError.js +133 -0
  288. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/errors.js +9 -0
  289. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/external.js +6 -0
  290. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  291. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  292. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  293. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/util.js +133 -0
  294. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/locales/en.js +109 -0
  295. topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/types.js +3693 -0
  296. topview_3d_cli/_runtime/director-cli/package.json +6 -0
  297. topview_3d_cli/_runtime/director-cli/playwright.mjs +26 -0
  298. topview_3d_cli/_runtime/director-cli/render.mjs +336 -0
  299. topview_3d_cli/_runtime/director-cli/static/headless.html +124 -0
  300. topview_3d_cli/_runtime/director-cli/static/measure.mjs +61 -0
  301. topview_3d_cli/_runtime/director-cli/static/pose.mjs +71 -0
  302. topview_3d_cli/_runtime/director-cli/staticIntent.mjs +135 -0
  303. topview_3d_cli/_runtime/runtime.json +6 -0
  304. topview_3d_cli/bom.py +326 -0
  305. topview_3d_cli/contracts/director-v1/schema/director-document.schema.json +1097 -0
  306. topview_3d_cli/contracts/director-v1/schema/fcurves-compact-v1.schema.json +105 -0
  307. topview_3d_cli/director_camera_view.py +54 -0
  308. topview_3d_cli/director_document.py +793 -0
  309. topview_3d_cli/director_geometry.py +66 -0
  310. topview_3d_cli/director_operations.py +679 -0
  311. topview_3d_cli/director_static.py +134 -0
  312. topview_3d_cli/local_assets.py +248 -0
  313. topview_3d_cli/local_bom.py +108 -0
  314. topview_3d_cli/local_catalog.py +156 -0
  315. topview_3d_cli/local_cli.py +493 -0
  316. topview_3d_cli/local_edit.py +350 -0
  317. topview_3d_cli/local_errors.py +104 -0
  318. topview_3d_cli/local_inspect.py +302 -0
  319. topview_3d_cli/local_project.py +280 -0
  320. topview_3d_cli/local_read.py +139 -0
  321. topview_3d_cli/local_render.py +97 -0
  322. topview_3d_cli/node_batch.py +299 -0
  323. topview_3d_cli/renderer.py +75 -0
  324. topview_3d_cli/runtime.py +107 -0
  325. topview_3d_cli-0.1.0.dist-info/METADATA +48 -0
  326. topview_3d_cli-0.1.0.dist-info/RECORD +332 -0
  327. topview_3d_cli-0.1.0.dist-info/WHEEL +5 -0
  328. topview_3d_cli-0.1.0.dist-info/entry_points.txt +2 -0
  329. topview_3d_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  330. topview_3d_cli-0.1.0.dist-info/licenses/LICENSE-ASSETS +396 -0
  331. topview_3d_cli-0.1.0.dist-info/licenses/NOTICE +18 -0
  332. topview_3d_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,679 @@
1
+ """Director operation engine shared by the local CLI and the local agent backend.
2
+
3
+ State is kept as separate entities, exactly like the live-operation service
4
+ (`agent/topview_3d_cli/contracts/director-v1/README.md`):
5
+
6
+ - ``director`` DIRECTOR_SCENE document without nodes, clips and fcurves
7
+ - ``<nodeId>`` DIRECTOR_NODE one DraftNode
8
+ - ``<clipId>`` DIRECTOR_CLIP ``{"clipKind": ..., "clip": ...}``
9
+ - ``fcurves__<nodeId>`` DIRECTOR_FCURVES ``{"version": 1, "encoding": "compact-v1", "fcurves": [...]}``
10
+
11
+ `DirectorStore.assemble()` rebuilds the document plus one merged compact-v1
12
+ fcurves object. Every accepted operation advances ``sceneSequence`` by one.
13
+
14
+ Version rules (``strict_versions``):
15
+
16
+ - strict (service parity): updating or deleting an existing entity requires
17
+ ``expectedEntityVersion`` equal to its current version; a deleted id can
18
+ never be written again (``ENTITY_DELETED``).
19
+ - single-writer: a missing/null ``expectedEntityVersion`` means "overwrite the
20
+ current version"; a supplied value must still match. A deleted id may be
21
+ recreated and continues its version counter.
22
+
23
+ ``baseSequence`` is optional in both modes; when present it must satisfy
24
+ ``0 <= baseSequence <= sceneSequence`` (``SEQUENCE_GAP``). ``node.delete`` does
25
+ not cascade: callers that want referencing clips/fcurves removed must send
26
+ those deletes in the same batch (see ``director_static.delete_node_operations``).
27
+ Missing parent/children/clip-target references are reported as warnings.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import copy
32
+ import hashlib
33
+ import json
34
+ import math
35
+ import re
36
+ from dataclasses import dataclass, field
37
+ from typing import Any, Iterable
38
+
39
+ from topview_3d_cli.director_document import (
40
+ DIRECTOR_DOCUMENT_TYPE,
41
+ empty_director_document,
42
+ fcurve_node_id,
43
+ new_scene_operation_id,
44
+ normalize_document,
45
+ normalize_node,
46
+ preserve_node_metadata,
47
+ scene_content_from_payload,
48
+ validate_director_node,
49
+ )
50
+
51
+ DIRECTOR_SCENE = "DIRECTOR_SCENE"
52
+ DIRECTOR_NODE = "DIRECTOR_NODE"
53
+ DIRECTOR_CLIP = "DIRECTOR_CLIP"
54
+ DIRECTOR_FCURVES = "DIRECTOR_FCURVES"
55
+ SCENE_ENTITY_ID = "director"
56
+ FCURVES_ENCODING = "compact-v1"
57
+ MAX_BATCH_OPERATIONS = 64
58
+ PUBLIC_KINDS = frozenset({
59
+ "director.scene.set", "director.node.upsert", "director.node.delete",
60
+ "director.clip.upsert", "director.clip.delete",
61
+ "director.fcurves.set", "director.fcurves.delete",
62
+ })
63
+ CLIP_ARRAYS = {
64
+ "cameraMotion": "cameraMotionClips",
65
+ "motion": "motionClips",
66
+ "pathMotion": "pathMotionClips",
67
+ }
68
+ _ID = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
69
+ _KIND = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
70
+ _FCURVES_ENTITY = re.compile(r"^fcurves__[A-Za-z0-9_-]+$")
71
+
72
+
73
+ class DirectorOperationError(ValueError):
74
+ """The operation or batch is malformed; nothing was applied."""
75
+
76
+ def __init__(self, code: str, detail: str | None = None):
77
+ self.code = code
78
+ self.detail = detail
79
+ super().__init__(f"{code}:{detail}" if detail else code)
80
+
81
+
82
+ class DirectorConflictError(RuntimeError):
83
+ """The operation is well formed but conflicts with current state; nothing was applied."""
84
+
85
+ def __init__(self, code: str, detail: str | None = None):
86
+ self.code = code
87
+ self.detail = detail
88
+ super().__init__(f"{code}:{detail}" if detail else code)
89
+
90
+
91
+ @dataclass
92
+ class Entity:
93
+ entity_type: str
94
+ version: int
95
+ state: dict[str, Any] | None
96
+ order: int
97
+ created_sequence: int
98
+ updated_sequence: int
99
+ deleted_sequence: int | None = None
100
+
101
+ @property
102
+ def live(self) -> bool:
103
+ return self.deleted_sequence is None
104
+
105
+ def to_json(self) -> dict[str, Any]:
106
+ return {
107
+ "entityType": self.entity_type,
108
+ "entityVersion": self.version,
109
+ "state": self.state,
110
+ "order": self.order,
111
+ "createdSequence": self.created_sequence,
112
+ "updatedSequence": self.updated_sequence,
113
+ "deletedSequence": self.deleted_sequence,
114
+ }
115
+
116
+ @classmethod
117
+ def from_json(cls, raw: dict[str, Any]) -> "Entity":
118
+ return cls(
119
+ entity_type=str(raw["entityType"]),
120
+ version=int(raw["entityVersion"]),
121
+ state=raw.get("state"),
122
+ order=int(raw.get("order", 0)),
123
+ created_sequence=int(raw.get("createdSequence", 0)),
124
+ updated_sequence=int(raw.get("updatedSequence", 0)),
125
+ deleted_sequence=raw.get("deletedSequence"),
126
+ )
127
+
128
+
129
+ def empty_fcurves() -> dict[str, Any]:
130
+ return {"version": 1, "encoding": FCURVES_ENCODING, "fcurves": []}
131
+
132
+
133
+ def _is_int(value: Any) -> bool:
134
+ return isinstance(value, int) and not isinstance(value, bool)
135
+
136
+
137
+ def _require_exact_fields(value: dict[str, Any], required: Iterable[str], what: str) -> None:
138
+ expected = set(required)
139
+ if set(value) != expected:
140
+ raise DirectorOperationError(
141
+ "INVALID_DIRECTOR_PAYLOAD", f"{what} must have exactly {sorted(expected)}, got {sorted(value)}")
142
+
143
+
144
+ def _operation_hash(operation: dict[str, Any]) -> str:
145
+ body = {key: value for key, value in operation.items() if key != "toolCallId"}
146
+ encoded = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
147
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
148
+
149
+
150
+ def wrap_scene_set(scene: Any) -> dict[str, Any]:
151
+ try:
152
+ incoming = scene_content_from_payload(scene)
153
+ except ValueError as exc:
154
+ raise DirectorOperationError("INVALID_DIRECTOR_SCENE", str(exc)) from exc
155
+ incoming.setdefault("version", 1)
156
+ wrapped: dict[str, Any] = {
157
+ "type": scene["type"] if isinstance(scene.get("type"), str) else DIRECTOR_DOCUMENT_TYPE,
158
+ "content": incoming,
159
+ }
160
+ if "pippitAssetId" in scene:
161
+ wrapped["pippitAssetId"] = scene["pippitAssetId"]
162
+ if "extra" in scene:
163
+ wrapped["extra"] = copy.deepcopy(scene["extra"])
164
+ return wrapped
165
+
166
+
167
+ def merge_scene_set(current: dict[str, Any] | None, scene: Any) -> dict[str, Any]:
168
+ """Shallow-merge ``scene.content`` fields into the stored scene entity."""
169
+ wrapped = wrap_scene_set(scene)
170
+ merged = copy.deepcopy(current) if isinstance(current, dict) else empty_director_document()
171
+ merged["type"] = wrapped["type"]
172
+ for key in ("pippitAssetId", "extra"):
173
+ if key in wrapped:
174
+ merged[key] = copy.deepcopy(wrapped[key])
175
+ content = merged.get("content") if isinstance(merged.get("content"), dict) else {}
176
+ for key, value in wrapped["content"].items():
177
+ if key != "nodes":
178
+ content[key] = copy.deepcopy(value)
179
+ content.pop("nodes", None)
180
+ merged["content"] = content
181
+ try:
182
+ normalized = normalize_document(merged)
183
+ except ValueError as exc:
184
+ raise DirectorOperationError("INVALID_DIRECTOR_SCENE", str(exc)) from exc
185
+ normalized["content"].pop("nodes", None)
186
+ animation = normalized["content"].get("timeline", {}).get("animation")
187
+ if isinstance(animation, dict):
188
+ for key in (*CLIP_ARRAYS.values(), "fcurves"):
189
+ animation.pop(key, None)
190
+ return normalized
191
+
192
+
193
+ def _require_xyz(value: Any, field_name: str) -> None:
194
+ if not isinstance(value, dict) or set(value) != {"x", "y", "z"}:
195
+ raise DirectorOperationError("INVALID_DIRECTOR_TRANSFORM", field_name)
196
+ for axis in ("x", "y", "z"):
197
+ number = value[axis]
198
+ if isinstance(number, bool) or not isinstance(number, (int, float)) or not math.isfinite(number):
199
+ raise DirectorOperationError("INVALID_DIRECTOR_TRANSFORM", f"{field_name}.{axis}")
200
+
201
+
202
+ def _require_transform(transform: Any) -> None:
203
+ if not isinstance(transform, dict) or set(transform) != {"position", "rotation", "scale"}:
204
+ raise DirectorOperationError("INVALID_DIRECTOR_TRANSFORM", "transform")
205
+ for key in ("position", "rotation", "scale"):
206
+ _require_xyz(transform[key], key)
207
+
208
+
209
+ def _clip_frame_sort_key(state: dict[str, Any]) -> tuple[int, str]:
210
+ clip = state.get("clip") if isinstance(state.get("clip"), dict) else {}
211
+ start = clip.get("frameStart")
212
+ frame = int(start) if isinstance(start, (int, float)) and not isinstance(start, bool) else 0
213
+ return frame, str(clip.get("id") or "")
214
+
215
+
216
+ def split_document(document: Any, fcurves: Any) -> tuple[
217
+ dict[str, Any] | None, list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
218
+ """Split an assembled document + fcurves into scene, nodes, clip states and fcurve states."""
219
+ scene = None
220
+ nodes: list[dict[str, Any]] = []
221
+ clips: list[dict[str, Any]] = []
222
+ if isinstance(document, dict):
223
+ scene = copy.deepcopy(document)
224
+ content = scene.get("content")
225
+ if isinstance(content, dict):
226
+ for node in content.pop("nodes", None) or []:
227
+ if isinstance(node, dict):
228
+ nodes.append(copy.deepcopy(node))
229
+ timeline = content.get("timeline") if isinstance(content.get("timeline"), dict) else None
230
+ animation = timeline.get("animation") if timeline and isinstance(timeline.get("animation"), dict) else None
231
+ if animation is not None:
232
+ for clip_kind, key in CLIP_ARRAYS.items():
233
+ for clip in animation.pop(key, None) or []:
234
+ if isinstance(clip, dict):
235
+ clips.append({"clipKind": clip_kind, "clip": copy.deepcopy(clip)})
236
+ animation["fcurves"] = []
237
+ return scene, nodes, clips, split_fcurves(fcurves)
238
+
239
+
240
+ def split_fcurves(fcurves: Any) -> list[dict[str, Any]]:
241
+ """Group a merged compact-v1 fcurves object into per-node entity states."""
242
+ grouped: dict[str, list[dict[str, Any]]] = {}
243
+ curves = fcurves.get("fcurves") if isinstance(fcurves, dict) else None
244
+ for curve in curves if isinstance(curves, list) else []:
245
+ node_id = fcurve_node_id(curve)
246
+ if node_id is None:
247
+ raise DirectorOperationError("INVALID_DIRECTOR_FCURVES", "fcurve target required")
248
+ grouped.setdefault(node_id, []).append(copy.deepcopy(curve))
249
+ return [{"version": 1, "encoding": FCURVES_ENCODING, "fcurves": items} for items in grouped.values()]
250
+
251
+
252
+ def fcurves_entity_id(state: dict[str, Any]) -> str | None:
253
+ curves = state.get("fcurves") if isinstance(state, dict) else None
254
+ node_id = fcurve_node_id(curves[0]) if isinstance(curves, list) and curves else None
255
+ return f"fcurves__{node_id}" if node_id else None
256
+
257
+
258
+ @dataclass
259
+ class DirectorStore:
260
+ entities: dict[str, Entity] = field(default_factory=dict)
261
+ scene_sequence: int = 0
262
+ next_order: int = 0
263
+ operations: dict[str, dict[str, Any]] = field(default_factory=dict)
264
+
265
+ # --- persistence -------------------------------------------------------------------
266
+
267
+ def to_json(self) -> dict[str, Any]:
268
+ return {
269
+ "sceneSequence": self.scene_sequence,
270
+ "nextOrder": self.next_order,
271
+ "entities": {entity_id: entity.to_json() for entity_id, entity in self.entities.items()},
272
+ "operations": copy.deepcopy(self.operations),
273
+ }
274
+
275
+ @classmethod
276
+ def from_json(cls, raw: dict[str, Any]) -> "DirectorStore":
277
+ entities = {str(key): Entity.from_json(value) for key, value in (raw.get("entities") or {}).items()}
278
+ return cls(
279
+ entities=entities,
280
+ scene_sequence=int(raw.get("sceneSequence", 0)),
281
+ next_order=int(raw.get("nextOrder", len(entities))),
282
+ operations=copy.deepcopy(raw.get("operations") or {}),
283
+ )
284
+
285
+ @classmethod
286
+ def from_snapshot(
287
+ cls,
288
+ document: Any,
289
+ fcurves: Any = None,
290
+ *,
291
+ scene_sequence: int = 0,
292
+ entity_versions: dict[str, Any] | None = None,
293
+ ) -> "DirectorStore":
294
+ """Load an assembled snapshot without replaying operations (versions default to 1)."""
295
+ versions = entity_versions or {}
296
+ store = cls(scene_sequence=scene_sequence)
297
+ scene, nodes, clips, curve_states = split_document(document, None)
298
+ curve_states += _legacy_fcurve_states(fcurves) if _is_entity_keyed(fcurves) else split_fcurves(fcurves)
299
+
300
+ def add(entity_id: str | None, entity_type: str, state: dict[str, Any]) -> None:
301
+ if not entity_id:
302
+ return
303
+ store.entities[entity_id] = Entity(
304
+ entity_type=entity_type, version=int(versions.get(entity_id) or 1), state=state,
305
+ order=store.next_order, created_sequence=0, updated_sequence=0)
306
+ store.next_order += 1
307
+
308
+ if scene is not None:
309
+ add(SCENE_ENTITY_ID, DIRECTOR_SCENE, scene)
310
+ for node in nodes:
311
+ add(str(node.get("id") or ""), DIRECTOR_NODE, node)
312
+ for clip in clips:
313
+ add(str(clip["clip"].get("id") or ""), DIRECTOR_CLIP, clip)
314
+ for state in curve_states:
315
+ add(state.pop("__entityId", None) or fcurves_entity_id(state), DIRECTOR_FCURVES, state)
316
+ return store
317
+
318
+ # --- reads ------------------------------------------------------------------------
319
+
320
+ def _live(self, entity_type: str | None = None) -> list[tuple[str, Entity]]:
321
+ items = [
322
+ (entity_id, entity) for entity_id, entity in self.entities.items()
323
+ if entity.live and (entity_type is None or entity.entity_type == entity_type)
324
+ ]
325
+ return sorted(items, key=lambda item: (item[1].order, item[0]))
326
+
327
+ def entity_versions(self) -> dict[str, int]:
328
+ return {entity_id: entity.version for entity_id, entity in self._live()}
329
+
330
+ def assemble(self) -> dict[str, Any]:
331
+ scene_entity = self.entities.get(SCENE_ENTITY_ID)
332
+ document = copy.deepcopy(scene_entity.state) if scene_entity and scene_entity.live and scene_entity.state else {}
333
+ content = document.setdefault("content", {})
334
+ content["nodes"] = [copy.deepcopy(entity.state) for _, entity in self._live(DIRECTOR_NODE)]
335
+ timeline = content.get("timeline") if isinstance(content.get("timeline"), dict) else content.setdefault("timeline", {})
336
+ animation = timeline.get("animation") if isinstance(timeline.get("animation"), dict) else timeline.setdefault("animation", {})
337
+ animation["fcurves"] = []
338
+ clip_states = [entity.state for _, entity in self._live(DIRECTOR_CLIP)]
339
+ for clip_kind, key in CLIP_ARRAYS.items():
340
+ matching = [state for state in clip_states if state.get("clipKind") == clip_kind]
341
+ animation[key] = [copy.deepcopy(state["clip"]) for state in sorted(matching, key=_clip_frame_sort_key)]
342
+ merged = empty_fcurves()
343
+ for _, entity in self._live(DIRECTOR_FCURVES):
344
+ merged["fcurves"].extend(copy.deepcopy((entity.state or {}).get("fcurves") or []))
345
+ return {
346
+ "document": document,
347
+ "fcurves": merged,
348
+ "sceneSequence": self.scene_sequence,
349
+ "entityVersions": self.entity_versions(),
350
+ }
351
+
352
+ def dangling_references(self) -> list[dict[str, str]]:
353
+ live_nodes = {entity_id for entity_id, _ in self._live(DIRECTOR_NODE)}
354
+ issues = []
355
+ for entity_id, entity in self._live(DIRECTOR_NODE):
356
+ issues += [{"entityId": entity_id, "field": name, "ref": ref}
357
+ for name, ref in _node_refs(entity.state) if ref not in live_nodes]
358
+ for entity_id, entity in self._live(DIRECTOR_CLIP):
359
+ ref = _clip_target(entity.state)
360
+ if ref and ref not in live_nodes:
361
+ issues.append({"entityId": entity_id, "field": "target.nodeId", "ref": ref})
362
+ for entity_id, _ in self._live(DIRECTOR_FCURVES):
363
+ node_id = entity_id.removeprefix("fcurves__")
364
+ if node_id not in live_nodes:
365
+ issues.append({"entityId": entity_id, "field": "entityId", "ref": node_id})
366
+ return issues
367
+
368
+ # --- writes -----------------------------------------------------------------------
369
+
370
+ def apply(
371
+ self,
372
+ operations: Any,
373
+ *,
374
+ strict_versions: bool,
375
+ batch_id: str | None = None,
376
+ ) -> dict[str, Any]:
377
+ """Apply one batch atomically; the store is untouched when any operation fails."""
378
+ if not isinstance(operations, list) or not 1 <= len(operations) <= MAX_BATCH_OPERATIONS:
379
+ raise DirectorOperationError(
380
+ "INVALID_DIRECTOR_BATCH", f"operations must be a list of 1..{MAX_BATCH_OPERATIONS} items")
381
+ work = copy.deepcopy(self)
382
+ accepted: list[dict[str, Any]] = []
383
+ warnings: list[dict[str, Any]] = []
384
+ for index, operation in enumerate(operations):
385
+ try:
386
+ accepted.append(work._accept(operation, strict_versions, batch_id, warnings))
387
+ except (DirectorOperationError, DirectorConflictError) as exc:
388
+ exc.detail = f"operations[{index}]" + (f": {exc.detail}" if exc.detail else "")
389
+ exc.args = (f"{exc.code}:{exc.detail}",)
390
+ raise
391
+ self.entities, self.scene_sequence = work.entities, work.scene_sequence
392
+ self.next_order, self.operations = work.next_order, work.operations
393
+ return {"sceneSequence": self.scene_sequence, "accepted": accepted, "warnings": warnings}
394
+
395
+ def _accept(
396
+ self, operation: Any, strict: bool, batch_id: str | None, warnings: list[dict[str, Any]],
397
+ ) -> dict[str, Any]:
398
+ if not isinstance(operation, dict):
399
+ raise DirectorOperationError("INVALID_DIRECTOR_OPERATION", "operation must be an object")
400
+ operation_id = operation.get("operationId")
401
+ if not isinstance(operation_id, str) or not _ID.match(operation_id):
402
+ raise DirectorOperationError("INVALID_OPERATION_ID", "operationId must match [A-Za-z0-9_-]{1,128}")
403
+ kind = operation.get("kind")
404
+ if not isinstance(kind, str) or not _KIND.match(kind) or kind not in PUBLIC_KINDS:
405
+ raise DirectorOperationError("UNSUPPORTED_DIRECTOR_KIND", str(kind))
406
+ payload = operation.get("payload")
407
+ if not isinstance(payload, dict):
408
+ raise DirectorOperationError("INVALID_DIRECTOR_PAYLOAD", "payload must be an object")
409
+ digest = _operation_hash(operation)
410
+ prior = self.operations.get(operation_id)
411
+ if prior is not None:
412
+ if prior["sha256"] != digest:
413
+ raise DirectorConflictError("OPERATION_KEY_REUSED", operation_id)
414
+ return {**copy.deepcopy(prior["record"]), "replayed": True}
415
+ base = operation.get("baseSequence")
416
+ if base is None:
417
+ base = self.scene_sequence
418
+ elif not _is_int(base):
419
+ raise DirectorOperationError("INVALID_DIRECTOR_OPERATION", "baseSequence must be an integer")
420
+ if base < 0 or base > self.scene_sequence:
421
+ raise DirectorConflictError("SEQUENCE_GAP", f"baseSequence {base} > sceneSequence {self.scene_sequence}")
422
+ entity_id = operation.get("entityId")
423
+ if entity_id in (None, ""):
424
+ raise DirectorOperationError("INVALID_ENTITY_ID", "entityId required")
425
+ if not isinstance(entity_id, str) or not _ID.match(entity_id):
426
+ raise DirectorOperationError("INVALID_ENTITY_ID", str(entity_id))
427
+ expected = operation.get("expectedEntityVersion")
428
+ if expected is not None and not _is_int(expected):
429
+ raise DirectorOperationError("INVALID_DIRECTOR_OPERATION", "expectedEntityVersion must be an integer or null")
430
+
431
+ payload = copy.deepcopy(payload)
432
+ entity = self.entities.get(entity_id)
433
+ if kind == "director.scene.set":
434
+ entity_type, state = DIRECTOR_SCENE, self._scene_state(entity_id, payload, entity)
435
+ elif kind == "director.node.upsert":
436
+ entity_type, state = DIRECTOR_NODE, self._node_state(entity_id, payload, entity, warnings)
437
+ elif kind == "director.clip.upsert":
438
+ entity_type, state = DIRECTOR_CLIP, self._clip_state(entity_id, payload, warnings)
439
+ elif kind == "director.fcurves.set":
440
+ entity_type, state = DIRECTOR_FCURVES, self._fcurves_state(entity_id, payload)
441
+ else:
442
+ entity_type = {"director.node.delete": DIRECTOR_NODE, "director.clip.delete": DIRECTOR_CLIP,
443
+ "director.fcurves.delete": DIRECTOR_FCURVES}[kind]
444
+ _require_exact_fields(payload, (), kind)
445
+ state = None
446
+
447
+ sequence = self.scene_sequence + 1
448
+ if state is None:
449
+ version_after = self._delete(entity_id, entity_type, entity, expected, strict, sequence)
450
+ else:
451
+ version_after = self._upsert(entity_id, entity_type, entity, expected, strict, sequence, state)
452
+ self.scene_sequence = sequence
453
+ record = {
454
+ "sequence": sequence,
455
+ "operationId": operation_id,
456
+ "batchId": batch_id,
457
+ "baseSequence": base,
458
+ "kind": kind,
459
+ "entityId": entity_id,
460
+ "expectedEntityVersion": expected,
461
+ "entityVersionAfter": version_after,
462
+ "status": "accepted",
463
+ }
464
+ self.operations[operation_id] = {"sha256": digest, "record": record}
465
+ return {**record, "payload": payload}
466
+
467
+ def _upsert(self, entity_id: str, entity_type: str, entity: Entity | None, expected: int | None,
468
+ strict: bool, sequence: int, state: dict[str, Any]) -> int:
469
+ if entity is None:
470
+ if expected is not None:
471
+ raise DirectorConflictError("ENTITY_VERSION_CONFLICT", f"{entity_id} does not exist")
472
+ self.entities[entity_id] = Entity(entity_type, 1, state, self.next_order, sequence, sequence)
473
+ self.next_order += 1
474
+ return 1
475
+ if not entity.live:
476
+ if strict:
477
+ raise DirectorConflictError("ENTITY_DELETED", entity_id)
478
+ if expected is not None and expected != entity.version:
479
+ raise DirectorConflictError("ENTITY_VERSION_CONFLICT", f"{entity_id} is at {entity.version}")
480
+ self.entities[entity_id] = Entity(
481
+ entity_type, entity.version + 1, state, self.next_order, sequence, sequence)
482
+ self.next_order += 1
483
+ return entity.version + 1
484
+ if entity.entity_type != entity_type:
485
+ raise DirectorConflictError("ENTITY_TYPE_CONFLICT", f"{entity_id} is {entity.entity_type}")
486
+ self._check_version(entity_id, entity, expected, strict)
487
+ entity.state = state
488
+ entity.version += 1
489
+ entity.updated_sequence = sequence
490
+ return entity.version
491
+
492
+ def _delete(self, entity_id: str, entity_type: str, entity: Entity | None, expected: int | None,
493
+ strict: bool, sequence: int) -> int:
494
+ if entity is None:
495
+ raise DirectorConflictError("ENTITY_NOT_FOUND", entity_id)
496
+ if not entity.live:
497
+ raise DirectorConflictError("ENTITY_DELETED", entity_id)
498
+ if entity.entity_type != entity_type:
499
+ raise DirectorConflictError("ENTITY_TYPE_CONFLICT", f"{entity_id} is {entity.entity_type}")
500
+ self._check_version(entity_id, entity, expected, strict)
501
+ entity.version += 1
502
+ entity.state = None
503
+ entity.updated_sequence = sequence
504
+ entity.deleted_sequence = sequence
505
+ return entity.version
506
+
507
+ @staticmethod
508
+ def _check_version(entity_id: str, entity: Entity, expected: int | None, strict: bool) -> None:
509
+ if expected is None and not strict:
510
+ return
511
+ if expected is None or expected != entity.version or expected < 1:
512
+ raise DirectorConflictError(
513
+ "ENTITY_VERSION_CONFLICT", f"{entity_id} is at {entity.version}, expected {expected}")
514
+
515
+ def _scene_state(self, entity_id: str, payload: dict[str, Any], entity: Entity | None) -> dict[str, Any]:
516
+ if entity_id != SCENE_ENTITY_ID:
517
+ raise DirectorOperationError("INVALID_DIRECTOR_SCENE", "scene.set requires entityId 'director'")
518
+ _require_exact_fields(payload, ("scene",), "director.scene.set payload")
519
+ if not isinstance(payload["scene"], dict):
520
+ raise DirectorOperationError("INVALID_DIRECTOR_SCENE", "scene must be an object")
521
+ current = entity.state if entity is not None and entity.live else None
522
+ merged = merge_scene_set(current, payload["scene"])
523
+ if merged.get("type") != DIRECTOR_DOCUMENT_TYPE or merged["content"].get("version") != 1:
524
+ raise DirectorOperationError("INVALID_DIRECTOR_SCENE", "type and content.version must be director v1")
525
+ payload["scene"] = merged
526
+ return copy.deepcopy(merged)
527
+
528
+ def _node_state(self, entity_id: str, payload: dict[str, Any], entity: Entity | None,
529
+ warnings: list[dict[str, Any]]) -> dict[str, Any]:
530
+ _require_exact_fields(payload, ("node",), "director.node.upsert payload")
531
+ try:
532
+ node = normalize_node(payload["node"])
533
+ if entity is not None and isinstance(entity.state, dict):
534
+ preserve_node_metadata(node, entity.state)
535
+ validate_director_node(node)
536
+ except ValueError as exc:
537
+ raise DirectorOperationError("INVALID_DIRECTOR_NODE", str(exc)) from exc
538
+ if node["id"] != entity_id:
539
+ raise DirectorOperationError("INVALID_DIRECTOR_NODE", f"node.id {node['id']} != entityId {entity_id}")
540
+ _require_transform(node.get("transform"))
541
+ for name, ref in _node_refs(node):
542
+ self._warn_missing(ref, entity_id, name, warnings)
543
+ payload["node"] = node
544
+ return copy.deepcopy(node)
545
+
546
+ def _clip_state(self, entity_id: str, payload: dict[str, Any],
547
+ warnings: list[dict[str, Any]]) -> dict[str, Any]:
548
+ _require_exact_fields(payload, ("clipKind", "clip"), "director.clip.upsert payload")
549
+ clip_kind, clip = payload["clipKind"], payload["clip"]
550
+ if clip_kind not in CLIP_ARRAYS:
551
+ raise DirectorOperationError("INVALID_DIRECTOR_CLIP", f"clipKind {clip_kind!r}")
552
+ if not isinstance(clip, dict) or clip.get("id") != entity_id:
553
+ raise DirectorOperationError("INVALID_DIRECTOR_CLIP", "clip.id must equal entityId")
554
+ if "frameStart" in clip or "frameEnd" in clip:
555
+ start, end = clip.get("frameStart"), clip.get("frameEnd")
556
+ numeric = all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in (start, end))
557
+ if not numeric or start > end:
558
+ raise DirectorOperationError("INVALID_DIRECTOR_CLIP", "frameStart/frameEnd must be numbers with start <= end")
559
+ motion = clip.get("motion") if isinstance(clip.get("motion"), dict) else {}
560
+ if clip_kind == "cameraMotion" and not isinstance(motion.get("curves"), list):
561
+ raise DirectorOperationError("INVALID_DIRECTOR_CLIP", "motion.curves required; use bake_camera_motion")
562
+ target = _clip_target({"clip": clip})
563
+ if target:
564
+ self._warn_missing(target, entity_id, "target.nodeId", warnings)
565
+ return {"clipKind": clip_kind, "clip": copy.deepcopy(clip)}
566
+
567
+ @staticmethod
568
+ def _fcurves_state(entity_id: str, payload: dict[str, Any]) -> dict[str, Any]:
569
+ if not _FCURVES_ENTITY.match(entity_id):
570
+ raise DirectorOperationError("INVALID_ENTITY_ID", "fcurves entityId must be fcurves__<nodeId>")
571
+ _require_exact_fields(payload, ("version", "encoding", "fcurves"), "director.fcurves.set payload")
572
+ if (not _is_int(payload["version"]) or payload["version"] != 1
573
+ or payload["encoding"] != FCURVES_ENCODING or not isinstance(payload["fcurves"], list)):
574
+ raise DirectorOperationError("INVALID_DIRECTOR_FCURVES", "expected version 1, encoding compact-v1, fcurves array")
575
+ return copy.deepcopy(payload)
576
+
577
+ def _warn_missing(self, ref: str, entity_id: str, field_name: str, warnings: list[dict[str, Any]]) -> None:
578
+ target = self.entities.get(ref)
579
+ if target is None or not target.live:
580
+ warnings.append({"code": "DIRECTOR_REFERENCE_MISSING", "entityId": entity_id,
581
+ "field": field_name, "ref": ref})
582
+
583
+
584
+ def _node_refs(node: Any) -> list[tuple[str, str]]:
585
+ if not isinstance(node, dict):
586
+ return []
587
+ refs = []
588
+ parent = node.get("parentId")
589
+ if isinstance(parent, str) and parent.strip():
590
+ refs.append(("parentId", parent))
591
+ for child in node.get("children") or []:
592
+ if isinstance(child, str) and child.strip():
593
+ refs.append(("children", child))
594
+ return refs
595
+
596
+
597
+ def _clip_target(state: Any) -> str | None:
598
+ clip = state.get("clip") if isinstance(state, dict) else None
599
+ target = clip.get("target") if isinstance(clip, dict) and isinstance(clip.get("target"), dict) else {}
600
+ node_id = target.get("nodeId")
601
+ return node_id if isinstance(node_id, str) and node_id.strip() else None
602
+
603
+
604
+ def _is_entity_keyed(fcurves: Any) -> bool:
605
+ return isinstance(fcurves, dict) and "fcurves" not in fcurves and any(
606
+ isinstance(key, str) and key.startswith("fcurves__") for key in fcurves)
607
+
608
+
609
+ def _legacy_fcurve_states(fcurves: dict[str, Any]) -> list[dict[str, Any]]:
610
+ states = []
611
+ for entity_id, value in fcurves.items():
612
+ if not _FCURVES_ENTITY.match(str(entity_id)):
613
+ continue
614
+ curves = value.get("fcurves") if isinstance(value, dict) else None
615
+ states.append({"__entityId": entity_id, "version": 1, "encoding": FCURVES_ENCODING,
616
+ "fcurves": copy.deepcopy(curves) if isinstance(curves, list) else []})
617
+ return states
618
+
619
+
620
+ def diff_operations(
621
+ current: dict[str, Any],
622
+ document: Any,
623
+ fcurves: Any,
624
+ base_entity_versions: dict[str, Any] | None = None,
625
+ *,
626
+ stem: str = "dop",
627
+ ) -> list[dict[str, Any]]:
628
+ """Operations turning assembled ``current`` into ``document`` + ``fcurves`` (service put semantics)."""
629
+ versions = base_entity_versions or {}
630
+ old_scene, old_nodes, old_clips, old_curves = split_document(current.get("document"), current.get("fcurves"))
631
+ new_scene, new_nodes, new_clips, new_curves = split_document(document, fcurves)
632
+ operations: list[dict[str, Any]] = []
633
+
634
+ def add(kind: str, entity_id: str, payload: dict[str, Any]) -> None:
635
+ expected = versions.get(entity_id)
636
+ operations.append({
637
+ "operationId": new_scene_operation_id(stem),
638
+ "kind": kind,
639
+ "entityId": entity_id,
640
+ "expectedEntityVersion": expected if _is_int(expected) else None,
641
+ "payload": payload,
642
+ })
643
+
644
+ if (old_scene or None) != (new_scene or None):
645
+ add("director.scene.set", SCENE_ENTITY_ID, {"scene": copy.deepcopy(new_scene or {})})
646
+ old_node_index = {str(node.get("id")): node for node in old_nodes}
647
+ new_node_index = {str(node.get("id")): node for node in new_nodes}
648
+ for node_id, node in new_node_index.items():
649
+ if old_node_index.get(node_id) != node:
650
+ add("director.node.upsert", node_id, {"node": copy.deepcopy(node)})
651
+ old_clip_index = {str(state["clip"].get("id")): state for state in old_clips}
652
+ new_clip_index = {str(state["clip"].get("id")): state for state in new_clips}
653
+ for clip_id, state in new_clip_index.items():
654
+ if old_clip_index.get(clip_id) != state:
655
+ add("director.clip.upsert", clip_id, copy.deepcopy(state))
656
+ old_curve_index = {fcurves_entity_id(state): state for state in old_curves}
657
+ new_curve_index = {fcurves_entity_id(state): state for state in new_curves}
658
+ for curve_id, state in new_curve_index.items():
659
+ if curve_id and old_curve_index.get(curve_id) != state:
660
+ add("director.fcurves.set", curve_id, copy.deepcopy(state))
661
+ for curve_id in old_curve_index:
662
+ if curve_id and curve_id not in new_curve_index:
663
+ add("director.fcurves.delete", curve_id, {})
664
+ for clip_id in old_clip_index:
665
+ if clip_id not in new_clip_index:
666
+ add("director.clip.delete", clip_id, {})
667
+ for node_id in old_node_index:
668
+ if node_id not in new_node_index:
669
+ add("director.node.delete", node_id, {})
670
+ return operations
671
+
672
+
673
+ def genesis_store(document: Any, fcurves: Any = None) -> DirectorStore:
674
+ """New store whose entities come from ``document`` via accepted operations (service genesis)."""
675
+ store = DirectorStore()
676
+ operations = diff_operations(store.assemble() | {"document": None}, document, fcurves, stem="gen")
677
+ for start in range(0, len(operations), MAX_BATCH_OPERATIONS):
678
+ store.apply(operations[start:start + MAX_BATCH_OPERATIONS], strict_versions=True, batch_id="genesis")
679
+ return store