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,134 @@
1
+ """Stored-document helpers for static tools; never accept a caller's replacement scene."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import math
6
+
7
+ from topview_3d_cli.director_document import fcurve_node_id, new_scene_operation_id
8
+
9
+
10
+ def node_in(document: dict, node_id: str) -> dict:
11
+ node = next((n for n in document['content']['nodes'] if n['id'] == node_id), None)
12
+ if node is None:
13
+ raise ValueError(f'DIRECTOR_NODE_NOT_FOUND:{node_id}')
14
+ return node
15
+
16
+
17
+ def camera_view(body: dict, camera_id: str | None) -> dict:
18
+ if camera_id:
19
+ camera = node_in(body['document'], camera_id)
20
+ if camera.get('type') != 'camera':
21
+ raise ValueError('DIRECTOR_CAMERA_REQUIRED')
22
+ body['document']['content']['activeShotCameraNodeId'] = camera_id
23
+ body['cameraNodeId'] = camera_id
24
+ return body
25
+
26
+
27
+ def _detach_subject(camera: dict, subject: dict):
28
+ binding = camera['camera'].pop('subject')
29
+ if not binding.get('follow'):
30
+ return
31
+ rotation = subject['transform']['rotation']
32
+ yaw = math.atan2(math.sin(math.radians(rotation['y'])),
33
+ math.cos(math.radians(rotation['x'])) * math.cos(math.radians(rotation['y']))) \
34
+ if binding.get('followRotation') else 0
35
+ position = subject['transform']['position']
36
+ def world(offset):
37
+ return {'x': position['x'] + offset['x'] * math.cos(yaw) + offset['z'] * math.sin(yaw),
38
+ 'y': position['y'] + offset['y'],
39
+ 'z': position['z'] - offset['x'] * math.sin(yaw) + offset['z'] * math.cos(yaw)}
40
+ camera['transform']['position'] = world(binding['offset'])
41
+ camera['camera']['lookAt'] = world(binding['lookAtOffset'])
42
+
43
+
44
+ def delete_node_operations(snapshot: dict, node_id: str) -> tuple[dict, dict]:
45
+ document = snapshot['document']
46
+ target = node_in(document, node_id)
47
+ if target.get('locked'):
48
+ raise ValueError(f'DIRECTOR_NODE_LOCKED:{node_id}')
49
+ nodes = document['content']['nodes']
50
+ cameras = [n for n in nodes if n['type'] == 'camera' and n['id'] != node_id]
51
+ if target['type'] == 'camera' and not cameras:
52
+ raise ValueError('DIRECTOR_LAST_CAMERA_REQUIRED')
53
+ if target.get('children'):
54
+ raise ValueError('DIRECTOR_DELETE_CHILDREN_FIRST')
55
+ sequence = snapshot.get('sceneSequence', 0)
56
+ versions = snapshot.get('entityVersions') or {}
57
+ operations = []
58
+ def add(kind, entity_id, payload):
59
+ operations.append({'kind': kind, 'entityId': entity_id, 'payload': payload,
60
+ 'operationId': new_scene_operation_id('static_delete'),
61
+ 'baseSequence': sequence, 'expectedEntityVersion': versions.get(entity_id)})
62
+ add('director.node.delete', node_id, {})
63
+ detached = []
64
+ for original in nodes:
65
+ if original['id'] == node_id:
66
+ continue
67
+ node = copy.deepcopy(original)
68
+ if node.get('camera', {}).get('subject', {}).get('nodeId') == node_id:
69
+ _detach_subject(node, target)
70
+ detached.append(node['id'])
71
+ if node.get('camera', {}).get('lookAtTarget', {}).get('nodeId') == node_id:
72
+ look_target = node['camera'].pop('lookAtTarget')
73
+ offset = look_target.get('offset') or {'x': 0, 'y': 1.2, 'z': 0}
74
+ node['camera']['lookAt'] = {
75
+ axis: target['transform']['position'][axis] + offset[axis]
76
+ for axis in ('x', 'y', 'z')}
77
+ if node['id'] not in detached:
78
+ detached.append(node['id'])
79
+ if node_id in node.get('children', []):
80
+ node['children'] = [n for n in node['children'] if n != node_id]
81
+ if node != original:
82
+ add('director.node.upsert', node['id'], {'node': node})
83
+ content = document['content']
84
+ removed_clips = []
85
+ animation = content['timeline']['animation']
86
+ for key in ('cameraMotionClips', 'motionClips', 'pathMotionClips'):
87
+ for clip in animation.get(key, []):
88
+ if clip.get('target', {}).get('nodeId') == node_id or clip.get('pathNodeId') == node_id:
89
+ add('director.clip.delete', clip['id'], {})
90
+ removed_clips.append(clip['id'])
91
+ curves_id = 'fcurves__' + node_id
92
+ curves = snapshot.get('fcurves') or {}
93
+ if curves_id in versions or curves_id in curves or any(fcurve_node_id(c) == node_id for c in curves.get('fcurves', [])):
94
+ add('director.fcurves.delete', curves_id, {})
95
+ # Scene-level patch preserves unrelated environment/timeline data.
96
+ scene = {}
97
+ animation_patch = {}
98
+ for key in ('fcurves', 'motionTransitions'):
99
+ original = animation.get(key, [])
100
+ kept_entries = [entry for entry in original if not (
101
+ isinstance(entry, dict) and (
102
+ entry.get('nodeId') == node_id
103
+ or entry.get('targetNodeId') == node_id
104
+ or (isinstance(entry.get('target'), dict) and entry['target'].get('nodeId') == node_id)
105
+ or any(entry.get(field) in removed_clips for field in ('fromClipId', 'toClipId'))))]
106
+ if kept_entries != original:
107
+ animation_patch[key] = kept_entries
108
+ if animation_patch:
109
+ # scene.set replaces content.timeline as a whole, so send the full timeline.
110
+ timeline = copy.deepcopy(content['timeline'])
111
+ timeline['animation'] = {
112
+ key: value for key, value in timeline['animation'].items()
113
+ if key not in ('cameraMotionClips', 'motionClips', 'pathMotionClips')}
114
+ timeline['animation'].update(animation_patch)
115
+ scene['timeline'] = timeline
116
+ if content.get('activeShotCameraNodeId') == node_id:
117
+ scene['activeShotCameraNodeId'] = cameras[0]['id']
118
+ editorial = copy.deepcopy(content.get('editorial'))
119
+ if isinstance(editorial, dict):
120
+ for sequence_item in editorial.get('sequences', []):
121
+ sequence_item['clips'] = [c for c in sequence_item.get('clips', []) if c.get('cameraNodeId') != node_id]
122
+ if editorial != content['editorial']:
123
+ scene['editorial'] = editorial
124
+ constraints = content.get('physicalConstraints', [])
125
+ kept = [c for c in constraints if not (isinstance(c, dict) and any(
126
+ c.get(key) == node_id for key in ('nodeId', 'targetNodeId', 'sourceNodeId', 'supportNodeId')))]
127
+ if kept != constraints:
128
+ scene['physicalConstraints'] = kept
129
+ if scene:
130
+ add('director.scene.set', 'director', {'scene': {'content': {'version': 1, **scene}}})
131
+ if len(operations) > 64:
132
+ raise ValueError('DIRECTOR_DELETE_TOO_MANY_DEPENDENCIES')
133
+ return ({'schemaVersion': 2, 'batchId': new_scene_operation_id('delete_node'), 'operations': operations},
134
+ {'deletedNodeId': node_id, 'deletedClipIds': removed_clips, 'detachedCameraIds': detached})
@@ -0,0 +1,248 @@
1
+ """Offline asset layer: built-in and project asset manifests.
2
+
3
+ A manifest (``manifest.json``, format ``scene3d-asset-manifest`` v1) lists assets. File-backed
4
+ assets (character, prop, pose) map the asset ``key`` used in director documents
5
+ (``metadata.modelUrl``) to a file next to the manifest; primitives are code-generated and carry
6
+ their geometry. The renderer only serves files listed here, so nothing is fetched remotely.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import copy
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ import tempfile
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from topview_3d_cli.local_errors import LocalProjectError
21
+ from topview_3d_cli.local_project import ProjectPaths
22
+ from topview_3d_cli.runtime import runtime
23
+
24
+ MANIFEST_FORMAT = "scene3d-asset-manifest"
25
+ MANIFEST_VERSION = 1
26
+ MANIFEST_NAME = "manifest.json"
27
+ ASSET_KINDS = ("character", "prop", "pose", "primitive")
28
+ FILE_KINDS = {"character": (".glb", ".gltf"), "prop": (".glb", ".gltf"), "pose": (".json",)}
29
+ COVER_TYPES = (".webp", ".png", ".jpg", ".jpeg")
30
+ PRIMITIVE_KINDS = ("BoxGeometry", "SphereGeometry", "CylinderGeometry", "ConeGeometry", "PlaneGeometry")
31
+ ASSET_ID = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
32
+
33
+
34
+ def builtin_asset_root() -> Path:
35
+ override = os.environ.get("TOPVIEW3D_BUILTIN_ASSETS")
36
+ return Path(override).expanduser().resolve() if override else runtime().builtin_assets
37
+
38
+
39
+ def _invalid(root: Path, message: str) -> LocalProjectError:
40
+ return LocalProjectError("ASSET_MANIFEST_INVALID", f"{root / MANIFEST_NAME}: {message}")
41
+
42
+
43
+ def empty_manifest() -> dict[str, Any]:
44
+ return {"format": MANIFEST_FORMAT, "version": MANIFEST_VERSION, "assets": []}
45
+
46
+
47
+ def asset_key(raw: Any) -> str:
48
+ return str(raw or "").lstrip("/").split("?")[0]
49
+
50
+
51
+ def _inside(root: Path, relative: str) -> Path | None:
52
+ if not isinstance(relative, str) or not relative or Path(relative).is_absolute():
53
+ return None
54
+ if any(part in ("..", ".") for part in re.split(r"[\\/]+", relative)):
55
+ return None
56
+ resolved = (root / relative).resolve()
57
+ return resolved if resolved.is_relative_to(root.resolve()) else None
58
+
59
+
60
+ def read_manifest(root: Path) -> dict[str, Any]:
61
+ path = root / MANIFEST_NAME
62
+ if not path.is_file():
63
+ return empty_manifest()
64
+ try:
65
+ manifest = json.loads(path.read_text(encoding="utf-8"))
66
+ except json.JSONDecodeError as exc:
67
+ raise _invalid(root, exc.msg) from exc
68
+ if not isinstance(manifest, dict) or manifest.get("format") != MANIFEST_FORMAT:
69
+ raise _invalid(root, f"format must be {MANIFEST_FORMAT!r}")
70
+ if manifest.get("version") != MANIFEST_VERSION:
71
+ raise _invalid(root, f"unsupported version {manifest.get('version')!r}")
72
+ if not isinstance(manifest.get("assets"), list):
73
+ raise _invalid(root, "assets must be a list")
74
+ return manifest
75
+
76
+
77
+ def load_assets(root: Path, origin: str) -> list[dict[str, Any]]:
78
+ """Validated manifest entries with an absolute ``path`` for file-backed assets."""
79
+ entries = []
80
+ seen: set[str] = set()
81
+ for index, raw in enumerate(read_manifest(root)["assets"]):
82
+ where = f"assets[{index}]"
83
+ if not isinstance(raw, dict):
84
+ raise _invalid(root, f"{where} must be an object")
85
+ entry = copy.deepcopy(raw)
86
+ asset_id, kind = entry.get("id"), entry.get("kind")
87
+ if not isinstance(asset_id, str) or not ASSET_ID.match(asset_id) or asset_id in seen:
88
+ raise _invalid(root, f"{where}.id missing, malformed or duplicated")
89
+ seen.add(asset_id)
90
+ if kind not in ASSET_KINDS:
91
+ raise _invalid(root, f"{where}.kind must be one of {', '.join(ASSET_KINDS)}")
92
+ if kind == "primitive":
93
+ primitive = entry.get("primitive")
94
+ if (not isinstance(primitive, dict) or primitive.get("kind") not in PRIMITIVE_KINDS
95
+ or not isinstance(primitive.get("parameters"), dict)):
96
+ raise _invalid(root, f"{where}.primitive must name a supported geometry with parameters")
97
+ else:
98
+ path = _inside(root, entry.get("file"))
99
+ if path is None:
100
+ raise _invalid(root, f"{where}.file must be a relative path inside the asset root")
101
+ if not path.is_file():
102
+ raise LocalProjectError("ASSET_FILE_MISSING", f"{asset_id}: {path}")
103
+ if isinstance(entry.get("bytes"), int) and path.stat().st_size != entry["bytes"]:
104
+ raise _invalid(root, f"{where}: {path.name} size does not match bytes")
105
+ entry["path"] = str(path)
106
+ if kind in ("character", "prop") and not asset_key(entry.get("key")):
107
+ raise _invalid(root, f"{where}.key is required for {kind} assets")
108
+ if entry.get("cover") is not None:
109
+ cover = _inside(root, entry["cover"])
110
+ if cover is None or not cover.is_file():
111
+ raise _invalid(root, f"{where}.cover must be an existing file inside the asset root")
112
+ entry["origin"] = origin
113
+ entries.append(entry)
114
+ return entries
115
+
116
+
117
+ def asset_catalog(paths: ProjectPaths | None = None) -> list[dict[str, Any]]:
118
+ """Built-in assets, overridden by the project's own assets (same id or key)."""
119
+ catalog = {entry["id"]: entry for entry in load_assets(builtin_asset_root(), "builtin")}
120
+ if paths is not None:
121
+ for entry in load_assets(paths.assets, "project"):
122
+ catalog.pop(entry["id"], None)
123
+ catalog[entry["id"]] = entry
124
+ return list(catalog.values())
125
+
126
+
127
+ def local_asset_map(catalog: list[dict[str, Any]]) -> dict[str, str]:
128
+ """Asset key -> absolute file for the renderer (characters and props)."""
129
+ mapping: dict[str, str] = {}
130
+ for entry in catalog:
131
+ if entry["kind"] in ("character", "prop"):
132
+ mapping[asset_key(entry["key"])] = entry["path"]
133
+ return mapping
134
+
135
+
136
+ def find_asset(catalog: list[dict[str, Any]], kind: str, asset_id: str) -> dict[str, Any]:
137
+ """Look an asset up by id; library poses also match without their ``a3d_pose_`` prefix."""
138
+ candidates = {asset_id, f"a3d_pose_{asset_id}"} if kind == "pose" else {asset_id}
139
+ for entry in catalog:
140
+ if entry["kind"] == kind and entry["id"] in candidates:
141
+ return entry
142
+ raise LocalProjectError("ASSET_NOT_FOUND", f"no {kind} asset {asset_id!r} in the local manifests")
143
+
144
+
145
+ def public_asset_entry(entry: dict[str, Any]) -> dict[str, Any]:
146
+ return {key: value for key, value in entry.items() if key != "path"}
147
+
148
+
149
+ def document_asset_refs(document: dict[str, Any]) -> dict[str, list[dict[str, str]]]:
150
+ """Asset references of a director document: required models and optional motions."""
151
+ content = document.get("content") or {}
152
+ required = []
153
+ for node in content.get("nodes") or []:
154
+ if node.get("type") not in ("character", "prop"):
155
+ continue
156
+ metadata = node.get("metadata") or {}
157
+ if node["type"] == "character" and metadata.get("assetSource") == "user" and metadata.get("assetId"):
158
+ required.append({"nodeId": node["id"], "key": f"user:{metadata['assetId']}"})
159
+ continue
160
+ key = asset_key(metadata.get("modelUrl"))
161
+ if key:
162
+ required.append({"nodeId": node["id"], "key": key})
163
+ motion_ids = {clip.get("motion", {}).get("assetId")
164
+ for clip in ((content.get("timeline") or {}).get("animation") or {}).get("motionClips") or []}
165
+ motions = [{"assetId": entry.get("id"), "key": asset_key(entry.get("path"))}
166
+ for entry in ((content.get("asset") or {}).get("motionPath") or [])
167
+ if entry.get("id") in motion_ids]
168
+ return {"required": required, "motions": motions}
169
+
170
+
171
+ def check_render_assets(document: dict[str, Any], mapping: dict[str, str],
172
+ public_asset_base: str = "") -> list[str]:
173
+ """Raise ASSET_NOT_AVAILABLE for missing models; return warnings for missing motions."""
174
+ refs = document_asset_refs(document)
175
+ def available(key: str) -> bool:
176
+ return (key in mapping or re.match(r"^https?://", key, re.I) is not None
177
+ or (bool(public_asset_base) and key.startswith("3d-builder/public/")))
178
+ missing = [ref for ref in refs["required"] if not available(ref["key"])]
179
+ if missing:
180
+ raise LocalProjectError(
181
+ "ASSET_NOT_AVAILABLE",
182
+ "models not available offline: " + ", ".join(f"{m['nodeId']} ({m['key']})" for m in missing),
183
+ details={"missing": missing})
184
+ return [f"MOTION_NOT_AVAILABLE:{motion['assetId']}" for motion in refs["motions"]
185
+ if not available(motion["key"])]
186
+
187
+
188
+ def _sha256(path: Path) -> str:
189
+ digest = hashlib.sha256()
190
+ with path.open("rb") as stream:
191
+ for chunk in iter(lambda: stream.read(1 << 20), b""):
192
+ digest.update(chunk)
193
+ return digest.hexdigest()
194
+
195
+
196
+ def import_asset(root: Path, source: str | os.PathLike[str], *, kind: str, asset_id: str,
197
+ key: str | None = None, cover: str | os.PathLike[str] | None = None,
198
+ name: str | None = None, category: str | None = None,
199
+ tags: list[str] | None = None, rig: str | None = None,
200
+ license_name: str | None = None, source_url: str | None = None) -> dict[str, Any]:
201
+ """Copy one file into ``root`` and add or replace its manifest entry."""
202
+ if kind not in FILE_KINDS:
203
+ raise LocalProjectError("ASSET_IMPORT_INVALID", f"kind must be one of {', '.join(FILE_KINDS)}")
204
+ if not ASSET_ID.match(asset_id or ""):
205
+ raise LocalProjectError("ASSET_IMPORT_INVALID", "id must match [A-Za-z0-9_-]{1,128}")
206
+ file = Path(source).expanduser()
207
+ if not file.is_file():
208
+ raise LocalProjectError("ASSET_IMPORT_INVALID", f"{file} is not a file")
209
+ if file.suffix.lower() not in FILE_KINDS[kind]:
210
+ raise LocalProjectError("ASSET_IMPORT_INVALID", f"{kind} files must be {', '.join(FILE_KINDS[kind])}")
211
+ if kind == "pose":
212
+ try:
213
+ pose = json.loads(file.read_text(encoding="utf-8"))
214
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
215
+ raise LocalProjectError("ASSET_IMPORT_INVALID", f"pose is not JSON: {exc}") from exc
216
+ if not isinstance(pose, dict) or not isinstance(pose.get("bones"), dict):
217
+ raise LocalProjectError("ASSET_IMPORT_INVALID", "pose JSON must contain a bones object")
218
+ manifest = read_manifest(root)
219
+ relative = f"{kind}s/{asset_id}{file.suffix.lower()}"
220
+ entry: dict[str, Any] = {"id": asset_id, "kind": kind, "name": name or asset_id, "file": relative,
221
+ "bytes": file.stat().st_size, "sha256": _sha256(file)}
222
+ if kind in ("character", "prop"):
223
+ entry["key"] = asset_key(key) or f"local/{relative}"
224
+ elif key:
225
+ entry["key"] = asset_key(key)
226
+ cover_file = Path(cover).expanduser() if cover else None
227
+ if cover_file is not None:
228
+ if not cover_file.is_file() or cover_file.suffix.lower() not in COVER_TYPES:
229
+ raise LocalProjectError("ASSET_IMPORT_INVALID", f"cover must be one of {', '.join(COVER_TYPES)}")
230
+ entry["cover"] = f"covers/{asset_id}{cover_file.suffix.lower()}"
231
+ for field, value in (("category", category), ("tags", tags), ("rig", rig),
232
+ ("license", license_name), ("source", source_url)):
233
+ if value:
234
+ entry[field] = value
235
+ target = root / relative
236
+ target.parent.mkdir(parents=True, exist_ok=True)
237
+ shutil.copyfile(file, target)
238
+ if cover_file is not None:
239
+ (root / "covers").mkdir(parents=True, exist_ok=True)
240
+ shutil.copyfile(cover_file, root / entry["cover"])
241
+ manifest["assets"] = [item for item in manifest["assets"] if item.get("id") != asset_id] + [entry]
242
+ descriptor, temporary = tempfile.mkstemp(prefix=".manifest.", suffix=".tmp", dir=root)
243
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
244
+ json.dump(manifest, stream, ensure_ascii=False, indent=2)
245
+ stream.write("\n")
246
+ os.chmod(temporary, 0o644)
247
+ os.replace(temporary, root / MANIFEST_NAME)
248
+ return entry
@@ -0,0 +1,108 @@
1
+ """``.topview3d/bom.json``: the plan / constraint record kept next to the director document."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ from topview_3d_cli.bom import (
8
+ derive_verification, encode_bom, merge_checkpoint, normalize_bom, observed_from_snapshot,
9
+ record_runtime_verification,
10
+ )
11
+ from topview_3d_cli.local_errors import LocalProjectError
12
+ from topview_3d_cli.local_project import Project, utc_timestamp, atomic_write_json, base_result, open_project
13
+
14
+ _CHECKPOINT_CODES = {
15
+ "SCENE_SEQUENCE_CONFLICT": "SCENE_SEQUENCE_CONFLICT",
16
+ "BOM_REVISION_CONFLICT": "BOM_REVISION_CONFLICT",
17
+ "BOM_TOO_LARGE": "BOM_TOO_LARGE",
18
+ "BOM_REVIEW_REQUIRES_CURRENT_RUNTIME_EVIDENCE": "BOM_REVIEW_EVIDENCE_MISSING",
19
+ "BOM_REVIEW_IMAGE_MISMATCH": "BOM_REVIEW_EVIDENCE_MISSING",
20
+ }
21
+
22
+
23
+ def read_bom(project: Project) -> dict[str, Any]:
24
+ """Stored record in the current shape; a missing file is an empty BOM."""
25
+ try:
26
+ raw = json.loads(project.paths.bom.read_text(encoding="utf-8"))
27
+ except FileNotFoundError:
28
+ raw = None
29
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
30
+ raise LocalProjectError("BOM_JSON_INVALID", f"{project.paths.bom}: not JSON; fix or delete it") from exc
31
+ if raw is not None and not isinstance(raw, dict):
32
+ raise LocalProjectError("BOM_JSON_INVALID", f"{project.paths.bom} must contain an object")
33
+ return normalize_bom(raw, project.paths.root.name)
34
+
35
+
36
+ # Node references a checkpoint may carry, per entry list.
37
+ _NODE_REFERENCES = {"relationships": ("subjectId", "targetId"), "cameras": ("nodeId",)}
38
+
39
+
40
+ def _check_references(project: Project, patch: dict[str, Any]) -> None:
41
+ nodes = {node["id"] for node in project.store.assemble()["document"]["content"]["nodes"]}
42
+ for field, keys in _NODE_REFERENCES.items():
43
+ for index, entry in enumerate(patch.get(field) or []):
44
+ for key in keys:
45
+ values = entry.get(key) if isinstance(entry, dict) else None
46
+ for value in values if isinstance(values, list) else [values]:
47
+ if isinstance(value, str) and value not in nodes:
48
+ raise LocalProjectError(
49
+ "DIRECTOR_NODE_NOT_FOUND",
50
+ f"{field}.{index}.{key}: no node {value!r}; nothing was written",
51
+ details={"field": field, "index": index, "id": entry.get("id"), "property": key,
52
+ "ref": value})
53
+
54
+
55
+ def _public(bom: dict[str, Any]) -> dict[str, Any]:
56
+ """The record as shown: ``guidesRead`` belongs to the hosted guide service and stays unused here."""
57
+ return {key: value for key, value in bom.items() if key != "guidesRead"}
58
+
59
+
60
+ def write_bom(project: Project, bom: dict[str, Any]) -> None:
61
+ bom = {**bom, "updatedAt": utc_timestamp()}
62
+ try:
63
+ encode_bom(bom)
64
+ except ValueError as exc:
65
+ raise LocalProjectError("BOM_TOO_LARGE", str(exc)) from exc
66
+ atomic_write_json(project.paths.bom, bom)
67
+
68
+
69
+ def _with_observed(project: Project, bom: dict[str, Any]) -> dict[str, Any]:
70
+ observed = observed_from_snapshot(project.store.assemble())
71
+ return _public({**bom, "observed": observed, "verification": derive_verification(bom, observed)})
72
+
73
+
74
+ def bom_get(directory: str) -> dict[str, Any]:
75
+ project = open_project(directory)
76
+ return {**base_result(project), "path": str(project.paths.bom), "exists": project.paths.bom.is_file(),
77
+ "bom": _with_observed(project, read_bom(project))}
78
+
79
+
80
+ def bom_checkpoint(directory: str, patch: dict[str, Any]) -> dict[str, Any]:
81
+ project = open_project(directory)
82
+ current = read_bom(project)
83
+ try:
84
+ merged = merge_checkpoint(current, patch, project.store.scene_sequence)
85
+ except ValueError as exc:
86
+ prefix = str(exc).split(":", 1)[0]
87
+ code = _CHECKPOINT_CODES.get(prefix, "BOM_CHECKPOINT_INVALID")
88
+ message, details = str(exc), None
89
+ if code == "BOM_REVIEW_EVIDENCE_MISSING":
90
+ evidence = current.get("verification") or {}
91
+ details = {"evidenceSceneSequence": evidence.get("sceneSequence"),
92
+ "currentSceneSequence": project.store.scene_sequence,
93
+ "evidenceStale": bool(evidence.get("stale", True))}
94
+ message = (f"{message}: the last complete `inspect views` evidence is from sceneSequence "
95
+ f"{details['evidenceSceneSequence']}, the project is at {details['currentSceneSequence']}; "
96
+ "rerun `inspect views` (any write, including adding or deleting a diagnostic camera, "
97
+ "invalidates it)")
98
+ raise LocalProjectError(code, message, details=details) from exc
99
+ _check_references(project, patch)
100
+ merged.pop("observed", None)
101
+ write_bom(project, merged)
102
+ merged = read_bom(project)
103
+ return {**base_result(project), "path": str(project.paths.bom), "bomRevision": merged["bomRevision"],
104
+ "bom": _with_observed(project, merged)}
105
+
106
+
107
+ def record_views(project: Project, result: dict[str, Any]) -> None:
108
+ write_bom(project, record_runtime_verification(read_bom(project), result))
@@ -0,0 +1,156 @@
1
+ """Offline asset search, asset details and the pose catalog over the local manifests."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from collections import Counter
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from topview_3d_cli.local_assets import ASSET_KINDS, asset_catalog, public_asset_entry
10
+ from topview_3d_cli.local_errors import LocalProjectError
11
+ from topview_3d_cli.local_project import open_project
12
+
13
+ SEARCH_LIMIT_DEFAULT = 20
14
+ SEARCH_LIMIT_MAX = 200
15
+ TAG_FACET_LIMIT = 40
16
+ POSE_PREFIX = "a3d_pose_"
17
+
18
+
19
+ def _catalog(directory: str | None) -> list[dict[str, Any]]:
20
+ return asset_catalog(open_project(directory).paths if directory else None)
21
+
22
+
23
+ def _root_of(entry: dict[str, Any]) -> Path | None:
24
+ if not entry.get("path") or not entry.get("file"):
25
+ return None
26
+ return Path(entry["path"]).parents[len(Path(entry["file"]).parts) - 1]
27
+
28
+
29
+ def _cover_path(entry: dict[str, Any]) -> str | None:
30
+ root = _root_of(entry)
31
+ return str(root / entry["cover"]) if root and entry.get("cover") else None
32
+
33
+
34
+ def _fold(value: Any) -> str:
35
+ return str(value or "").casefold()
36
+
37
+
38
+ def _score(entry: dict[str, Any], tokens: list[str]) -> int | None:
39
+ """None when a token is missing; otherwise name hits count 3, tag hits 2, other fields 1."""
40
+ name, tags = _fold(entry.get("name")), [_fold(tag) for tag in entry.get("tags") or []]
41
+ other = " ".join(_fold(entry.get(key)) for key in ("id", "category", "description", "rig"))
42
+ score = 0
43
+ for token in tokens:
44
+ if token in name:
45
+ score += 3
46
+ elif any(token == tag for tag in tags):
47
+ score += 2
48
+ elif any(token in tag for tag in tags) or token in other:
49
+ score += 1
50
+ else:
51
+ return None
52
+ return score
53
+
54
+
55
+ def compact_entry(entry: dict[str, Any]) -> dict[str, Any]:
56
+ row = {key: entry[key] for key in ("id", "kind", "name", "category", "tags", "description", "rig", "key",
57
+ "license", "origin") if entry.get(key) not in (None, "", [])}
58
+ if entry["kind"] == "pose":
59
+ row["shortId"] = entry["id"].removeprefix(POSE_PREFIX)
60
+ if entry["kind"] == "primitive":
61
+ row["primitive"] = entry["primitive"]
62
+ cover = _cover_path(entry)
63
+ if cover:
64
+ row["coverPath"] = cover
65
+ return row
66
+
67
+
68
+ def _facets(entries: list[dict[str, Any]]) -> dict[str, Any]:
69
+ tags = Counter(tag for entry in entries for tag in entry.get("tags") or [])
70
+ return {
71
+ "kind": dict(Counter(entry["kind"] for entry in entries)),
72
+ "category": dict(Counter(entry.get("category") or "" for entry in entries if entry.get("category"))),
73
+ "rig": dict(Counter(entry["rig"] for entry in entries if entry.get("rig"))),
74
+ "tags": dict(tags.most_common(TAG_FACET_LIMIT)),
75
+ }
76
+
77
+
78
+ def asset_search(directory: str | None, query: str | None = None, *, kind: str | None = None,
79
+ category: str | None = None, tags: list[str] | None = None, rig: str | None = None,
80
+ limit: int = SEARCH_LIMIT_DEFAULT, offset: int = 0) -> dict[str, Any]:
81
+ """Keyword search (every word must match) with exact filters; facets cover all matches."""
82
+ if not 1 <= limit <= SEARCH_LIMIT_MAX or offset < 0:
83
+ raise LocalProjectError("USAGE_INVALID", f"--limit must be 1..{SEARCH_LIMIT_MAX} and --offset >= 0")
84
+ tokens = _fold(query).split()
85
+ wanted_tags = {_fold(tag) for tag in tags or []}
86
+ matches = []
87
+ for entry in _catalog(directory):
88
+ if kind and entry["kind"] != kind:
89
+ continue
90
+ if category and _fold(entry.get("category")) != _fold(category):
91
+ continue
92
+ if rig and entry.get("rig") != rig:
93
+ continue
94
+ if not wanted_tags <= {_fold(tag) for tag in entry.get("tags") or []}:
95
+ continue
96
+ score = _score(entry, tokens)
97
+ if score is not None:
98
+ matches.append((score, entry))
99
+ matches.sort(key=lambda item: (-item[0], ASSET_KINDS.index(item[1]["kind"]), _fold(item[1].get("name"))))
100
+ page = matches[offset:offset + limit]
101
+ end = offset + len(page)
102
+ return {
103
+ "ok": True,
104
+ "query": {"text": query or "", "kind": kind, "category": category, "tags": sorted(wanted_tags),
105
+ "rig": rig, "limit": limit, "offset": offset},
106
+ "total": len(matches),
107
+ "complete": end >= len(matches),
108
+ "nextOffset": None if end >= len(matches) else end,
109
+ "facets": _facets([entry for _, entry in matches]),
110
+ "items": [{**compact_entry(entry), "score": score} for score, entry in page],
111
+ }
112
+
113
+
114
+ def find_any_asset(catalog: list[dict[str, Any]], asset_id: str, kind: str | None = None) -> dict[str, Any]:
115
+ """Look up by exact id (pose ids also without ``a3d_pose_``), optionally restricted to one kind."""
116
+ candidates = [entry for entry in catalog if kind is None or entry["kind"] == kind]
117
+ for wanted in (asset_id, POSE_PREFIX + asset_id):
118
+ found = next((entry for entry in candidates if entry["id"] == wanted), None)
119
+ if found:
120
+ return found
121
+ raise LocalProjectError("ASSET_NOT_FOUND", f"no {kind or 'asset'} {asset_id!r} in the local manifests")
122
+
123
+
124
+ def asset_show(directory: str | None, asset_id: str, *, kind: str | None = None) -> dict[str, Any]:
125
+ entry = find_any_asset(_catalog(directory), asset_id, kind)
126
+ detail: dict[str, Any] = {**public_asset_entry(entry), **compact_entry(entry)}
127
+ if entry.get("path"):
128
+ detail["path"] = entry["path"]
129
+ result: dict[str, Any] = {"ok": True, "asset": detail}
130
+ if entry["kind"] == "pose":
131
+ pose = json.loads(Path(entry["path"]).read_text(encoding="utf-8"))
132
+ bones = pose.get("bones") if isinstance(pose.get("bones"), dict) else {}
133
+ result["pose"] = {"boneCount": len(bones), "hips": pose.get("hips")}
134
+ result["usage"] = {"command": f"topview-3d-cli pose apply <dir> <characterNodeId> {detail['shortId']}",
135
+ "batchItem": {"nodeId": "<characterNodeId>", "poseId": detail["shortId"]}}
136
+ elif entry["kind"] in ("character", "prop"):
137
+ result["usage"] = {"change": {"action": "add_library", "kind": f"{entry['kind']}s",
138
+ "libraryId": entry["id"], "nodeId": "<new node id>"}}
139
+ else:
140
+ result["usage"] = {"change": {"action": "add_primitive", "nodeId": "<new node id>",
141
+ "primitive": entry["primitive"]}}
142
+ return result
143
+
144
+
145
+ def pose_catalog(directory: str | None, *, category: str | None = None, tag: str | None = None) -> dict[str, Any]:
146
+ """Every pose (built-in and project), compact and deterministic."""
147
+ poses = [entry for entry in _catalog(directory) if entry["kind"] == "pose"]
148
+ categories = dict(Counter(entry.get("category") or "" for entry in poses))
149
+ if category:
150
+ poses = [entry for entry in poses if _fold(entry.get("category")) == _fold(category)]
151
+ if tag:
152
+ poses = [entry for entry in poses if _fold(tag) in {_fold(item) for item in entry.get("tags") or []}]
153
+ poses.sort(key=lambda entry: (_fold(entry.get("category")), _fold(entry.get("name")), entry["id"]))
154
+ items = [{key: value for key, value in compact_entry(entry).items() if key not in ("kind", "license", "key")}
155
+ for entry in poses]
156
+ return {"ok": True, "total": len(items), "categories": categories, "items": items}