"""정식 물 영상의 실제 9개 장면·기준 재사용·부피·주석 위치를 검사한다.""" import argparse import hashlib import json from pathlib import Path import sys import bpy import bmesh from bpy_extras.object_utils import world_to_camera_view sys.path.insert(0,str(Path(__file__).resolve().parent)) from verify_model import verify ROOT=Path(__file__).resolve().parents[3] PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension' CUTS=json.loads((PROJECT/'stages/07-cuts/r1/artifact.json').read_text(encoding='utf-8'))['payload']['cuts'] SCENES=json.loads((PROJECT/'stages/08-scenes/r3/artifact.json').read_text(encoding='utf-8'))['payload']['scenes'] EXPECTED={} for cut,plan in zip(CUTS,SCENES): name=plan['start_state'].split(';')[0].split('=')[1] start,end=cut['start_ms']//20+1,cut['end_ms']//20+1 EXPECTED[name]=(min(EXPECTED.get(name,(start,end))[0],start),end) def volume(obj): evaluated=obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) mesh=evaluated.to_mesh() bm=bmesh.new() bm.from_mesh(mesh) assert all(e.is_manifold for e in bm.edges), '닫힌 물방울 mesh 아님' bm.transform(evaluated.matrix_world) value=abs(bm.calc_volume(signed=True)) bm.free() evaluated.to_mesh_clear() return value def run(source,animated): result=verify(source,EXPECTED) assert bpy.context.scene.get('approved_source_sha256') or bpy.data.scenes['BondPair'].get('approved_source_sha256'), '승인 원본 연결 없음' assert not bpy.data.libraries and not bpy.data.images and not bpy.data.fonts and not bpy.data.sounds comparison=bpy.data.scenes['EqualVolumeComparison'] bpy.context.window.scene=comparison comparison.frame_set(comparison.frame_start) volumes=[volume(comparison.objects[name]) for name in ['ComparisonLong','ComparisonRound']] assert abs(volumes[0]/volumes[1]-1)<.01, '비교 두 물방울 부피 불일치' result['comparison_volumes']=volumes anchors=[] for cut,plan in zip(CUTS,SCENES): scene=bpy.data.scenes[plan['start_state'].split(';')[0].split('=')[1]] bpy.context.window.scene=scene start,end=cut['start_ms']//20,cut['end_ms']//20 samples=[] for logical in sorted(set([start,(start+end)//2,end])): scene.frame_set(logical+1) labels={} for obj in scene.objects: if obj.get('anchor'): point=world_to_camera_view(scene,scene.camera,obj.matrix_world.translation) assert 00, '주석 위치 화면 밖' labels[obj['anchor']]=dict(object_name=obj.name,x=round(point.x*1080,3),y=round((1-point.y)*1920,3),depth=round(point.z,3)) assert labels, '컷 주석 앵커 누락' samples.append(dict(logical_frame=logical,blender_frame=logical+1,labels=labels)) anchors.append(dict(cut_id=cut['id'],scene_name=scene.name,start_frame=start,end_frame=end,samples=samples)) result['cut_anchors']=anchors if animated: assert bpy.data.scenes['BondPair'].get('reference_source_sha256'), '실제 10 원본 연결 없음' scene=bpy.data.scenes['DropletStretch'] bpy.context.window.scene=scene values=[] for logical in range(3172,4077): scene.frame_set(logical+1) values.append(volume(scene.objects['Droplet'])) relative=max(abs(v/values[0]-1) for v in values) assert relative<.01, '복귀 포함 물방울 부피 1% 초과' scene.frame_set(3853) assert max(abs(s-1) for s in scene.objects['Droplet'].scale)<.001, 'c21 구형 복귀 없음' result['full_drop_volume']=dict(logical_start=3172,logical_end=4076,frames=905,relative_change=relative) network=bpy.data.scenes['LiquidNetwork'] bpy.context.window.scene=network pair_sets=[] maximum=0 for frame in range(1637,2470): network.frame_set(frame) pairs=set() for obj in network.objects: if obj.get('role')!='hydrogen_bond' or obj.scale.x<=.0011: continue h,o=network.objects[obj['hydrogen']],network.objects[obj['oxygen']] assert h.parent!=o.parent and h['element']=='H' and o['element']=='O' a,b=h.matrix_world.translation,o.matrix_world.translation error=(obj.location-a).cross(b-a).length assert error<.003, '액체 점선이 실제 H···O 선분에서 벗어남' maximum=max(maximum,error) pairs.add((h.name,o.name)) if frame in [1637,2019,2257,2469]: pair_sets.append(sorted(pairs)) assert pair_sets[0]!=pair_sets[1] and pair_sets[1]!=pair_sets[2], '액체 연결 반복 교체 없음' result['network_frames']=833 result['network_active_pairs']=pair_sets result['network_maximum_line_error']=maximum result['animated']=animated result['result']='GREEN' return result if __name__=='__main__': parser=argparse.ArgumentParser() parser.add_argument('--source',required=True) parser.add_argument('--out',required=True) parser.add_argument('--animated',action='store_true') args=parser.parse_args(sys.argv[sys.argv.index('--')+1:]) result=run(Path(args.source),args.animated) Path(args.out).write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') print('정식 원본 검사 GREEN',result['source_sha256'])