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,139 @@
|
|
|
1
|
+
"""Read-only views: document outline and entities, render runs and their PNGs, camera presets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from topview_3d_cli.director_document import get_director_entity, summarize_director_snapshot
|
|
10
|
+
from topview_3d_cli.local_errors import LocalProjectError
|
|
11
|
+
from topview_3d_cli.local_project import base_result, document_get, open_project
|
|
12
|
+
from topview_3d_cli.renderer import director_cli
|
|
13
|
+
|
|
14
|
+
ENTITY_TYPES = ("node", "clip", "fcurves")
|
|
15
|
+
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def document_summary(directory: str) -> dict[str, Any]:
|
|
19
|
+
project = open_project(directory)
|
|
20
|
+
summary = summarize_director_snapshot(project.store.assemble())
|
|
21
|
+
summary["message"] = ("Outline only: curves and full nodes omitted. "
|
|
22
|
+
"Use `document get <dir> --entity <id>` for one node, clip or fcurves shard.")
|
|
23
|
+
return {**base_result(project), "summary": summary}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def document_entity(directory: str, entity_id: str, *, entity_type: str | None = None,
|
|
27
|
+
include_curves: bool = False) -> dict[str, Any]:
|
|
28
|
+
"""One node, clip or ``fcurves__<nodeId>`` shard; the type is inferred when omitted."""
|
|
29
|
+
project = open_project(directory)
|
|
30
|
+
snapshot = project.store.assemble()
|
|
31
|
+
if entity_type:
|
|
32
|
+
types = [entity_type]
|
|
33
|
+
elif entity_id.startswith("fcurves__"):
|
|
34
|
+
types = ["fcurves"]
|
|
35
|
+
else:
|
|
36
|
+
types = ["node", "clip", "fcurves"]
|
|
37
|
+
for candidate in types:
|
|
38
|
+
try:
|
|
39
|
+
found = get_director_entity(snapshot, candidate, entity_id, include_curves=include_curves)
|
|
40
|
+
except ValueError:
|
|
41
|
+
continue
|
|
42
|
+
key = found.get("entityId", entity_id)
|
|
43
|
+
return {**base_result(project), **found, "entityId": key,
|
|
44
|
+
"entityVersion": snapshot["entityVersions"].get(key)}
|
|
45
|
+
raise LocalProjectError("DOCUMENT_ENTITY_NOT_FOUND", f"no {' or '.join(types)} {entity_id!r} in the document")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def document_view(directory: str, *, summary: bool, entity: str | None, entity_type: str | None,
|
|
49
|
+
include_curves: bool) -> dict[str, Any]:
|
|
50
|
+
if summary and entity:
|
|
51
|
+
raise LocalProjectError("USAGE_INVALID", "use either --summary or --entity")
|
|
52
|
+
if entity_type and not entity:
|
|
53
|
+
raise LocalProjectError("USAGE_INVALID", "--type needs --entity")
|
|
54
|
+
if summary:
|
|
55
|
+
return document_summary(directory)
|
|
56
|
+
if entity:
|
|
57
|
+
return document_entity(directory, entity, entity_type=entity_type, include_curves=include_curves)
|
|
58
|
+
return document_get(directory)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_run(run_dir: Path) -> dict[str, Any] | None:
|
|
62
|
+
try:
|
|
63
|
+
manifest = json.loads((run_dir / "render.json").read_text(encoding="utf-8"))
|
|
64
|
+
except (OSError, json.JSONDecodeError):
|
|
65
|
+
return None
|
|
66
|
+
return manifest if isinstance(manifest, dict) else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _runs(renders: Path) -> list[tuple[Path, dict[str, Any]]]:
|
|
70
|
+
rows = []
|
|
71
|
+
for run_dir in renders.iterdir() if renders.is_dir() else []:
|
|
72
|
+
manifest = _read_run(run_dir) if run_dir.is_dir() else None
|
|
73
|
+
if manifest:
|
|
74
|
+
rows.append((run_dir, manifest))
|
|
75
|
+
rows.sort(key=lambda row: (str(row[1].get("createdAt") or ""), row[0].name))
|
|
76
|
+
return rows
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def renders_list(directory: str) -> dict[str, Any]:
|
|
80
|
+
project = open_project(directory)
|
|
81
|
+
sequence = project.store.scene_sequence
|
|
82
|
+
runs = [{
|
|
83
|
+
"runId": run_dir.name,
|
|
84
|
+
"createdAt": manifest.get("createdAt"),
|
|
85
|
+
"cameraNodeId": manifest.get("cameraNodeId"),
|
|
86
|
+
"sceneSequence": manifest.get("sceneSequence"),
|
|
87
|
+
"stale": manifest.get("sceneSequence") != sequence,
|
|
88
|
+
"frames": [frame.get("frame") for frame in manifest.get("frames") or []],
|
|
89
|
+
"width": manifest.get("width"),
|
|
90
|
+
"height": manifest.get("height"),
|
|
91
|
+
"blank": manifest.get("blank"),
|
|
92
|
+
"blockedRequests": len(manifest.get("blockedRequests") or []),
|
|
93
|
+
} for run_dir, manifest in _runs(project.paths.renders)]
|
|
94
|
+
return {**base_result(project), "count": len(runs), "runs": runs}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def renders_show(directory: str, run_id: str | None = None, *, frame: int | None = None) -> dict[str, Any]:
|
|
98
|
+
"""Path and checksum of one PNG inside the project (the contact sheet unless --frame)."""
|
|
99
|
+
project = open_project(directory)
|
|
100
|
+
runs = _runs(project.paths.renders)
|
|
101
|
+
if run_id is None:
|
|
102
|
+
if not runs:
|
|
103
|
+
raise LocalProjectError("RENDER_NOT_FOUND", "the project has no renders yet; run `topview-3d-cli render`")
|
|
104
|
+
run_dir, manifest = runs[-1]
|
|
105
|
+
else:
|
|
106
|
+
run_dir, manifest = next(((path, data) for path, data in runs if path.name == run_id), (None, None))
|
|
107
|
+
if run_dir is None:
|
|
108
|
+
raise LocalProjectError("RENDER_NOT_FOUND", f"no render run {run_id!r}")
|
|
109
|
+
if frame is None:
|
|
110
|
+
entry = manifest.get("contactSheet") or {}
|
|
111
|
+
else:
|
|
112
|
+
entry = next((item for item in manifest.get("frames") or [] if item.get("frame") == frame), None)
|
|
113
|
+
if entry is None:
|
|
114
|
+
raise LocalProjectError("RENDER_NOT_FOUND", f"run {run_dir.name} has no frame {frame}")
|
|
115
|
+
path = (run_dir / str(entry.get("file") or "")).resolve()
|
|
116
|
+
if not entry.get("file") or path.parent != run_dir.resolve() or not path.is_file():
|
|
117
|
+
raise LocalProjectError("RENDER_NOT_FOUND", f"{run_dir.name}: image file missing")
|
|
118
|
+
data = path.read_bytes()
|
|
119
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
120
|
+
if not data.startswith(PNG_SIGNATURE) or (entry.get("sha256") and entry["sha256"] != digest):
|
|
121
|
+
raise LocalProjectError("RENDER_IMAGE_INVALID", f"{path} is not the PNG recorded in render.json")
|
|
122
|
+
return {
|
|
123
|
+
**base_result(project),
|
|
124
|
+
"runId": run_dir.name,
|
|
125
|
+
"kind": "contactSheet" if frame is None else "frame",
|
|
126
|
+
"frame": frame,
|
|
127
|
+
"path": str(path),
|
|
128
|
+
"mimeType": "image/png",
|
|
129
|
+
"sizeBytes": len(data),
|
|
130
|
+
"sha256": digest,
|
|
131
|
+
"cameraNodeId": manifest.get("cameraNodeId"),
|
|
132
|
+
"sceneSequence": manifest.get("sceneSequence"),
|
|
133
|
+
"stale": manifest.get("sceneSequence") != project.store.scene_sequence,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def camera_presets() -> dict[str, Any]:
|
|
138
|
+
listed = director_cli("list-camera-presets")
|
|
139
|
+
return {"ok": True, "presets": listed.get("presets", [])}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Render runs under ``.topview3d/renders/<runId>`` with the Node renderer and local model files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from topview_3d_cli.local_assets import asset_catalog, check_render_assets, local_asset_map
|
|
12
|
+
from topview_3d_cli.local_errors import LocalProjectError
|
|
13
|
+
from topview_3d_cli.local_project import CLI_VERSION, Project, open_project
|
|
14
|
+
from topview_3d_cli.renderer import director_cli
|
|
15
|
+
from topview_3d_cli.runtime import builder_version
|
|
16
|
+
|
|
17
|
+
# Console noise from the headless page, not scene problems: the builder's two bundles each import
|
|
18
|
+
# Mediabunny, and SwiftShader reports ReadPixels stalls.
|
|
19
|
+
BENIGN_RENDER_WARNINGS = ("Mediabunny was loaded twice", "GPU stall due to ReadPixels")
|
|
20
|
+
RENDER_PAYLOAD_KEYS = {"frames", "width", "height", "cameraNodeId", "publicAssetBase"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def document_sha256(document: dict[str, Any]) -> str:
|
|
24
|
+
canonical = json.dumps(document, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
25
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def require_camera(document: dict[str, Any], camera_id: str | None) -> str:
|
|
29
|
+
"""The camera to use: ``camera_id`` (which must exist and be a camera) or the document's default."""
|
|
30
|
+
nodes = document["content"]["nodes"]
|
|
31
|
+
if camera_id:
|
|
32
|
+
node = next((item for item in nodes if item["id"] == camera_id), None)
|
|
33
|
+
if node is None:
|
|
34
|
+
raise LocalProjectError("CAMERA_NOT_FOUND", f"no node {camera_id!r}; cameras: "
|
|
35
|
+
f"{[item['id'] for item in nodes if item.get('type') == 'camera']}",
|
|
36
|
+
details={"cameraNodeId": camera_id})
|
|
37
|
+
if node.get("type") != "camera":
|
|
38
|
+
raise LocalProjectError("CAMERA_REQUIRED", f"{camera_id} is a {node.get('type')}, not a camera",
|
|
39
|
+
details={"cameraNodeId": camera_id})
|
|
40
|
+
return camera_id
|
|
41
|
+
active = document["content"].get("activeShotCameraNodeId")
|
|
42
|
+
if any(item["id"] == active and item.get("type") == "camera" for item in nodes):
|
|
43
|
+
return active
|
|
44
|
+
fallback = next((item["id"] for item in nodes if item.get("type") == "camera"), None)
|
|
45
|
+
if fallback is None:
|
|
46
|
+
raise LocalProjectError("CAMERA_REQUIRED", "the scene has no camera; add one with `node batch` add_camera")
|
|
47
|
+
return fallback
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_snapshot(project: Project, assembled: dict[str, Any], options: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
"""One render run of ``assembled``; the run directory is removed again when rendering fails."""
|
|
52
|
+
body: dict[str, Any] = {"frames": [0], **options}
|
|
53
|
+
mapping = local_asset_map(asset_catalog(project.paths))
|
|
54
|
+
warnings = check_render_assets(assembled["document"], mapping, str(body.get("publicAssetBase") or ""))
|
|
55
|
+
run_id = uuid.uuid4().hex
|
|
56
|
+
output_dir = project.paths.renders / run_id
|
|
57
|
+
body.update({
|
|
58
|
+
"document": assembled["document"],
|
|
59
|
+
"fcurves": assembled["fcurves"],
|
|
60
|
+
"sceneSequence": assembled["sceneSequence"],
|
|
61
|
+
"outputDir": str(output_dir),
|
|
62
|
+
"publicAssetBase": body.get("publicAssetBase") or "",
|
|
63
|
+
"localAssets": mapping,
|
|
64
|
+
"metadata": {
|
|
65
|
+
"revision": project.metadata.get("revision", 0),
|
|
66
|
+
"documentSha256": document_sha256(assembled["document"]),
|
|
67
|
+
"builderVersion": builder_version(),
|
|
68
|
+
"cliVersion": CLI_VERSION,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
try:
|
|
72
|
+
result = director_cli("render-frames", body)
|
|
73
|
+
except LocalProjectError:
|
|
74
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
75
|
+
raise
|
|
76
|
+
if isinstance(result.get("warnings"), list):
|
|
77
|
+
result["warnings"] = [item for item in result["warnings"]
|
|
78
|
+
if not any(marker in str(item) for marker in BENIGN_RENDER_WARNINGS)]
|
|
79
|
+
return {"ok": True, "runId": run_id, **body["metadata"], "sceneSequence": assembled["sceneSequence"],
|
|
80
|
+
"warnings": warnings, "result": result}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render(directory: str, payload_path: str | None = None) -> dict[str, Any]:
|
|
84
|
+
project = open_project(directory)
|
|
85
|
+
options: dict[str, Any] = {}
|
|
86
|
+
if payload_path:
|
|
87
|
+
try:
|
|
88
|
+
supplied = json.loads(Path(payload_path).read_text(encoding="utf-8"))
|
|
89
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
90
|
+
raise LocalProjectError("RENDER_PAYLOAD_INVALID", str(exc)) from exc
|
|
91
|
+
if not isinstance(supplied, dict) or set(supplied) - RENDER_PAYLOAD_KEYS:
|
|
92
|
+
raise LocalProjectError("RENDER_PAYLOAD_INVALID",
|
|
93
|
+
f"payload must be an object with keys from {sorted(RENDER_PAYLOAD_KEYS)}")
|
|
94
|
+
options.update(supplied)
|
|
95
|
+
assembled = project.store.assemble()
|
|
96
|
+
options["cameraNodeId"] = require_camera(assembled["document"], options.get("cameraNodeId"))
|
|
97
|
+
return render_snapshot(project, assembled, options)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Typed, ordered node changes (``node batch``) and their compilation to one operation batch.
|
|
2
|
+
|
|
3
|
+
Staging runs on a copy of the stored snapshot, so a rejected change writes nothing.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from jsonschema import Draft7Validator
|
|
11
|
+
|
|
12
|
+
from topview_3d_cli.director_document import apply_scene_set, fcurve_node_id, new_scene_operation_id
|
|
13
|
+
from topview_3d_cli.director_static import delete_node_operations
|
|
14
|
+
|
|
15
|
+
CLIP_ARRAYS = ("cameraMotionClips", "motionClips", "pathMotionClips")
|
|
16
|
+
BATCH_OPERATION_LIMIT = 64
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def obj(properties, required=()):
|
|
20
|
+
return {"type": "object", "properties": properties, "required": list(required),
|
|
21
|
+
"additionalProperties": False}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def batch_schema(*, require_sequence: bool = True) -> dict[str, Any]:
|
|
25
|
+
identifier = {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}
|
|
26
|
+
number = {"type": "number"}
|
|
27
|
+
positive = {"type": "number", "exclusiveMinimum": 0}
|
|
28
|
+
vector = {**obj({axis: number for axis in "xyz"}), "minProperties": 1}
|
|
29
|
+
scale = {**obj({axis: positive for axis in "xyz"}), "minProperties": 1}
|
|
30
|
+
transform = {"position": vector, "rotation": vector, "scale": scale}
|
|
31
|
+
point = obj({axis: number for axis in "xyz"}, "xyz")
|
|
32
|
+
view = {"oneOf": [
|
|
33
|
+
obj({"mode": {"const": "world"}, "position": point, "target": point},
|
|
34
|
+
("mode", "position", "target")),
|
|
35
|
+
obj({"mode": {"const": "subject"}, "subjectNodeId": identifier,
|
|
36
|
+
"offset": point, "targetOffset": point},
|
|
37
|
+
("mode", "subjectNodeId", "offset", "targetOffset")),
|
|
38
|
+
]}
|
|
39
|
+
name = {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "\\S"}
|
|
40
|
+
camera = {
|
|
41
|
+
"fov": {"type": "number", "minimum": 12, "maximum": 120},
|
|
42
|
+
"distance": {"type": "number", "minimum": 0.1, "maximum": 200},
|
|
43
|
+
"view": view,
|
|
44
|
+
}
|
|
45
|
+
geometry = []
|
|
46
|
+
for kind, dimensions in (
|
|
47
|
+
("BoxGeometry", ("width", "height", "depth")),
|
|
48
|
+
("SphereGeometry", ("radius",)),
|
|
49
|
+
("CylinderGeometry", ("radiusTop", "radiusBottom", "height")),
|
|
50
|
+
("ConeGeometry", ("radius", "height")),
|
|
51
|
+
):
|
|
52
|
+
parameters = {key: positive for key in dimensions}
|
|
53
|
+
if kind == "CylinderGeometry":
|
|
54
|
+
parameters.update({key: {"type": "number", "minimum": 0}
|
|
55
|
+
for key in ("radiusTop", "radiusBottom")})
|
|
56
|
+
if kind in {"CylinderGeometry", "ConeGeometry"}:
|
|
57
|
+
parameters["radialSegments"] = {"type": "integer", "minimum": 3, "maximum": 128}
|
|
58
|
+
geometry.append(obj({"kind": {"const": kind}, "parameters": obj(parameters, dimensions)},
|
|
59
|
+
("kind", "parameters")))
|
|
60
|
+
|
|
61
|
+
def action(kind, fields, required=(), description=""):
|
|
62
|
+
schema = obj({"action": {"const": kind}, "nodeId": identifier, **fields},
|
|
63
|
+
("action", "nodeId", *required))
|
|
64
|
+
if description:
|
|
65
|
+
schema["description"] = description
|
|
66
|
+
return schema
|
|
67
|
+
return obj({
|
|
68
|
+
"expectedSceneSequence": {"type": "integer", "minimum": 0},
|
|
69
|
+
"changes": {"type": "array", "minItems": 1, "maxItems": 64, "items": {"oneOf": [
|
|
70
|
+
action("add_primitive", {"name": name, "primitive": {"oneOf": geometry}, **transform},
|
|
71
|
+
("primitive",), "Create a new primitive. Requires primitive geometry."),
|
|
72
|
+
action("add_library", {"kind": {"enum": ["characters", "props"]},
|
|
73
|
+
"libraryId": identifier, "name": name, **transform}, ("kind", "libraryId"),
|
|
74
|
+
"Create a new character or prop from the asset library. Requires kind and libraryId."),
|
|
75
|
+
action("add_camera", {"presetId": name, "subjectNodeId": identifier,
|
|
76
|
+
"name": name, **transform, **camera}, ("presetId",),
|
|
77
|
+
"Create a new camera. Requires presetId. Do not use update to create cameras."),
|
|
78
|
+
{**action("update", {"name": name, **transform, **camera},
|
|
79
|
+
description="Change an existing node: name, partial transform, fov, distance. "
|
|
80
|
+
"Does not create nodes and does not accept presetId."),
|
|
81
|
+
"minProperties": 3},
|
|
82
|
+
action("delete", {}, description="Delete an existing node by nodeId."),
|
|
83
|
+
action("repeat_primitive", {
|
|
84
|
+
"name": name, "primitive": {"oneOf": geometry}, **transform,
|
|
85
|
+
"count": {"type": "integer", "minimum": 2, "maximum": 32},
|
|
86
|
+
"step": point,
|
|
87
|
+
}, ("primitive", "count", "step"),
|
|
88
|
+
"Create regularly spaced primitive copies nodeId_1 through nodeId_count."),
|
|
89
|
+
]}},
|
|
90
|
+
}, ("expectedSceneSequence", "changes") if require_sequence else ("changes",))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
BATCH_SCHEMA = batch_schema()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def schema_error(schema: dict[str, Any], arguments: Any) -> str | None:
|
|
97
|
+
"""First schema violation as ``path: message``, preferring the change's own action schema."""
|
|
98
|
+
error = next(Draft7Validator(schema).iter_errors(arguments), None)
|
|
99
|
+
if error is None:
|
|
100
|
+
return None
|
|
101
|
+
for index, change in enumerate(arguments.get("changes", []) if isinstance(arguments, dict) else []):
|
|
102
|
+
if not isinstance(change, dict):
|
|
103
|
+
continue
|
|
104
|
+
selected = next((item for item in schema["properties"]["changes"]["items"]["oneOf"]
|
|
105
|
+
if item["properties"]["action"]["const"] == change.get("action")), None)
|
|
106
|
+
detail = next(Draft7Validator(selected).iter_errors(change), None) if selected else None
|
|
107
|
+
if detail and list(detail.path)[:1] == ["primitive"] and isinstance(change.get("primitive"), dict):
|
|
108
|
+
return f"changes.{index}.{_primitive_error(selected['properties']['primitive'], change['primitive'])}"
|
|
109
|
+
if detail:
|
|
110
|
+
return f"changes.{index}.{'.'.join(map(str, detail.path))}: {detail.message[:450]}"
|
|
111
|
+
return f"{'.'.join(map(str, error.path))}: {error.message[:450]}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _primitive_error(schema: dict[str, Any], primitive: dict[str, Any]) -> str:
|
|
115
|
+
"""Validate against the branch for ``primitive.kind`` so the message names the kind and field."""
|
|
116
|
+
branches = {branch["properties"]["kind"]["const"]: branch for branch in schema["oneOf"]}
|
|
117
|
+
kind = primitive.get("kind")
|
|
118
|
+
if kind not in branches:
|
|
119
|
+
return f"primitive.kind: {kind!r} is not one of {sorted(branches)}"
|
|
120
|
+
detail = next(Draft7Validator(branches[kind]).iter_errors(primitive), None)
|
|
121
|
+
if detail is None:
|
|
122
|
+
return f"primitive: invalid {kind}"
|
|
123
|
+
return f"primitive.{'.'.join(map(str, detail.path))}: {detail.message[:400]} ({kind} takes " \
|
|
124
|
+
f"{sorted(branches[kind]['properties']['parameters']['properties'])})"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def expand_changes(changes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
128
|
+
"""``repeat_primitive`` becomes ``add_primitive`` for nodeId_1..nodeId_count."""
|
|
129
|
+
expanded = []
|
|
130
|
+
for change in changes:
|
|
131
|
+
if change["action"] != "repeat_primitive":
|
|
132
|
+
expanded.append(change)
|
|
133
|
+
continue
|
|
134
|
+
for copy_index in range(change["count"]):
|
|
135
|
+
expanded.append({
|
|
136
|
+
**{key: value for key, value in change.items() if key not in {"count", "step"}},
|
|
137
|
+
"action": "add_primitive",
|
|
138
|
+
"nodeId": f"{change['nodeId']}_{copy_index + 1}",
|
|
139
|
+
"position": {
|
|
140
|
+
axis: change.get("position", {}).get(axis, 0) + copy_index * change["step"][axis]
|
|
141
|
+
for axis in "xyz"
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
return expanded
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def stage_delete(snapshot: dict[str, Any], node_id: str) -> set[str]:
|
|
148
|
+
"""Apply the cascading delete of ``node_id`` to a staged snapshot; return deleted fcurve ids."""
|
|
149
|
+
batch, _ = delete_node_operations(snapshot, node_id)
|
|
150
|
+
for operation in batch["operations"]:
|
|
151
|
+
kind, entity_id, payload = (operation[key] for key in ("kind", "entityId", "payload"))
|
|
152
|
+
content = snapshot["document"]["content"]
|
|
153
|
+
if kind == "director.node.delete":
|
|
154
|
+
content["nodes"] = [node for node in content["nodes"] if node["id"] != entity_id]
|
|
155
|
+
elif kind == "director.node.upsert":
|
|
156
|
+
content["nodes"] = [payload["node"] if node["id"] == entity_id else node for node in content["nodes"]]
|
|
157
|
+
elif kind == "director.clip.delete":
|
|
158
|
+
for key in CLIP_ARRAYS:
|
|
159
|
+
animation = content["timeline"]["animation"]
|
|
160
|
+
animation[key] = [clip for clip in animation.get(key, []) if clip["id"] != entity_id]
|
|
161
|
+
elif kind == "director.fcurves.delete":
|
|
162
|
+
curves = snapshot.get("fcurves") or {}
|
|
163
|
+
snapshot["fcurves"] = curves
|
|
164
|
+
curves.pop(entity_id, None)
|
|
165
|
+
if curves.get("encoding") == "compact-v1":
|
|
166
|
+
curves["fcurves"] = [c for c in curves.get("fcurves", []) if fcurve_node_id(c) != node_id]
|
|
167
|
+
elif kind == "director.scene.set":
|
|
168
|
+
snapshot["document"] = apply_scene_set(snapshot["document"], payload["scene"])
|
|
169
|
+
return {op["entityId"] for op in batch["operations"] if op["kind"] == "director.fcurves.delete"}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def scene_fields(document: dict[str, Any]) -> dict[str, Any]:
|
|
173
|
+
content = copy.deepcopy(document["content"])
|
|
174
|
+
content.pop("nodes", None)
|
|
175
|
+
for key in CLIP_ARRAYS:
|
|
176
|
+
content.get("timeline", {}).get("animation", {}).pop(key, None)
|
|
177
|
+
return content
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def compile_operations(before: dict[str, Any], after: dict[str, Any], deleted_curves: set[str]):
|
|
181
|
+
"""Diff two snapshots into director operations; returns (operations, old nodes, new nodes)."""
|
|
182
|
+
operations = []
|
|
183
|
+
|
|
184
|
+
def add(kind, entity_id, payload):
|
|
185
|
+
operations.append({"operationId": new_scene_operation_id("batch_node"),
|
|
186
|
+
"baseSequence": before["sceneSequence"], "entityId": entity_id,
|
|
187
|
+
"expectedEntityVersion": (before.get("entityVersions") or {}).get(entity_id),
|
|
188
|
+
"kind": kind, "payload": payload})
|
|
189
|
+
old = {node["id"]: node for node in before["document"]["content"]["nodes"]}
|
|
190
|
+
new = {node["id"]: node for node in after["document"]["content"]["nodes"]}
|
|
191
|
+
for node_id, node in new.items():
|
|
192
|
+
if old.get(node_id) != node:
|
|
193
|
+
add("director.node.upsert", node_id, {"node": node})
|
|
194
|
+
for node_id in old.keys() - new.keys():
|
|
195
|
+
add("director.node.delete", node_id, {})
|
|
196
|
+
for key in CLIP_ARRAYS:
|
|
197
|
+
old_clips = before["document"]["content"]["timeline"]["animation"].get(key, [])
|
|
198
|
+
kept = {clip["id"] for clip in after["document"]["content"]["timeline"]["animation"].get(key, [])}
|
|
199
|
+
for clip in old_clips:
|
|
200
|
+
if clip["id"] not in kept:
|
|
201
|
+
add("director.clip.delete", clip["id"], {})
|
|
202
|
+
for entity_id in sorted(deleted_curves):
|
|
203
|
+
add("director.fcurves.delete", entity_id, {})
|
|
204
|
+
old_scene, new_scene = scene_fields(before["document"]), scene_fields(after["document"])
|
|
205
|
+
delta = {key: value for key, value in new_scene.items() if old_scene.get(key) != value}
|
|
206
|
+
if delta:
|
|
207
|
+
add("director.scene.set", "director", {"scene": {"content": {"version": 1, **delta}}})
|
|
208
|
+
if len(operations) > BATCH_OPERATION_LIMIT:
|
|
209
|
+
raise ValueError("DIRECTOR_BATCH_OPERATION_LIMIT: split at dependency boundaries (including delete cleanup)")
|
|
210
|
+
return operations, old, new
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def affected_nodes(snapshot: dict[str, Any], node_ids) -> list[str]:
|
|
214
|
+
"""Include children and bound cameras whose world state follows edited nodes."""
|
|
215
|
+
nodes = snapshot["document"]["content"]["nodes"]
|
|
216
|
+
affected = set(node_ids)
|
|
217
|
+
while True:
|
|
218
|
+
previous = len(affected)
|
|
219
|
+
for node in nodes:
|
|
220
|
+
camera = node.get("camera") or {}
|
|
221
|
+
if (node.get("parentId") in affected
|
|
222
|
+
or (camera.get("subject") or {}).get("nodeId") in affected
|
|
223
|
+
or (camera.get("lookAtTarget") or {}).get("nodeId") in affected):
|
|
224
|
+
affected.add(node["id"])
|
|
225
|
+
if node["id"] in affected:
|
|
226
|
+
affected.update(node.get("children") or [])
|
|
227
|
+
if node.get("parentId"):
|
|
228
|
+
affected.add(node["parentId"])
|
|
229
|
+
if len(affected) == previous:
|
|
230
|
+
return [node["id"] for node in nodes if node["id"] in affected]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def compact_numbers(value, ndigits=3):
|
|
234
|
+
if isinstance(value, bool) or value is None:
|
|
235
|
+
return value
|
|
236
|
+
if isinstance(value, float):
|
|
237
|
+
return round(value, ndigits)
|
|
238
|
+
if isinstance(value, list):
|
|
239
|
+
return [compact_numbers(item, ndigits) for item in value]
|
|
240
|
+
if isinstance(value, dict):
|
|
241
|
+
return {key: compact_numbers(item, ndigits) for key, item in value.items()}
|
|
242
|
+
return value
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _near(value, expected) -> bool:
|
|
246
|
+
return isinstance(value, (int, float)) and abs(float(value) - expected) < 1e-6
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def compact_node_state(row: dict[str, Any]) -> dict[str, Any]:
|
|
250
|
+
row = compact_numbers(row)
|
|
251
|
+
transform = row.get("transform")
|
|
252
|
+
if isinstance(transform, dict):
|
|
253
|
+
scale, rotation = transform.get("scale"), transform.get("rotation")
|
|
254
|
+
if isinstance(scale, dict) and all(_near(scale.get(axis, 1), 1) for axis in "xyz"):
|
|
255
|
+
transform.pop("scale", None)
|
|
256
|
+
if isinstance(rotation, dict) and all(_near(rotation.get(axis, 0), 0) for axis in "xyz"):
|
|
257
|
+
transform.pop("rotation", None)
|
|
258
|
+
if row.get("visible") is True:
|
|
259
|
+
row.pop("visible", None)
|
|
260
|
+
if row.get("locked") is False:
|
|
261
|
+
row.pop("locked", None)
|
|
262
|
+
measurement = row.get("measurement")
|
|
263
|
+
if isinstance(measurement, dict):
|
|
264
|
+
position = transform.get("position") if isinstance(transform, dict) else None
|
|
265
|
+
if measurement.get("origin") == position:
|
|
266
|
+
measurement.pop("origin", None)
|
|
267
|
+
axis = measurement.get("localForwardAxis")
|
|
268
|
+
if isinstance(axis, dict) and _near(axis.get("x", 0), 0) and _near(axis.get("y", 0), 0) \
|
|
269
|
+
and _near(axis.get("z", 0), 1):
|
|
270
|
+
measurement.pop("localForwardAxis", None)
|
|
271
|
+
surfaces = [{key: surface[key] for key in ("height", "worldWidth", "worldDepth") if key in surface}
|
|
272
|
+
for surface in measurement.get("supportSurfaces") or [] if isinstance(surface, dict)]
|
|
273
|
+
if surfaces:
|
|
274
|
+
measurement["supportSurfaces"] = surfaces
|
|
275
|
+
else:
|
|
276
|
+
measurement.pop("supportSurfaces", None)
|
|
277
|
+
for key in list(measurement):
|
|
278
|
+
if key not in {"status", "size", "bounds", "supportSurfaces", "landmarks", "origin", "localForwardAxis"}:
|
|
279
|
+
measurement.pop(key, None)
|
|
280
|
+
return row
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def node_state(node: dict[str, Any], versions: dict[str, Any]) -> dict[str, Any]:
|
|
284
|
+
row = {"nodeId": node["id"], "type": node["type"], "name": node.get("name"),
|
|
285
|
+
"entityVersion": versions.get(node["id"]), "transform": copy.deepcopy(node["transform"])}
|
|
286
|
+
if "parentId" in node:
|
|
287
|
+
row["parentId"] = node["parentId"]
|
|
288
|
+
if (node.get("metadata") or {}).get("assetId"):
|
|
289
|
+
row["assetId"] = node["metadata"]["assetId"]
|
|
290
|
+
for key in ("visible", "locked"):
|
|
291
|
+
if key in node:
|
|
292
|
+
row[key] = node[key]
|
|
293
|
+
if node.get("camera"):
|
|
294
|
+
row["camera"] = {key: copy.deepcopy(node["camera"][key])
|
|
295
|
+
for key in ("fov", "lookAt", "subject", "lookAtTarget") if key in node["camera"]}
|
|
296
|
+
animation = (node.get("character") or {}).get("animation") or {}
|
|
297
|
+
if animation.get("posePresetId"):
|
|
298
|
+
row["posePresetId"] = animation["posePresetId"]
|
|
299
|
+
return row
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Calls into the Node renderer (``director-cli``): one process per command, JSON in and out."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from topview_3d_cli.local_errors import LocalProjectError
|
|
14
|
+
from topview_3d_cli.runtime import node_env, runtime
|
|
15
|
+
|
|
16
|
+
BROWSER_COMMANDS = frozenset({"render-frames", "inspect-nodes", "apply-library-pose", "apply-library-poses"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def node_path() -> str:
|
|
20
|
+
node = shutil.which("node")
|
|
21
|
+
if not node:
|
|
22
|
+
raise LocalProjectError("NODE_UNAVAILABLE", "node is not on PATH; install Node.js 20.6 or newer")
|
|
23
|
+
return node
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def director_cli(command: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
27
|
+
"""Run one director-cli command with a JSON payload file; return its last JSON line.
|
|
28
|
+
|
|
29
|
+
Failures raise ``RENDERER_FAILED`` (``RENDER_FAILED`` for ``render-frames``) with the thrown
|
|
30
|
+
error message as the message and the full stderr (stack included) in ``details.stderr``, or
|
|
31
|
+
``BROWSER_NOT_INSTALLED`` when Chromium is missing.
|
|
32
|
+
"""
|
|
33
|
+
current = runtime()
|
|
34
|
+
node = node_path()
|
|
35
|
+
args = [node, "cli.mjs", command]
|
|
36
|
+
payload_file = None
|
|
37
|
+
if body is not None:
|
|
38
|
+
descriptor, payload_file = tempfile.mkstemp(prefix="topview3d-", suffix=".json")
|
|
39
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
40
|
+
json.dump(body, stream, ensure_ascii=False)
|
|
41
|
+
args.append(payload_file)
|
|
42
|
+
try:
|
|
43
|
+
process = subprocess.run(args, cwd=current.director_cli, env=node_env(current), check=False,
|
|
44
|
+
capture_output=True, text=True, encoding="utf-8")
|
|
45
|
+
finally:
|
|
46
|
+
if payload_file:
|
|
47
|
+
Path(payload_file).unlink(missing_ok=True)
|
|
48
|
+
if process.returncode:
|
|
49
|
+
message = (process.stderr or process.stdout).strip()[-2000:]
|
|
50
|
+
if "PLAYWRIGHT_NOT_INSTALLED" in message or "Executable doesn't exist" in message:
|
|
51
|
+
raise LocalProjectError("BROWSER_NOT_INSTALLED", "run `topview-3d-cli browser ensure` first")
|
|
52
|
+
failure = "RENDER_FAILED" if command == "render-frames" else "RENDERER_FAILED"
|
|
53
|
+
reason = renderer_reason(message)
|
|
54
|
+
raise LocalProjectError(failure, reason or f"director-cli {command} exited with {process.returncode}",
|
|
55
|
+
details={"command": command, "reason": reason, "stderr": message[-4000:]})
|
|
56
|
+
lines = [line for line in process.stdout.splitlines() if line.strip()]
|
|
57
|
+
if not lines:
|
|
58
|
+
raise LocalProjectError("RENDER_RESULT_MISSING")
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(lines[-1])
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
raise LocalProjectError("RENDER_RESULT_INVALID", str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def renderer_reason(message: str) -> str:
|
|
66
|
+
"""The thrown error message: an ``...Error:`` line, else the first line that is not a Node warning."""
|
|
67
|
+
for line in message.splitlines():
|
|
68
|
+
match = re.match(r"\s*(?:Uncaught )?[A-Za-z]*Error: (.+)", line)
|
|
69
|
+
if match:
|
|
70
|
+
return match.group(1).strip()
|
|
71
|
+
for line in message.splitlines():
|
|
72
|
+
text = line.strip()
|
|
73
|
+
if text and not text.startswith(("(node:", "(Use `node --trace")):
|
|
74
|
+
return text.removeprefix("Error: ")
|
|
75
|
+
return ""
|