"""Koru / Impulse: directed 18–85 kn planing fantasy. Kinematics, not CFD.
Run after build_scene.py (or alongside Koru-Impulse.blend). No external assets.
"""
import bpy, math, random, os
from pathlib import Path
from mathutils import Vector
ROOT=Path(os.environ.get('KORU_ROOT',Path(__file__).resolve().parent.parent))
if not (ROOT/'Koru-Impulse.blend').exists(): ROOT=Path(__file__).resolve().parent
bpy.ops.wm.open_mainfile(filepath=str(ROOT/'Koru-Impulse.blend'))
s=bpy.context.scene;s.frame_end=432;s.render.resolution_percentage=100;s.eevee.taa_render_samples=32
s.render.filepath=str(ROOT/'renders/v3/frame_');(ROOT/'renders/v3').mkdir(parents=True,exist_ok=True)
random.seed(8518)
ship=bpy.data.objects['KORU • motion rig'];water=bpy.data.objects['Water effects • translation only'];tracking=bpy.data.objects['Camera tracking • same forward speed']
def smooth(x):x=max(0,min(1,x));return x*x*(3-2*x)
def values(t):
 u=max(0,min(1,(t-2)/8));q=smooth(u)
 speed=(18+67*q)*1852/3600
 distance=18*1852/3600*t+(67*1852/3600)*(8*(u**3-.5*u**4)+max(0,t-10))
 lift=smooth((t-3)/5)
 return speed,distance,lift
for o in (ship,water,tracking):o.animation_data_clear()
for f in range(1,434):
 t=(f-1)/24;speed,x,lift=values(t)
 for o in (ship,water,tracking):o.location.x=x;o.keyframe_insert('location',index=0,frame=f)
 ship.location.z=.08+3.0*lift+.10*math.sin(t*5)*(1+.8*lift);ship.keyframe_insert('location',index=2,frame=f)
 ship.rotation_euler=(.003*math.sin(t*4),-math.radians(.3+4.6*lift+1.1*math.sin(math.pi*lift)),0);ship.keyframe_insert('rotation_euler',frame=f)
# Controller properties power the streaming wake without hundreds of large baked caches.
control=bpy.data.objects.new('Cinematic controls • directed, not force solved',None);s.collection.objects.link(control)
for f in range(1,434):
 speed,x,lift=values((f-1)/24)
 control['speed']=speed;control.keyframe_insert(data_path='["speed"]',frame=f)
 control['lift']=lift;control.keyframe_insert(data_path='["lift"]',frame=f)
# Remove the flat v2 bow ribbons and tiny spray, preserving all yacht geometry.
for o in list(bpy.data.objects):
 if o.name.startswith(('Lacing wake ribbon','Bow wave spray','Broken trailing foam','Advected bow spray')):bpy.data.objects.remove(o,do_unlink=True)
# The sea extends over the complete 580m cinematic run.
sea=bpy.data.objects['Rolling ocean surface'];sea.scale.x=2;sea.location.x=200
# Clean up the small identification line so it stays above the curved hull.
for o in bpy.data.objects:
 if o.name.startswith('New Shepard identification'):o.location.y=math.copysign(12.51,o.location.y)
# Full thrust ignites at 1.5s. Plume trails at yacht trim while wake remains on sea level.
for o in bpy.data.objects:
 if o.name.startswith('Animated exhaust envelope'):
  o.animation_data_clear()
  for f in range(1,434,2):
   t=(f-1)/24;power=smooth((t-1.5)/.6);o.scale=(max(.002,power*(1.10+.055*math.sin(f*1.71))),max(.002,power),max(.002,power));o.keyframe_insert('scale',frame=f)
foam=bpy.data.materials['Whitewater'];foam.diffuse_color=(.76,.89,.9,1)
foam.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value=(.76,.89,.9,1)
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1,radius=1)
proto=bpy.context.object;data=proto.data;bpy.data.objects.remove(proto,do_unlink=True);data.materials.append(foam)
def driver(ob,prop,idx,expr):
 d=ob.driver_add(prop,idx).driver;d.expression=expr
 for name,path in [('v','speed'),('lift','lift')]:
  va=d.variables.new();va.name=name;va.targets[0].id=control;va.targets[0].data_path='["%s"]'%path
for i in range(1450):
 side=-1 if i%2 else 1;ob=bpy.data.objects.new('Planing whitewater %04d'%i,data);s.collection.objects.link(ob);ob.parent=water
 spray=i<550;life=1.5 if spray else 6.0;phase=random.uniform(0,life)
 age='(((frame-1)/24+%.5f)%%%f)'%(phase,life)
 # As the bow rises, the wet contact moves to the aft half of the hull.
 driver(ob,'location',0,'(44-61*lift)-v*.9*%s'%age)
 spread=random.uniform(2,8) if spray else random.uniform(.5,2.7)
 driver(ob,'location',1,'%d*(4.5+%f+(1+lift)*%f*%s)'%(side,random.uniform(0,3),spread,age))
 if spray:
  height=random.uniform(1.5,4.0)
  driver(ob,'location',2,'.08+(0.3+lift)*%f*sin(3.14159*%s/%f)'%(height,age,life))
 else:driver(ob,'location',2,'.10+.08*sin(%s*6+%f)'%(age,phase))
 env='min(1,%s*5)*min(1,(%f-%s)*4)'%(age,life,age)
 driver(ob,'scale',0,'(%f+v*.012)*(0.5+lift*.8)*(%s)'%(random.uniform(.12,.42) if spray else random.uniform(.15,1.4),env))
 driver(ob,'scale',1,'%f*(0.6+lift)*(%s)'%(random.uniform(.06,.30),env))
 driver(ob,'scale',2,'%f*(0.5+lift)*(%s)'%(.22 if spray else .035,env))
# Three camera passes: rise onto plane, low running attitude, and final fast orbit.
for marker in list(s.timeline_markers):s.timeline_markers.remove(marker)
for o in list(bpy.data.objects):
 if o.type=='CAMERA' or o.name in ('Camera aim','Detail camera aim'):bpy.data.objects.remove(o,do_unlink=True)
def camera(name,lens,shots,start):
 d=bpy.data.cameras.new(name);d.lens=lens;d.clip_end=30000;ob=bpy.data.objects.new(name,d);s.collection.objects.link(ob);ob.parent=tracking
 aim=bpy.data.objects.new(name+' aim',None);s.collection.objects.link(aim);aim.parent=tracking
 con=ob.constraints.new('TRACK_TO');con.target=aim;con.track_axis='TRACK_NEGATIVE_Z';con.up_axis='UP_Y'
 for f,pos,target in shots:
  ob.location=pos;ob.keyframe_insert('location',frame=f);aim.location=target;aim.keyframe_insert('location',frame=f)
 marker=s.timeline_markers.new(name,frame=start);marker.camera=ob
 return ob
cams=[camera('01 • ignition and rise',48,[(1,(102,-238,57),(0,0,26)),(216,(45,-228,41),(-4,0,28))],1),
 camera('02 • low planing pass',38,[(217,(31,-159,21),(-7,0,15)),(312,(-10,-160,22),(-13,0,15))],217),
 camera('03 • high speed broadside',48,[(313,(-27,-238,42),(-8,0,28)),(432,(-82,-229,50),(-6,0,26))],313)]
# Permanent in-film label survives downloads and fullscreen viewing.
hud=bpy.data.materials.new('Cinematic caption');hud.use_nodes=True
nt=hud.node_tree;nt.nodes.clear();out=nt.nodes.new('ShaderNodeOutputMaterial');e=nt.nodes.new('ShaderNodeEmission');e.inputs[0].default_value=(.72,.83,.88,1);e.inputs[1].default_value=1.5;nt.links.new(e.outputs[0],out.inputs[0])
for cam in cams:
 def caption(body,x,y,size):
  d=bpy.data.curves.new(body,'FONT');d.body=body;d.size=size;d.space_character=1.15
  ob=bpy.data.objects.new(body,d);s.collection.objects.link(ob);ob.parent=cam;ob.location=(x,y,-1.1);d.materials.append(hud)
 caption('KORU / IMPULSE',-.36,.187,.011)
 caption('CINEMATIC PLANING  |  DIRECTED 18-85 KN RUN',-.36,-.204,.008)
# Add a little more crispness than the prior hazy scene, while keeping yacht palette.
s.view_settings.look='AgX - Medium High Contrast'
s.world.mist_settings.start=260;s.world.mist_settings.depth=2200
s['Animation']='18 seconds / 432 frames / 24fps. Directed 18-85 knot planing fantasy; prescribed rise, trim and speed, not CFD or a force-balance prediction.'
s['PhysicsBoundary']='Kinematic motion is deliberately exaggerated. The separate scientific model is unchanged.'
s.frame_set(280);s.camera=cams[1]
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'Koru-Planing.blend'))
s.render.resolution_percentage=60;s.render.filepath=str(ROOT/'renders/v3/preview.png');bpy.ops.render.render(write_still=True)
print('CINEMATIC_BUILD_COMPLETE',flush=True)
