113 lines
7.1 KiB
Python
113 lines
7.1 KiB
Python
"""Blender 내부에서 실제 원본·구조·평가 부피를 검사한다."""
|
||
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
|
||
|
||
|
||
def verify(source):
|
||
assert source.is_file(), "원본 파일 없음"
|
||
bpy.ops.wm.open_mainfile(filepath=str(source))
|
||
expected = {"BondPair": (905, 1637), "Interface": (2469, 3173), "DropletStretch": (3173, 4077)}
|
||
assert set(bpy.data.scenes) and set(s.name for s in bpy.data.scenes) == set(expected), "대표 Scene 누락/혼입"
|
||
assert not any(d.library for d in bpy.data.objects), "외부 객체"
|
||
assert not bpy.data.images and not bpy.data.fonts and not bpy.data.movieclips, "외부 자산"
|
||
checks = {}
|
||
for name, bounds in expected.items():
|
||
scene = bpy.data.scenes[name]
|
||
bpy.context.window.scene = scene
|
||
assert (scene.frame_start, scene.frame_end) == bounds, "실측 범위를 덮지 못함"
|
||
assert scene.camera and scene.render.fps == 50
|
||
assert (scene.render.resolution_x, scene.render.resolution_y) == (1080, 1920)
|
||
assert scene.render.engine == "BLENDER_EEVEE"
|
||
molecules = [o for o in scene.objects if o.get("role") == "molecule"]
|
||
for mol in molecules:
|
||
atoms = [o for o in mol.children if o.get("role") == "atom"]
|
||
assert sorted(o["element"] for o in atoms) == ["H", "H", "O"], "O1H2 위반"
|
||
assert all(o.type == "MESH" and len(o.data.vertices) > 20 for o in atoms)
|
||
assert all(o.data.materials[0].name==('O_Coral' if o['element']=='O' else 'H_Ivory') for o in atoms), '원자 색 재질 불일치'
|
||
oxygen = next(o for o in atoms if o["element"] == "O")
|
||
hydrogen = [o for o in atoms if o["element"] == "H"]
|
||
assert (hydrogen[0].location - oxygen.location).cross(hydrogen[1].location - oxygen.location).length > .01, "V형태 아님"
|
||
bonds = [o for o in mol.children if o.get("role") == "covalent"]
|
||
assert len(bonds) == 2 and all(o.type == "MESH" for o in bonds), "내부 실선 두 개 누락"
|
||
assert all(o.data.materials[0].name=='OH_Navy' for o in bonds), '내부 실선 재질 불일치'
|
||
assert all(min((bond.location - (h.location+oxygen.location)/2).length for h in hydrogen)<.001 for bond in bonds), "실선의 O–H 위치 불일치"
|
||
for segment in [o for o in scene.objects if o.get("role") == "hydrogen_bond"]:
|
||
h, oxygen = (scene.objects[segment[k]] for k in ("hydrogen", "oxygen"))
|
||
assert h["element"] == "H" and oxygen["element"] == "O" and h.parent != oxygen.parent, "H···O 분자간 점선 위반"
|
||
assert segment.type == "MESH" and segment.dimensions.length > 0
|
||
assert segment.data.materials[0].name=='HO_Gold', '분자간 점선 재질 불일치'
|
||
checks[name] = {"molecules": len(molecules), "frame_range": list(bounds)}
|
||
assert checks["BondPair"]["molecules"] == 2 and checks["Interface"]["molecules"] >= 8
|
||
pair = bpy.data.scenes["BondPair"]
|
||
bpy.context.window.scene = pair
|
||
pair.frame_set(1018)
|
||
before = pair.objects["PairB"].matrix_world.translation.copy()
|
||
pair.frame_set(1117)
|
||
assert (pair.objects["PairB"].matrix_world.translation - before).length > .1, "실제 3D 이동 없음"
|
||
interface = bpy.data.scenes['Interface']
|
||
bpy.context.window.scene = interface
|
||
connection_sets, neighborhoods = [], []
|
||
for frame in (2807,2907,3051):
|
||
interface.frame_set(frame)
|
||
active = sorted({(o['hydrogen'],o['oxygen']) for o in interface.objects if o.get('role')=='hydrogen_bond' and o.scale.x > .5})
|
||
connection_sets.append(active)
|
||
for segment in [o for o in interface.objects if o.get('role')=='hydrogen_bond']:
|
||
h, oxygen = (interface.objects[segment[k]] for k in ('hydrogen','oxygen'))
|
||
a,b = h.matrix_world.translation, oxygen.matrix_world.translation
|
||
assert (segment.location-a).cross(b-a).length < .002, '점선이 실제 H···O 선분에서 벗어남'
|
||
counts=[]
|
||
for name in ('Liquid4','Liquid1'):
|
||
center=interface.objects[name].matrix_world.translation
|
||
neighbors=[o.matrix_world.translation-center for o in interface.objects if o.get('role')=='molecule' and o.name!=name and (o.matrix_world.translation-center).length<2.2]
|
||
counts.append(dict(neighbors=len(neighbors),above=sum(p.z>.7 for p in neighbors)))
|
||
assert counts[0]['neighbors']>counts[1]['neighbors'] and counts[0]['above']>0 and counts[1]['above']==0, '내부/표면 이웃 배치 차이 없음'
|
||
neighborhoods.append(counts)
|
||
assert connection_sets[0] != connection_sets[-1], '실제 활성 연결 쌍이 바뀌지 않음'
|
||
scene = bpy.data.scenes["DropletStretch"]
|
||
bpy.context.window.scene = scene
|
||
droplet = scene.objects["Droplet"]
|
||
volumes, scales = [], []
|
||
for frame in (3173, 3223, 3273, 3323, 3373):
|
||
scene.frame_set(frame)
|
||
deps = bpy.context.evaluated_depsgraph_get()
|
||
evaluated = droplet.evaluated_get(deps)
|
||
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)
|
||
volumes.append(abs(bm.calc_volume(signed=True)))
|
||
scales.append(list(evaluated.scale))
|
||
bm.free()
|
||
evaluated.to_mesh_clear()
|
||
relative = max(abs(v / volumes[0] - 1) for v in volumes)
|
||
assert relative <= .01, "중간 프레임 포함 부피 변화 1% 초과"
|
||
assert abs(scales[-1][0] - scales[0][0]) > .5, "변형이 실제 평가되지 않음"
|
||
anchors = {}
|
||
for name, logical in (("BondPair", 1219), ("Interface", 3050), ("DropletStretch", 3372)):
|
||
scene = bpy.data.scenes[name]
|
||
bpy.context.window.scene = scene
|
||
scene.frame_set(logical + 1)
|
||
anchors[name] = {"logical_frame":logical,"labels":{}}
|
||
for obj in scene.objects:
|
||
if obj.get("label"):
|
||
p = world_to_camera_view(scene, scene.camera, obj.matrix_world.translation)
|
||
anchors[name]['labels'][obj["label"]] = {"x": round(p.x * 1080, 2), "y": round((1-p.y)*1920, 2), "depth": round(p.z, 2)}
|
||
return {"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), "blender_version": bpy.app.version_string, "scenes": checks, "interface_frames_blender":[2807,2907,3051], "interface_active_pairs":connection_sets,"interface_neighborhoods":neighborhoods,"volume_frames_blender": [3173,3223,3273,3323,3373], "volumes": volumes, "relative_volume_change": relative, "evaluated_scales": scales, "label_anchors": anchors, "text_baked": False, "physical_simulation": False}
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--source", required=True)
|
||
parser.add_argument("--out", required=True)
|
||
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:])
|
||
result = verify(Path(args.source).resolve())
|
||
Path(args.out).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print("MODEL_GREEN", result["source_sha256"], result["relative_volume_change"])
|