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,793 @@
1
+ """Normalize and fail-closed-validate director-document-v1 JSON."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import json
6
+ import re
7
+ import uuid
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from jsonschema import Draft7Validator
12
+
13
+ DIRECTOR_DOCUMENT_TYPE = "biz/scene3d-director-document"
14
+ NODE_TYPES = ("camera", "character", "prop", "path", "group", "primitive")
15
+ SCENE3D_OP_ID = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
16
+ _SCENE3D_OP_ID_UNSAFE = re.compile(r"[^A-Za-z0-9_-]")
17
+ SCHEMA_PATH = Path(__file__).resolve().parent / "contracts/director-v1/schema/director-document.schema.json"
18
+ FCURVES_SCHEMA_PATH = SCHEMA_PATH.with_name("fcurves-compact-v1.schema.json")
19
+ _VALIDATOR: Draft7Validator | None = None
20
+ _FCURVES_VALIDATOR: Draft7Validator | None = None
21
+
22
+
23
+ def _validator() -> Draft7Validator:
24
+ global _VALIDATOR
25
+ if _VALIDATOR is None:
26
+ if not SCHEMA_PATH.is_file():
27
+ raise FileNotFoundError(f"director-document schema missing: {SCHEMA_PATH}")
28
+ _VALIDATOR = Draft7Validator(json.loads(SCHEMA_PATH.read_text(encoding="utf-8")))
29
+ return _VALIDATOR
30
+
31
+
32
+ def new_scene_operation_id(stem: str = "op") -> str:
33
+ suffix = uuid.uuid4().hex[:10]
34
+ cleaned = _SCENE3D_OP_ID_UNSAFE.sub("_", str(stem or "op")).strip("_") or "op"
35
+ cleaned = cleaned[: 128 - 1 - len(suffix)].rstrip("_") or "op"
36
+ return f"{cleaned}_{suffix}"
37
+
38
+
39
+ def fcurve_node_id(curve: Any) -> str | None:
40
+ """Node id of one compact-v1 curve (``t: ["node", id]`` or ``t: id``; legacy ``nodeId``)."""
41
+ if not isinstance(curve, dict):
42
+ return None
43
+ target = curve.get("t")
44
+ if isinstance(target, list) and len(target) >= 2 and isinstance(target[1], str):
45
+ return target[1]
46
+ if isinstance(target, str):
47
+ return target
48
+ legacy = curve.get("nodeId")
49
+ return legacy if isinstance(legacy, str) and legacy else None
50
+
51
+
52
+ def xyz(x: float = 0, y: float = 0, z: float = 0) -> dict[str, float]:
53
+ return {"x": float(x), "y": float(y), "z": float(z)}
54
+
55
+
56
+ def empty_director_document() -> dict[str, Any]:
57
+ camera = default_camera_node("cam-main")
58
+ return {
59
+ "type": DIRECTOR_DOCUMENT_TYPE,
60
+ "pippitAssetId": "local_director",
61
+ "extra": {},
62
+ "content": {
63
+ "version": 1,
64
+ "aspectRatio": "16:9",
65
+ "activeShotCameraNodeId": camera["id"],
66
+ "environment": {
67
+ "background": {"mode": "color", "skyColor": "#87CEEB"},
68
+ "display": {
69
+ "characterLabelsVisible": True,
70
+ "groundVisible": True,
71
+ "groundHeight": 0,
72
+ "groundOpacity": 1,
73
+ },
74
+ },
75
+ "asset": {"motionPath": []},
76
+ "nodes": [camera],
77
+ "physicalConstraints": [],
78
+ "timeline": {
79
+ "version": 1,
80
+ "fps": 24,
81
+ "frameStart": 0,
82
+ "frameEnd": 120,
83
+ "usePreviewRange": False,
84
+ "animation": {
85
+ "fcurves": [],
86
+ "cameraMotionClips": [],
87
+ "motionTransitions": [],
88
+ "motionClips": [],
89
+ "pathMotionClips": [],
90
+ },
91
+ },
92
+ },
93
+ }
94
+
95
+
96
+ def default_camera_node(node_id: str = "cam-main") -> dict[str, Any]:
97
+ return {
98
+ "id": node_id,
99
+ "type": "camera",
100
+ "name": "主相机",
101
+ "visible": True,
102
+ "locked": False,
103
+ "transform": {
104
+ "position": xyz(0, 1.8, 5),
105
+ "rotation": xyz(-8, 0, 0),
106
+ "scale": xyz(1, 1, 1),
107
+ },
108
+ "camera": {
109
+ "projection": "perspective",
110
+ "fov": 50,
111
+ "fovAxis": "vertical",
112
+ "near": 0.1,
113
+ "far": 2000,
114
+ "isPrimary": True,
115
+ "lookAt": xyz(0, 1.2, 0),
116
+ },
117
+ }
118
+
119
+
120
+ def _as_xyz(value: Any, field: str, default: dict[str, float] | None = None) -> dict[str, float]:
121
+ if isinstance(value, (list, tuple)) and len(value) == 3:
122
+ return xyz(value[0], value[1], value[2])
123
+ if isinstance(value, dict) and all(axis in value for axis in ("x", "y", "z")):
124
+ return xyz(value["x"], value["y"], value["z"])
125
+ if default is not None:
126
+ return dict(default)
127
+ raise ValueError(f"INVALID_DIRECTOR_TRANSFORM:{field}")
128
+
129
+
130
+ def normalize_transform(raw: Any) -> dict[str, dict[str, float]]:
131
+ source = raw if isinstance(raw, dict) else {}
132
+ return {
133
+ "position": _as_xyz(source.get("position"), "position", xyz()),
134
+ "rotation": _as_xyz(source.get("rotation"), "rotation", xyz()),
135
+ "scale": _as_xyz(source.get("scale"), "scale", xyz(1, 1, 1)),
136
+ }
137
+
138
+
139
+ def _merge_metadata(node: dict[str, Any]) -> dict[str, Any]:
140
+ metadata = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
141
+ asset = node.get("asset") if isinstance(node.get("asset"), dict) else {}
142
+ if asset.get("file") and not metadata.get("modelUrl"):
143
+ metadata["modelUrl"] = asset["file"]
144
+ if asset.get("id") and not metadata.get("assetId"):
145
+ metadata["assetId"] = asset["id"]
146
+ return metadata
147
+
148
+
149
+ def _fill_camera(raw: Any) -> dict[str, Any]:
150
+ camera = copy.deepcopy(raw) if isinstance(raw, dict) else {}
151
+ camera["projection"] = camera.get("projection") or "perspective"
152
+ camera["fov"] = float(camera["fov"]) if isinstance(camera.get("fov"), (int, float)) else 50
153
+ camera["fovAxis"] = camera.get("fovAxis") or "vertical"
154
+ camera["near"] = float(camera["near"]) if isinstance(camera.get("near"), (int, float)) else 0.1
155
+ camera["far"] = float(camera["far"]) if isinstance(camera.get("far"), (int, float)) else 2000
156
+ camera["isPrimary"] = camera["isPrimary"] if isinstance(camera.get("isPrimary"), bool) else True
157
+ camera["lookAt"] = _as_xyz(camera.get("lookAt"), "lookAt", xyz(0, 1.2, 0))
158
+ return camera
159
+
160
+
161
+ def _fill_character(raw: Any) -> dict[str, Any]:
162
+ character = copy.deepcopy(raw) if isinstance(raw, dict) else {}
163
+ appearance = character.get("appearance") if isinstance(character.get("appearance"), dict) else {}
164
+ label = character.get("label") if isinstance(character.get("label"), dict) else {}
165
+ animation = character.get("animation") if isinstance(character.get("animation"), dict) else {}
166
+ character["placeholder"] = (
167
+ character["placeholder"] if isinstance(character.get("placeholder"), bool) else True
168
+ )
169
+ character["gender"] = character.get("gender") or "unknown"
170
+ if "motionId" not in character:
171
+ character["motionId"] = None
172
+ character["appearance"] = {**appearance, "color": appearance.get("color") or "#d7b5ee"}
173
+ character["label"] = {
174
+ **label,
175
+ "showLabel": label["showLabel"] if isinstance(label.get("showLabel"), bool) else True,
176
+ "scale": float(label["scale"]) if isinstance(label.get("scale"), (int, float)) else 1,
177
+ "yOffset": float(label["yOffset"]) if isinstance(label.get("yOffset"), (int, float)) else 0.04,
178
+ }
179
+ character["animation"] = {
180
+ **animation,
181
+ "mode": animation.get("mode") or "pose",
182
+ "controlValues": animation["controlValues"]
183
+ if isinstance(animation.get("controlValues"), dict) else {},
184
+ }
185
+ return character
186
+
187
+
188
+ def _fill_prop(raw: Any, category: str | None) -> dict[str, Any]:
189
+ prop = raw if isinstance(raw, dict) else {}
190
+ return {"category": prop.get("category") or category or "set"}
191
+
192
+
193
+ def normalize_node(raw: Any) -> dict[str, Any]:
194
+ if not isinstance(raw, dict):
195
+ raise ValueError("INVALID_DIRECTOR_NODE")
196
+ node = copy.deepcopy(raw)
197
+ node_id = str(node.get("id") or "").strip()
198
+ node_type = str(node.get("type") or "").strip()
199
+ if not node_id or node_type not in NODE_TYPES:
200
+ raise ValueError("INVALID_DIRECTOR_NODE")
201
+ node["id"] = node_id
202
+ node["type"] = node_type
203
+ node["name"] = str(node.get("name") or node_id)
204
+ node["visible"] = node["visible"] if isinstance(node.get("visible"), bool) else True
205
+ node["locked"] = node["locked"] if isinstance(node.get("locked"), bool) else False
206
+ node["transform"] = normalize_transform(node.get("transform"))
207
+ metadata = _merge_metadata(node)
208
+ if metadata:
209
+ node["metadata"] = metadata
210
+ node.pop("asset", None)
211
+ if node_type == "camera":
212
+ node["camera"] = _fill_camera(node.get("camera"))
213
+ elif node_type == "character":
214
+ node["character"] = _fill_character(node.get("character"))
215
+ elif node_type == "prop":
216
+ node["prop"] = _fill_prop(node.get("prop"), None)
217
+ elif node_type == "group":
218
+ group = node.get("group") if isinstance(node.get("group"), dict) else {}
219
+ appearance = group.get("appearance") if isinstance(group.get("appearance"), dict) else {}
220
+ node["group"] = {
221
+ "appearance": {"color": appearance.get("color") or "#888888"},
222
+ "kind": group.get("kind") or "group",
223
+ }
224
+ elif node_type == "primitive":
225
+ primitive = node.get("primitive") if isinstance(node.get("primitive"), dict) else {}
226
+ node["primitive"] = {
227
+ "kind": primitive.get("kind") or "cube",
228
+ "parameters": primitive["parameters"]
229
+ if isinstance(primitive.get("parameters"), dict) else {},
230
+ }
231
+ elif node_type == "path":
232
+ path = node.get("path") if isinstance(node.get("path"), dict) else {}
233
+ if not isinstance(path.get("points"), list) or not path["points"]:
234
+ raise ValueError("INVALID_DIRECTOR_PATH:points required")
235
+ node["path"] = {
236
+ "source": path.get("source") or "manual",
237
+ "curve": path.get("curve") or "catmullrom",
238
+ "closed": path["closed"] if isinstance(path.get("closed"), bool) else False,
239
+ "groundSnap": path["groundSnap"] if isinstance(path.get("groundSnap"), bool) else True,
240
+ "parameterization": path.get("parameterization") or "arcLength",
241
+ "smoothing": float(path["smoothing"]) if isinstance(path.get("smoothing"), (int, float)) else 0.5,
242
+ "points": path["points"],
243
+ }
244
+ return node
245
+
246
+
247
+ def node_from_library(
248
+ kind: str,
249
+ entry: dict[str, Any],
250
+ *,
251
+ node_id: str | None = None,
252
+ transform: dict[str, Any] | None = None,
253
+ ) -> dict[str, Any]:
254
+ library_id = str(entry.get("id") or "").strip()
255
+ if not library_id:
256
+ raise ValueError("DIRECTOR_LIBRARY_ENTRY_MISSING_ID")
257
+ resolved_id = str(node_id or library_id)
258
+ name = str(entry.get("name") or library_id)
259
+ file_key = entry.get("file")
260
+ if kind == "characters":
261
+ node = {
262
+ "id": resolved_id,
263
+ "type": "character",
264
+ "name": name,
265
+ "metadata": {"assetId": library_id, "modelUrl": file_key},
266
+ "character": _fill_character({}),
267
+ }
268
+ elif kind == "props":
269
+ node = {
270
+ "id": resolved_id,
271
+ "type": "prop",
272
+ "name": name,
273
+ "metadata": {"assetId": library_id, "modelUrl": file_key},
274
+ "prop": _fill_prop({}, entry.get("category") or "set"),
275
+ }
276
+ else:
277
+ raise ValueError("add_director_library_node only supports characters or props")
278
+ if transform:
279
+ node["transform"] = transform
280
+ return normalize_node(node)
281
+
282
+
283
+ def _fill_environment(raw: Any) -> dict[str, Any]:
284
+ environment = raw if isinstance(raw, dict) else {}
285
+ background = environment.get("background") if isinstance(environment.get("background"), dict) else {}
286
+ display = environment.get("display") if isinstance(environment.get("display"), dict) else {}
287
+ filled = {
288
+ "background": {
289
+ "mode": background.get("mode") or "color",
290
+ "skyColor": background.get("skyColor") or "#87CEEB",
291
+ },
292
+ "display": {
293
+ "characterLabelsVisible": display["characterLabelsVisible"]
294
+ if isinstance(display.get("characterLabelsVisible"), bool) else True,
295
+ "groundVisible": display["groundVisible"]
296
+ if isinstance(display.get("groundVisible"), bool) else True,
297
+ "groundHeight": float(display["groundHeight"])
298
+ if isinstance(display.get("groundHeight"), (int, float)) else 0,
299
+ "groundOpacity": float(display["groundOpacity"])
300
+ if isinstance(display.get("groundOpacity"), (int, float)) else 1,
301
+ },
302
+ }
303
+ for key, value in environment.items():
304
+ if key not in filled:
305
+ filled[key] = copy.deepcopy(value)
306
+ return filled
307
+
308
+
309
+ def _fill_timeline(raw: Any) -> dict[str, Any]:
310
+ timeline = raw if isinstance(raw, dict) else {}
311
+ animation = timeline.get("animation") if isinstance(timeline.get("animation"), dict) else {}
312
+ filled = {
313
+ "version": timeline.get("version") if isinstance(timeline.get("version"), (int, float)) else 1,
314
+ "fps": timeline.get("fps") if isinstance(timeline.get("fps"), (int, float)) else 24,
315
+ "frameStart": timeline.get("frameStart") if isinstance(timeline.get("frameStart"), (int, float)) else 0,
316
+ "frameEnd": timeline.get("frameEnd") if isinstance(timeline.get("frameEnd"), (int, float)) else 120,
317
+ "usePreviewRange": timeline["usePreviewRange"]
318
+ if isinstance(timeline.get("usePreviewRange"), bool) else False,
319
+ "animation": {
320
+ "fcurves": animation.get("fcurves") if isinstance(animation.get("fcurves"), list) else [],
321
+ "cameraMotionClips": animation.get("cameraMotionClips")
322
+ if isinstance(animation.get("cameraMotionClips"), list) else [],
323
+ "motionTransitions": animation.get("motionTransitions")
324
+ if isinstance(animation.get("motionTransitions"), list) else [],
325
+ "motionClips": animation.get("motionClips")
326
+ if isinstance(animation.get("motionClips"), list) else [],
327
+ "pathMotionClips": animation.get("pathMotionClips")
328
+ if isinstance(animation.get("pathMotionClips"), list) else [],
329
+ },
330
+ }
331
+ for key, value in animation.items():
332
+ if key not in filled["animation"]:
333
+ filled["animation"][key] = copy.deepcopy(value)
334
+ return filled
335
+
336
+
337
+ def normalize_document(raw: Any) -> dict[str, Any]:
338
+ if not isinstance(raw, dict):
339
+ raise ValueError("INVALID_DIRECTOR_DOCUMENT")
340
+ document = copy.deepcopy(raw)
341
+ document["type"] = document.get("type") or DIRECTOR_DOCUMENT_TYPE
342
+ if document["type"] != DIRECTOR_DOCUMENT_TYPE:
343
+ raise ValueError("INVALID_DIRECTOR_DOCUMENT")
344
+ document["pippitAssetId"] = str(document.get("pippitAssetId") or "director")
345
+ content = document.get("content") if isinstance(document.get("content"), dict) else {}
346
+ nodes = [normalize_node(node) for node in content.get("nodes") or [] if isinstance(node, dict)]
347
+ cameras = [node for node in nodes if node.get("type") == "camera"]
348
+ content["version"] = content.get("version") if isinstance(content.get("version"), (int, float)) else 1
349
+ content["aspectRatio"] = content.get("aspectRatio") or "16:9"
350
+ shot = content.get("activeShotCameraNodeId")
351
+ if isinstance(shot, str) and shot:
352
+ content["activeShotCameraNodeId"] = shot
353
+ elif cameras:
354
+ content["activeShotCameraNodeId"] = cameras[0]["id"]
355
+ else:
356
+ content.pop("activeShotCameraNodeId", None)
357
+ content["environment"] = _fill_environment(content.get("environment"))
358
+ content["asset"] = content.get("asset") if isinstance(content.get("asset"), dict) else {"motionPath": []}
359
+ content["asset"].setdefault("motionPath", [])
360
+ content["nodes"] = nodes
361
+ content["physicalConstraints"] = (
362
+ content.get("physicalConstraints")
363
+ if isinstance(content.get("physicalConstraints"), list) else []
364
+ )
365
+ content["timeline"] = _fill_timeline(content.get("timeline"))
366
+ document["content"] = content
367
+ return document
368
+
369
+
370
+ def validate_director_node(node: dict[str, Any]) -> dict[str, Any]:
371
+ document = empty_director_document()
372
+ document["content"]["nodes"] = [node]
373
+ document["content"]["activeShotCameraNodeId"] = (
374
+ node["id"] if node.get("type") == "camera"
375
+ else document["content"]["activeShotCameraNodeId"]
376
+ )
377
+ validate_director_document(document)
378
+ return node
379
+
380
+
381
+ def validate_director_document(document: dict[str, Any]) -> dict[str, Any]:
382
+ errors = sorted(_validator().iter_errors(document), key=lambda item: list(item.path))
383
+ if errors:
384
+ first = errors[0]
385
+ path = ".".join(str(part) for part in first.path) or "<root>"
386
+ raise ValueError(f"INVALID_DIRECTOR_DOCUMENT:{path}: {first.message}")
387
+ return document
388
+
389
+
390
+ def validate_fcurves(fcurves: dict[str, Any]) -> dict[str, Any]:
391
+ global _FCURVES_VALIDATOR
392
+ if _FCURVES_VALIDATOR is None:
393
+ _FCURVES_VALIDATOR = Draft7Validator(json.loads(FCURVES_SCHEMA_PATH.read_text(encoding="utf-8")))
394
+ errors = sorted(_FCURVES_VALIDATOR.iter_errors(fcurves), key=lambda item: list(item.path))
395
+ if errors:
396
+ first = errors[0]
397
+ path = ".".join(str(part) for part in first.path) or "<root>"
398
+ raise ValueError(f"INVALID_DIRECTOR_FCURVES:{path}: {first.message}")
399
+ return fcurves
400
+
401
+
402
+ _SCENE_ROOT_KEYS = {"type", "pippitAssetId", "extra", "content", "nodes"}
403
+ _CLIP_ARRAYS = ("cameraMotionClips", "motionClips", "pathMotionClips")
404
+
405
+
406
+ def _as_document(document: Any) -> dict[str, Any] | None:
407
+ if not isinstance(document, dict):
408
+ return None
409
+ if "content" in document or document.get("type") == DIRECTOR_DOCUMENT_TYPE:
410
+ return document
411
+ inner = document.get("document")
412
+ return inner if isinstance(inner, dict) else document
413
+
414
+
415
+ def _index_nodes(document: Any) -> dict[str, dict[str, Any]]:
416
+ resolved = _as_document(document) or {}
417
+ content = resolved.get("content") if isinstance(resolved, dict) else {}
418
+ nodes = content.get("nodes") if isinstance(content, dict) else []
419
+ return {
420
+ str(node["id"]): node
421
+ for node in nodes or []
422
+ if isinstance(node, dict) and node.get("id")
423
+ }
424
+
425
+
426
+ def preserve_node_metadata(node: dict[str, Any], existing: dict[str, Any] | None) -> dict[str, Any]:
427
+ if not existing:
428
+ return node
429
+ incoming_meta = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
430
+ existing_meta = existing.get("metadata") if isinstance(existing.get("metadata"), dict) else {}
431
+ merged = dict(existing_meta)
432
+ merged.update(incoming_meta)
433
+ if existing_meta.get("modelUrl") and not incoming_meta.get("modelUrl"):
434
+ merged["modelUrl"] = existing_meta["modelUrl"]
435
+ if existing_meta.get("assetId") and not incoming_meta.get("assetId"):
436
+ merged["assetId"] = existing_meta["assetId"]
437
+ if merged:
438
+ node["metadata"] = merged
439
+ return node
440
+
441
+
442
+ def scene_content_from_payload(scene: Any) -> dict[str, Any]:
443
+ if not isinstance(scene, dict):
444
+ raise ValueError("INVALID_DIRECTOR_SCENE")
445
+ content = scene.get("content") if isinstance(scene.get("content"), dict) else None
446
+ if "nodes" in scene or (content is not None and "nodes" in content):
447
+ raise ValueError("INVALID_DIRECTOR_SCENE:content cannot include nodes")
448
+ animation = {}
449
+ if content is not None:
450
+ timeline = content.get("timeline") if isinstance(content.get("timeline"), dict) else {}
451
+ animation = timeline.get("animation") if isinstance(timeline.get("animation"), dict) else {}
452
+ if any(key in animation for key in _CLIP_ARRAYS):
453
+ raise ValueError("INVALID_DIRECTOR_SCENE:content cannot include clips")
454
+ if content is not None:
455
+ return copy.deepcopy(content)
456
+ extracted = {
457
+ key: copy.deepcopy(value)
458
+ for key, value in scene.items()
459
+ if key not in _SCENE_ROOT_KEYS
460
+ }
461
+ return extracted
462
+
463
+
464
+ def apply_scene_set(document: dict[str, Any], scene: Any) -> dict[str, Any]:
465
+ incoming = scene_content_from_payload(scene)
466
+ current = copy.deepcopy(document) if isinstance(document, dict) else empty_director_document()
467
+ content = current.setdefault("content", {})
468
+ kept_nodes = copy.deepcopy(content.get("nodes") or [])
469
+ kept_timeline = copy.deepcopy(content.get("timeline") or {})
470
+ kept_animation = (kept_timeline.get("animation") if isinstance(kept_timeline, dict) else {}) or {}
471
+ for key, value in incoming.items():
472
+ if key == "timeline" and isinstance(value, dict):
473
+ merged_timeline = dict(kept_timeline)
474
+ incoming_animation = value.get("animation") if isinstance(value.get("animation"), dict) else {}
475
+ merged_animation = dict(kept_animation)
476
+ for anim_key, anim_value in incoming_animation.items():
477
+ if anim_key not in _CLIP_ARRAYS:
478
+ merged_animation[anim_key] = copy.deepcopy(anim_value)
479
+ merged_timeline.update({
480
+ item_key: copy.deepcopy(item_value)
481
+ for item_key, item_value in value.items()
482
+ if item_key != "animation"
483
+ })
484
+ merged_timeline["animation"] = merged_animation
485
+ content["timeline"] = merged_timeline
486
+ else:
487
+ content[key] = copy.deepcopy(value)
488
+ content["nodes"] = kept_nodes
489
+ if isinstance(scene, dict):
490
+ if scene.get("type"):
491
+ current["type"] = scene["type"]
492
+ if "pippitAssetId" in scene:
493
+ current["pippitAssetId"] = scene["pippitAssetId"]
494
+ if "extra" in scene:
495
+ current["extra"] = copy.deepcopy(scene["extra"])
496
+ current["content"] = content
497
+ return normalize_document(current)
498
+
499
+
500
+ def camera_clip_has_curves(clip: Any) -> bool:
501
+ if not isinstance(clip, dict):
502
+ return False
503
+ motion = clip.get("motion") if isinstance(clip.get("motion"), dict) else {}
504
+ curves = motion.get("curves")
505
+ return isinstance(curves, list) and bool(curves)
506
+
507
+
508
+ def stored_evaluate_body(
509
+ snapshot: Any,
510
+ frames: Any,
511
+ *,
512
+ width: int | None = None,
513
+ height: int | None = None,
514
+ ) -> dict[str, Any]:
515
+ if not isinstance(snapshot, dict) or not isinstance(snapshot.get("document"), dict):
516
+ raise ValueError("DIRECTOR_DOCUMENT_MISSING")
517
+ if not isinstance(frames, list) or not frames:
518
+ raise ValueError("INVALID_DIRECTOR_FRAMES")
519
+ document = prepare_put_document(snapshot["document"], current=snapshot["document"])
520
+ body: dict[str, Any] = {"document": document, "frames": list(frames)}
521
+ fcurves = snapshot.get("fcurves")
522
+ if isinstance(fcurves, dict) and fcurves.get("encoding") == "compact-v1":
523
+ body["fcurves"] = copy.deepcopy(fcurves)
524
+ if width is not None:
525
+ body["width"] = width
526
+ if height is not None:
527
+ body["height"] = height
528
+ return body
529
+
530
+
531
+ def _index_camera_clips(document: Any) -> dict[str, dict[str, Any]]:
532
+ resolved = _as_document(document) or {}
533
+ content = resolved.get("content") if isinstance(resolved, dict) else {}
534
+ timeline = content.get("timeline") if isinstance(content, dict) else {}
535
+ animation = timeline.get("animation") if isinstance(timeline, dict) else {}
536
+ clips = animation.get("cameraMotionClips") if isinstance(animation, dict) else []
537
+ return {
538
+ str(clip["id"]): clip
539
+ for clip in clips or []
540
+ if isinstance(clip, dict) and clip.get("id")
541
+ }
542
+
543
+
544
+ def _preserve_camera_motion_clips(document: dict[str, Any], current: Any) -> None:
545
+ existing = _index_camera_clips(current)
546
+ animation = document["content"].setdefault("timeline", {}).setdefault("animation", {})
547
+ kept: list[dict[str, Any]] = []
548
+ for clip in animation.get("cameraMotionClips") or []:
549
+ if not isinstance(clip, dict):
550
+ continue
551
+ if camera_clip_has_curves(clip):
552
+ kept.append(clip)
553
+ elif existing.get(str(clip.get("id") or "")) and camera_clip_has_curves(existing[str(clip["id"])]):
554
+ kept.append(copy.deepcopy(existing[str(clip["id"])]))
555
+ animation["cameraMotionClips"] = kept
556
+
557
+
558
+ def prepare_put_document(document: Any, current: Any = None) -> dict[str, Any]:
559
+ normalized = normalize_document(document)
560
+ existing = _index_nodes(current)
561
+ incoming_ids = {node["id"] for node in normalized["content"]["nodes"]}
562
+ for node in normalized["content"]["nodes"]:
563
+ preserve_node_metadata(node, existing.get(node["id"]))
564
+ if not any(node.get("type") == "camera" for node in normalized["content"]["nodes"]):
565
+ for node_id, node in existing.items():
566
+ if node.get("type") == "camera" and node_id not in incoming_ids:
567
+ normalized["content"]["nodes"].insert(0, copy.deepcopy(node))
568
+ if not normalized["content"].get("activeShotCameraNodeId"):
569
+ current_doc = _as_document(current) or {}
570
+ current_shot = (current_doc.get("content") or {}).get("activeShotCameraNodeId")
571
+ cameras = [node for node in normalized["content"]["nodes"] if node.get("type") == "camera"]
572
+ normalized["content"]["activeShotCameraNodeId"] = (
573
+ current_shot if isinstance(current_shot, str) and current_shot
574
+ else (cameras[0]["id"] if cameras else "cam-main")
575
+ )
576
+ _preserve_camera_motion_clips(normalized, current)
577
+ return validate_director_document(normalized)
578
+
579
+
580
+ def summarize_editorial(document: Any) -> dict[str, Any]:
581
+ resolved = _as_document(document) or {}
582
+ content = resolved.get("content") if isinstance(resolved, dict) else {}
583
+ editorial = content.get("editorial") if isinstance(content, dict) else None
584
+ if not isinstance(editorial, dict) or not isinstance(editorial.get("sequences"), list):
585
+ editorial = {
586
+ "version": 1,
587
+ "activeSequenceId": "sequence_1",
588
+ "sequences": [{"id": "sequence_1", "clips": []}],
589
+ }
590
+ sequences = []
591
+ for sequence in editorial.get("sequences") or []:
592
+ if not isinstance(sequence, dict):
593
+ continue
594
+ clips = []
595
+ duration = 0
596
+ for clip in sequence.get("clips") or []:
597
+ if not isinstance(clip, dict):
598
+ continue
599
+ start = clip.get("sourceFrameStart")
600
+ end = clip.get("sourceFrameEnd")
601
+ if isinstance(start, int) and isinstance(end, int) and end >= start:
602
+ duration += end - start + 1
603
+ clips.append({
604
+ "id": clip.get("id"),
605
+ "cameraNodeId": clip.get("cameraNodeId"),
606
+ "sourceFrameStart": start,
607
+ "sourceFrameEnd": end,
608
+ })
609
+ item = {"id": sequence.get("id"), "durationFrames": duration, "clips": clips}
610
+ if sequence.get("name"):
611
+ item["name"] = sequence["name"]
612
+ sequences.append(item)
613
+ return {
614
+ "version": editorial.get("version") or 1,
615
+ "activeSequenceId": editorial.get("activeSequenceId") or (
616
+ sequences[0]["id"] if sequences else "sequence_1"
617
+ ),
618
+ "sequences": sequences,
619
+ }
620
+
621
+
622
+ def summarize_director_snapshot(snapshot: Any) -> dict[str, Any]:
623
+ if not isinstance(snapshot, dict):
624
+ raise ValueError("DIRECTOR_DOCUMENT_MISSING")
625
+ document = snapshot.get("document") if isinstance(snapshot.get("document"), dict) else snapshot
626
+ content = document.get("content") if isinstance(document, dict) else {}
627
+ nodes = content.get("nodes") if isinstance(content, dict) else []
628
+ outlined = []
629
+ cameras = []
630
+ for node in nodes or []:
631
+ if not isinstance(node, dict):
632
+ continue
633
+ metadata = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
634
+ outlined.append({
635
+ "id": node.get("id"),
636
+ "type": node.get("type"),
637
+ "name": node.get("name"),
638
+ "visible": node.get("visible"),
639
+ "locked": node.get("locked"),
640
+ "assetId": metadata.get("assetId"),
641
+ })
642
+ if node.get("type") == "camera" and node.get("id"):
643
+ cameras.append(node["id"])
644
+ timeline = content.get("timeline") if isinstance(content.get("timeline"), dict) else {}
645
+ animation = timeline.get("animation") if isinstance(timeline.get("animation"), dict) else {}
646
+ clips = []
647
+ for clip_kind, key in (
648
+ ("cameraMotion", "cameraMotionClips"),
649
+ ("motion", "motionClips"),
650
+ ("pathMotion", "pathMotionClips"),
651
+ ):
652
+ for clip in animation.get(key) or []:
653
+ if not isinstance(clip, dict):
654
+ continue
655
+ target = clip.get("target") if isinstance(clip.get("target"), dict) else {}
656
+ clips.append({
657
+ "id": clip.get("id"),
658
+ "clipKind": clip_kind,
659
+ "nodeId": target.get("nodeId"),
660
+ "frameStart": clip.get("frameStart"),
661
+ "frameEnd": clip.get("frameEnd"),
662
+ })
663
+ fcurves = snapshot.get("fcurves") if isinstance(snapshot.get("fcurves"), dict) else {}
664
+ fcurve_ids = []
665
+ for item in fcurves.get("fcurves") or []:
666
+ node_id = fcurve_node_id(item)
667
+ if node_id:
668
+ entity_id = "fcurves__" + node_id
669
+ if entity_id not in fcurve_ids:
670
+ fcurve_ids.append(entity_id)
671
+ return {
672
+ "suggestedMode": "new" if len(cameras) == 1 and len(outlined) == 1 else "edit",
673
+ "activeCameraNodeId": content.get("activeShotCameraNodeId"),
674
+ "sceneSequence": snapshot.get("sceneSequence"),
675
+ "entityVersions": snapshot.get("entityVersions") or {},
676
+ "timeline": {
677
+ "fps": timeline.get("fps"),
678
+ "frameStart": timeline.get("frameStart"),
679
+ "frameEnd": timeline.get("frameEnd"),
680
+ },
681
+ "nodes": outlined,
682
+ "cameras": cameras,
683
+ "clips": clips,
684
+ "fcurvesNodeIds": fcurve_ids,
685
+ "editorial": summarize_editorial(document),
686
+ "message": (
687
+ "Outline only: curves and full nodes omitted. "
688
+ "Use get_director_entity for one node/clip. "
689
+ "Do not hand-edit this outline."
690
+ ),
691
+ }
692
+
693
+
694
+ def get_director_entity(snapshot: Any, entity_type: str, entity_id: str, *, include_curves: bool = False) -> dict[str, Any]:
695
+ if not isinstance(snapshot, dict):
696
+ raise ValueError("DIRECTOR_DOCUMENT_MISSING")
697
+ document = snapshot.get("document") if isinstance(snapshot.get("document"), dict) else {}
698
+ content = document.get("content") if isinstance(document, dict) else {}
699
+ if entity_type == "node":
700
+ for node in content.get("nodes") or []:
701
+ if isinstance(node, dict) and node.get("id") == entity_id:
702
+ return {"entityType": "node", "entity": copy.deepcopy(node)}
703
+ raise ValueError(f"DIRECTOR_ENTITY_NOT_FOUND:node:{entity_id}")
704
+ if entity_type == "clip":
705
+ animation = ((content.get("timeline") or {}).get("animation")
706
+ if isinstance(content.get("timeline"), dict) else {})
707
+ for key in ("cameraMotionClips", "motionClips", "pathMotionClips"):
708
+ for clip in (animation or {}).get(key) or []:
709
+ if not isinstance(clip, dict) or clip.get("id") != entity_id:
710
+ continue
711
+ body = copy.deepcopy(clip)
712
+ if not include_curves:
713
+ motion = body.get("motion") if isinstance(body.get("motion"), dict) else None
714
+ if motion and "curves" in motion:
715
+ motion = dict(motion)
716
+ motion.pop("curves", None)
717
+ motion["curvesOmitted"] = True
718
+ body["motion"] = motion
719
+ return {"entityType": "clip", "entity": body, "includeCurves": include_curves}
720
+ raise ValueError(f"DIRECTOR_ENTITY_NOT_FOUND:clip:{entity_id}")
721
+ if entity_type == "fcurves":
722
+ node_id = entity_id.removeprefix("fcurves__") if entity_id.startswith("fcurves__") else entity_id
723
+ matched = []
724
+ fcurves = snapshot.get("fcurves") if isinstance(snapshot.get("fcurves"), dict) else {}
725
+ for item in fcurves.get("fcurves") or []:
726
+ if fcurve_node_id(item) == node_id:
727
+ matched.append(copy.deepcopy(item) if include_curves else {
728
+ "nodeId": node_id,
729
+ "channel": item.get("p", item.get("channel")),
730
+ "index": item.get("i"),
731
+ "keyCount": len(item.get("k") or item.get("keys") or []),
732
+ })
733
+ if not matched:
734
+ raise ValueError(f"DIRECTOR_ENTITY_NOT_FOUND:fcurves:{entity_id}")
735
+ return {"entityType": "fcurves", "entityId": f"fcurves__{node_id}", "entity": matched}
736
+ raise ValueError("INVALID_DIRECTOR_ENTITY_TYPE")
737
+
738
+
739
+ _EVAL_POSE_KEYS = frozenset({
740
+ "camera", "lookAt", "position", "rotation", "scale", "transform",
741
+ })
742
+
743
+
744
+ def summarize_evaluate_frames(result: Any) -> dict[str, Any]:
745
+ if not isinstance(result, dict):
746
+ raise ValueError("DIRECTOR_EVALUATE_RESULT_INVALID")
747
+ summary: dict[str, Any] = {}
748
+ if "ok" in result:
749
+ summary["ok"] = result["ok"]
750
+ if "issues" in result:
751
+ summary["issues"] = copy.deepcopy(result["issues"])
752
+ frames: list[dict[str, Any]] = []
753
+ for item in result.get("frames") or []:
754
+ if not isinstance(item, dict):
755
+ continue
756
+ frame: dict[str, Any] = {}
757
+ if "frame" in item:
758
+ frame["frame"] = item["frame"]
759
+ for key, value in item.items():
760
+ if key == "frame" or key in _EVAL_POSE_KEYS:
761
+ continue
762
+ if isinstance(value, bool) or value is None:
763
+ frame[key] = value
764
+ elif isinstance(value, str) and len(value) <= 120:
765
+ frame[key] = value
766
+ elif isinstance(value, (int, float)) and not isinstance(value, bool):
767
+ frame[key] = value
768
+ elif isinstance(value, dict):
769
+ flags = {
770
+ flag_key: flag_value
771
+ for flag_key, flag_value in value.items()
772
+ if flag_key not in _EVAL_POSE_KEYS
773
+ and (
774
+ isinstance(flag_value, bool)
775
+ or (isinstance(flag_value, str) and len(flag_value) <= 80)
776
+ )
777
+ }
778
+ if flags:
779
+ frame[key] = flags
780
+ if isinstance(item.get("nodes"), list):
781
+ frame["nodes"] = [{k: copy.deepcopy(v) for k, v in node.items()
782
+ if k in {"nodeId", "type", "inFrustum", "cameraDistance", "groundPenetration", "finite", "positiveScale", "visible", "animated"}}
783
+ for node in item["nodes"] if isinstance(node, dict)]
784
+ frames.append(frame)
785
+ summary["frames"] = frames
786
+ for key, value in result.items():
787
+ if key in summary or key == "frames":
788
+ continue
789
+ if isinstance(value, (bool, int, float, str)) or value is None:
790
+ summary[key] = value
791
+ return summary
792
+
793
+