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.
- topview_3d_cli/__init__.py +3 -0
- topview_3d_cli/__main__.py +5 -0
- topview_3d_cli/_runtime/builtin-assets/LICENSE +396 -0
- topview_3d_cli/_runtime/builtin-assets/NOTICE +24 -0
- topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_30ec53c93eae4173946b38639691ebe0.glb +0 -0
- topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_461b4f4b76ae4b198f439fa4f6f743b6.glb +0 -0
- topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_7d220ac359844c80a8faaf0e25478477.glb +0 -0
- topview_3d_cli/_runtime/builtin-assets/characters/a3d_char_de37873a79244fbe8a0660ea08d1b4ab.glb +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-jab.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-lunge.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-attack-rear.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-crawling-forward-reaching-with-le.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-exaggerated-punch-with-right-hand.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-examine-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-holding-bow.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-jab-with-right-hand.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-on-hands-and-toes-right-leg-forwa.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-on-right-leg.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-push-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-backwards.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-with-both-hands.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-pushing-with-left-side-of-body.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_action-sliding.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-arms-spread-looking-upwards.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-examine-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-lean-fwd-right-hand-supporting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-leaning-against-object-look-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-back.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-down-back.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-fwd-f.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-look-fwd-m.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-on-knees-arms-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_crouch-sneaking.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-l-toe-top-of-right-foot.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-spread-lean-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-spread-lean-r.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-legs-x.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-foot-arms-extended-backwards.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-toe-arms-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-r-toe-l-leg-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-toes-arch.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dance-toes-reach.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-bent-forwards.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-left-leg-bent-hands-spread.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-legs-bent-and-arms-spread.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-floating-legs-raised-arms-extended-u.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-l-toe-look-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dyn-leaping-forward-arms-extended-backwa.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dynamic-float-arch.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_dynamic-float-dive.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-back-arched-arms-supp.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-legs-to-side-arms-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-legs-x-arms-supp.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-left-leg-bent-hands-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-left-leg-bent.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-back-legs-x-hands-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-left-side-left-arm-supporting-hea.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-left-side-left-arm-supporting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-right-side-right-arm-supporting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-right-side-right-hand-supporting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-on-stomach-elbows-supporting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-right-arm-supporting-l-arm-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_lay-right-leg-forward-arms-supp.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-run-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-run-reach.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-sneaking-forward.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-sprinting.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-stepping-forward-look-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walk-fwd-look-r.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walk-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking-arms-on-hips.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking-right-arm-on-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_loco-walking.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-holding-left-knee-reclining.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-holding-right-knee.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-resting-on-knees.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-supp.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-arms-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-elbows-on-legs-lean-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-elbows-on-legs-lean-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-knees-up-hold.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-back-arms-supp.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-fwd-elbows-on-knees.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-lean-fwd-hands-in-front.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-arms-x.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-hands-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-hold.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-reclining.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-legs-x-right-arm-over-knee.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reaching-hands-towards-toes.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reaching-left-hand-towards-right-toe.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-facepalm.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-hands-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-left-leg-crossed-hands-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-left-leg-crossed.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-look-down.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-reclined-right-leg-crossed-hands-hea.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-rev-sit-hands-grasping.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_sit-rev-sit-hands-relaxed.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-behind-back.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-down.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-arms-slightly-spread.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-bending-forward-arms-x-over-knees.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-hands-on-hips-look-back.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-hip-f.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-hip-m.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-l-leg-r-hip-l-arm-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-lean-back-arms-behind.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-lean-back-l-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-elbow-resting-obj.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-leg-obj-left-hand-on-knee-r-h.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-left-leg-obj-left-hand-on-knee.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-arms-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-l-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-leaning-obj.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-legs-x-left-arm-obj-r-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-down-r.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-fwd.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-l.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-look-r.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-arm-up-l-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-foot-l-leg-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-foot-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-hip-l-arm-up.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-r-hip.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-hand-behind-head.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-leg-obj-arms-x.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_stand-right-leg-obj-l-hip-right-hand-on.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/covers/a3d_pose_t-pose.webp +0 -0
- topview_3d_cli/_runtime/builtin-assets/manifest.json +2372 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-jab.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-lunge.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-attack-rear.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-crawling-forward-reaching-with-le.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-exaggerated-punch-with-right-hand.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-examine-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-holding-bow.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-jab-with-right-hand.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-on-hands-and-toes-right-leg-forwa.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-on-right-leg.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-push-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-backwards.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-with-both-hands.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-pushing-with-left-side-of-body.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_action-sliding.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-arms-spread-looking-upwards.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-examine-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-lean-fwd-right-hand-supporting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-leaning-against-object-look-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-back.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-down-back.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-fwd-f.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-look-fwd-m.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-on-knees-arms-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_crouch-sneaking.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-l-toe-top-of-right-foot.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-spread-lean-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-spread-lean-r.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-legs-x.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-foot-arms-extended-backwards.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-toe-arms-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-r-toe-l-leg-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-toes-arch.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dance-toes-reach.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-bent-forwards.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-left-leg-bent-hands-spread.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-legs-bent-and-arms-spread.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-floating-legs-raised-arms-extended-u.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-l-toe-look-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dyn-leaping-forward-arms-extended-backwa.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dynamic-float-arch.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_dynamic-float-dive.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-back-arched-arms-supp.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-legs-to-side-arms-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-legs-x-arms-supp.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-left-leg-bent-hands-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-left-leg-bent.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-back-legs-x-hands-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-left-side-left-arm-supporting-hea.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-left-side-left-arm-supporting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-right-side-right-arm-supporting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-right-side-right-hand-supporting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-on-stomach-elbows-supporting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-right-arm-supporting-l-arm-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_lay-right-leg-forward-arms-supp.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-run-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-run-reach.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-sneaking-forward.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-sprinting.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-stepping-forward-look-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walk-fwd-look-r.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walk-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking-arms-on-hips.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking-right-arm-on-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_loco-walking.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-holding-left-knee-reclining.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-holding-right-knee.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-resting-on-knees.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-supp.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-arms-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-elbows-on-legs-lean-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-elbows-on-legs-lean-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-knees-up-hold.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-back-arms-supp.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-fwd-elbows-on-knees.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-lean-fwd-hands-in-front.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-arms-x.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-hands-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-hold.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-reclining.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-legs-x-right-arm-over-knee.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reaching-hands-towards-toes.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reaching-left-hand-towards-right-toe.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-facepalm.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-hands-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-left-leg-crossed-hands-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-left-leg-crossed.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-look-down.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-reclined-right-leg-crossed-hands-hea.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-rev-sit-hands-grasping.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_sit-rev-sit-hands-relaxed.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-behind-back.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-down.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-arms-slightly-spread.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-bending-forward-arms-x-over-knees.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-hands-on-hips-look-back.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-hip-f.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-hip-m.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-l-leg-r-hip-l-arm-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-lean-back-arms-behind.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-lean-back-l-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-elbow-resting-obj.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-leg-obj-left-hand-on-knee-r-h.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-left-leg-obj-left-hand-on-knee.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-arms-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-l-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-leaning-obj.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-legs-x-left-arm-obj-r-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-down-r.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-fwd.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-l.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-look-r.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-arm-up-l-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-foot-l-leg-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-foot-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-hip-l-arm-up.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-r-hip.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-hand-behind-head.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-leg-obj-arms-x.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_stand-right-leg-obj-l-hip-right-hand-on.json +1 -0
- topview_3d_cli/_runtime/builtin-assets/poses/a3d_pose_t-pose.json +1 -0
- topview_3d_cli/_runtime/director-cli/applyIntent.mjs +35 -0
- topview_3d_cli/_runtime/director-cli/browser.mjs +47 -0
- topview_3d_cli/_runtime/director-cli/cli.mjs +56 -0
- topview_3d_cli/_runtime/director-cli/editSequence.mjs +125 -0
- topview_3d_cli/_runtime/director-cli/evaluate.mjs +254 -0
- topview_3d_cli/_runtime/director-cli/mediaUrl.mjs +47 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_decoder.js +34 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_decoder.wasm +0 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/draco/draco_wasm_wrapper.js +117 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/engine/index.mjs +22219 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/evaluate/index.mjs +4229 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/dist/headless/index.mjs +22832 -0
- topview_3d_cli/_runtime/director-cli/node_modules/@topview/3d-builder/package.json +75 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/LICENSE +21 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/build/three.core.js +59732 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/build/three.module.js +19552 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/controls/OrbitControls.js +1963 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/curves/NURBSCurve.js +155 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/curves/NURBSUtils.js +532 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_decoder.js +34 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_decoder.wasm +0 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/draco/draco_wasm_wrapper.js +117 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/libs/fflate.module.js +2672 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineMaterial.js +703 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineSegments2.js +426 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js +298 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/DRACOLoader.js +739 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/FBXLoader.js +4580 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/loaders/GLTFLoader.js +4860 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/utils/BufferGeometryUtils.js +1434 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- topview_3d_cli/_runtime/director-cli/node_modules/three/package.json +123 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/LICENSE +21 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/index.js +4 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/package.json +118 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/ZodError.js +133 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/errors.js +9 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/external.js +6 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/errorUtil.js +6 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/parseUtil.js +109 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/typeAliases.js +1 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/helpers/util.js +133 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/locales/en.js +109 -0
- topview_3d_cli/_runtime/director-cli/node_modules/zod/v3/types.js +3693 -0
- topview_3d_cli/_runtime/director-cli/package.json +6 -0
- topview_3d_cli/_runtime/director-cli/playwright.mjs +26 -0
- topview_3d_cli/_runtime/director-cli/render.mjs +336 -0
- topview_3d_cli/_runtime/director-cli/static/headless.html +124 -0
- topview_3d_cli/_runtime/director-cli/static/measure.mjs +61 -0
- topview_3d_cli/_runtime/director-cli/static/pose.mjs +71 -0
- topview_3d_cli/_runtime/director-cli/staticIntent.mjs +135 -0
- topview_3d_cli/_runtime/runtime.json +6 -0
- topview_3d_cli/bom.py +326 -0
- topview_3d_cli/contracts/director-v1/schema/director-document.schema.json +1097 -0
- topview_3d_cli/contracts/director-v1/schema/fcurves-compact-v1.schema.json +105 -0
- topview_3d_cli/director_camera_view.py +54 -0
- topview_3d_cli/director_document.py +793 -0
- topview_3d_cli/director_geometry.py +66 -0
- topview_3d_cli/director_operations.py +679 -0
- topview_3d_cli/director_static.py +134 -0
- topview_3d_cli/local_assets.py +248 -0
- topview_3d_cli/local_bom.py +108 -0
- topview_3d_cli/local_catalog.py +156 -0
- topview_3d_cli/local_cli.py +493 -0
- topview_3d_cli/local_edit.py +350 -0
- topview_3d_cli/local_errors.py +104 -0
- topview_3d_cli/local_inspect.py +302 -0
- topview_3d_cli/local_project.py +280 -0
- topview_3d_cli/local_read.py +139 -0
- topview_3d_cli/local_render.py +97 -0
- topview_3d_cli/node_batch.py +299 -0
- topview_3d_cli/renderer.py +75 -0
- topview_3d_cli/runtime.py +107 -0
- topview_3d_cli-0.1.0.dist-info/METADATA +48 -0
- topview_3d_cli-0.1.0.dist-info/RECORD +332 -0
- topview_3d_cli-0.1.0.dist-info/WHEEL +5 -0
- topview_3d_cli-0.1.0.dist-info/entry_points.txt +2 -0
- topview_3d_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- topview_3d_cli-0.1.0.dist-info/licenses/LICENSE-ASSETS +396 -0
- topview_3d_cli-0.1.0.dist-info/licenses/NOTICE +18 -0
- 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}
|