"""Critique 2: does the exported door animation really carry the end with the door? Read-only. Imports each A_DoorRobotArm___OpenClose.fbx into an empty scene and follows the tool bone (and A's distal fingers) over frames 1-120: its travel from frame 1 against the door's (1.22 m inboard, 6.14 m along: 6.259 m), and its change of orientation (the door never turns). Also: the door positions in the .blend at the key frames, text blocks that would make Blender warn with auto-run off, and whether the context's four absolute-path images are packed. blender -b --factory-startup --python previews/critique2/crit2_fbxcheck.py -- """ import json import math import os import sys import bpy from mathutils import Vector OUT = sys.argv[sys.argv.index("--") + 1] HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(os.path.dirname(HERE)) FBX = os.path.join(ROOT, "fbx") R = {"anim": {}} for ver in ("A", "B"): for key in ("AFT", "FORE"): f = "A_DoorRobotArm_%s_%s_OpenClose.fbx" % (ver, key) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(FBX, f), anim_offset=0.0) arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') sc = bpy.context.scene tool = "tool_a_hub" if ver == "A" else "tool_b" names = [tool, "flange", "carriage"] + (["finger_%s_distal" % w for w in ("top", "left", "bottom", "right")] if ver == "A" else []) names = [n for n in names if n in arm.pose.bones] m0 = {} rot = {n: 0.0 for n in names} rigid = {n: 0.0 for n in names} path = {} for fr in range(1, 121): sc.frame_set(fr) M = {n: arm.matrix_world @ arm.pose.bones[n].matrix for n in names} if fr == 1: m0 = {n: M[n].copy() for n in names} for n in names: q = m0[n].to_quaternion().inverted() @ M[n].to_quaternion() a = math.degrees(q.angle) rot[n] = max(rot[n], min(a, 360 - a)) # rigid with the tool: the part's pose relative to the tool's must not change r0 = m0[tool].inverted() @ m0[n] r1 = M[tool].inverted() @ M[n] rigid[n] = max(rigid[n], (r1.translation - r0.translation).length * 1000) if fr in (1, 12, 48, 72, 108, 120): path[fr] = {n: [round(x, 4) for x in (M[n].translation - m0[n].translation)] for n in (tool, "carriage") if n in M} d48 = (path[48][tool]) R["anim"][f] = {"bones": len(arm.pose.bones), "tool": tool, "tool_travel_at_48_m": round(Vector(d48).length, 4), "tool_path": path, "max_orientation_change_deg": {n: round(v, 4) for n, v in rot.items()}, "max_drift_from_tool_mm": {n: round(v, 3) for n, v in rigid.items()}, "frame_range": [sc.frame_start, sc.frame_end], "action_range": list(arm.animation_data.action.frame_range) if arm.animation_data and arm.animation_data.action else None} # the .blend: doors at the key frames, text blocks, the four context images bpy.ops.wm.open_mainfile(filepath=os.path.join(ROOT, "DoorRobotArm.blend")) sf = bpy.data.scenes["DoorArm | Ship fit"] doors = {} for n in ("Moonpool_Door_Aft", "Moonpool_Door_Fore"): o = bpy.data.objects[n] row = {} for fr in (1, 12, 30, 48, 72, 90, 108, 120): sf.frame_set(fr) row[fr] = [round(x, 4) for x in o.evaluated_get(bpy.context.evaluated_depsgraph_get()).matrix_world.translation] doors[n] = row R["doors"] = doors R["scene_ship_fit"] = {"frames": [sf.frame_start, sf.frame_end], "fps": sf.render.fps, "markers": sorted([m.frame, m.name] for m in sf.timeline_markers)} R["texts"] = [[t.name, t.use_module] for t in bpy.data.texts] R["context_images"] = [[im.name, im.filepath, im.packed_file is not None, im.has_data, im.size[:]] for im in bpy.data.images if not im.filepath.startswith("//") and im.source == 'FILE'] users = {} for m in bpy.data.materials: if m.node_tree: for nd in m.node_tree.nodes: if nd.type == 'TEX_IMAGE' and nd.image and not nd.image.filepath.startswith("//"): users.setdefault(nd.image.name, []).append([m.name, any(l.from_node == nd for l in m.node_tree.links)]) R["context_image_users_linked"] = users with open(OUT, "w") as fh: json.dump(R, fh, indent=1) print("[crit2] fbxcheck written", OUT, flush=True)