Skip to content

Commit 5798259

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign vse-cut-list as a program monitor over a timeline console
The editing bay now has a larger thin-bezel reference monitor on a walnut desk. In front of it sits a tilted timeline console that makes the cut list physical: one lane per channel (A, B, C, TXT, GC), each strip a coloured block laid out from its own [left_handle, right_handle) through the same strip_span accessor the check asserts. Raised handle notches mark both ends, a frame ruler runs along the back, and the playhead sits at frame 29. The GC block ramps from input_1's colour to input_2's, so the wiring reads too. Also fixes the program frame itself. render_still built the cut list at the factory 1920x1080 and rendered at 1280x720, and on 5.2 COLOR strips bake width/height at new_effect time, so every mosaic cell sat off its crosshair slot in the old still. The still now sets 1280x720 before building. The checks are unchanged. Evidence (live runs, local): - .scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check + --check-pixels exit 0, pixels TL(0.851,0.102,0.22) TR(0.059,0.749,0.8) BL(0.949,0.62,0.102) BR blend (0.341,0.349,0.463). --swap-inputs exits 7 ("GC wiring drifted: inputs=(T2, T1)"). - .scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check + --check-pixels exit 0, same pixels; the --output render passes framing (fill y=0.900, margins l=0.169 r=0.188 b=0.061 t=0.039). - Framing on 5.2.1 (Bay scene): fill x=0.644 y=0.900, same margins. - Contact sheet (docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp): candidate luma 0.219 / stage 0.182 / warmth +0.120 / sat 0.143, against armature-bend 0.279/0.213/-0.428/0.144 (the pre-redesign hero in this worktree), damped-track-aim 0.151/0.145/+0.277/0.231, and bmesh-gear 0.236/0.177/+0.303/0.250. Stage and luma sit inside the set, warmth is positive, and the cut list reads at thumbnail size. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8b83b01 commit 5798259

7 files changed

Lines changed: 169 additions & 57 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ still.
492492
</tr>
493493
<tr>
494494
<td width="46%" valign="middle">
495-
<a href="examples/vse-cut-list/"><img src="examples/vse-cut-list/preview.webp" alt="VSE cut list: a two-by-two program wall in a dark studio — crimson, teal, and amber color strips plus the mid cross-blend cell, composited by the sequencer over a scene strip, with the span caption below" /></a>
495+
<a href="examples/vse-cut-list/"><img src="examples/vse-cut-list/preview.webp" alt="VSE cut list: a reference monitor showing the two-by-two program wall — crimson, teal, and amber color strips plus the mid cross-blend cell, composited by the sequencer over a scene strip — above a timeline console laying out the same strip spans as coloured blocks with handle notches and a playhead" /></a>
496496
</td>
497497
<td valign="middle">
498498

17.5 KB
Loading
-5.17 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@
316316
"name": "vse-cut-list",
317317
"dir": "examples/vse-cut-list",
318318
"teaches": "The sequencer API rename from 4.5 LTS to 5.x — strips (never .sequences), new_effect ending in length= vs frame_end=, and left_handle/right_handle/duration replacing the deprecated frame_final_*. A deterministic cut list — color programs, a clamped GAMMA_CROSS, a scene strip, a text strip — asserted before and after save/reload.",
319-
"alt": "A monitor on a desk labelled PROGRAM, showing a sequencer frame of four colour blocks with a cut-list caption strip.",
319+
"alt": "A reference monitor on a walnut editing desk showing a sequencer frame of crimson, teal, amber and cross-blend cells, with a tilted timeline console in front laying out the same strips as coloured blocks under an orange playhead.",
320320
"witnessesFix": "Each side hard-fails the other's spelling: TypeError on the wrong end kwarg, AttributeError on .sequences. The cross clamps to the source overlap, effect strips consume their inputs only when stacked above them, and a same-scene scene strip renders transparent.",
321321
"hero": "docs/gallery/assets/vse-cut-list-hero.webp",
322322
"preview": "examples/vse-cut-list/preview.webp",

‎examples/vse-cut-list/README.md‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,22 @@ are the genuine sequencer output (evidence); only the bay around them is
9999
staged (presentation). Rendered locally with EEVEE (GPU host); the checks
100100
and `--check-pixels` need no GPU (Cycles CPU).
101101

102-
In the bay, each part bites the one it rests on. The monitor's neck rises
103-
0.10 m into the bezel, the stand base sits 5 mm into the desk, and the
104-
"PROGRAM" caption stands on the desk's front face. The neck used to butt its
105-
top face onto the bezel's bottom on one plane, with half its depth hanging
106-
out behind, and the caption lay flat on the desk top 6 mm up, where the
107-
camera read it as a smear. None of this touches the sequencer frame or any
108-
check.
102+
In front of the monitor, a tilted timeline console makes the same cut list
103+
physical: one lane per channel (A, B, C, TXT, GC), each strip a coloured
104+
block spanning its `[left_handle, right_handle)` read back through the same
105+
`strip_span` accessor the check asserts, with raised handle notches at both
106+
ends, a frame ruler, and the playhead at frame 29. The GC block ramps from
107+
its `input_1` colour to its `input_2` colour, so the wiring reads too. The
108+
console is laid out from the strips, never from constants — a span that
109+
drifted would move its block. The monitor's neck is seated into the monitor
110+
back and its foot sits on the walnut desk top; none of the bay touches the
111+
sequencer frame or any check.
112+
113+
The still builds the cut list at the 1280×720 render resolution. 5.2 COLOR
114+
strips bake `width`/`height` from the render resolution at `new_effect`
115+
time (the same trap `--check-pixels` documents), and the previous still
116+
built at the factory 1920×1080 and rendered at 1280×720, which pushed every
117+
mosaic cell off its crosshair slot.
109118

110119
## Framing measurement
111120

‎examples/vse-cut-list/preview.webp‎

17 KB
Loading

‎examples/vse-cut-list/vse_cut_list.py‎

Lines changed: 151 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
GC_SPAN = (25, 33) # 8-frame cross, exactly the T1/T2 overlap (clamped to it)
7272
TXT_SPAN = (1, 57) # caption lives for the whole color timeline
7373
SCENE_END = 250 # explicit scene frame_end -> scene strip spans [1, 251)
74+
SCENE_END_VIS = 57 # the console ruler spans the colour timeline, frames 1..57
7475
TXT_BODY = "CUT LIST A 001-032 B 025-056 CROSS 025-032 @ F29"
7576

7677
RENDER_W, RENDER_H = 1280, 720
@@ -440,55 +441,79 @@ def render_frame(sc, path, engine, w, h):
440441
return os.path.exists(path) and os.path.getsize(path) > 0
441442

442443

443-
def build_bay(sc, frame_path):
444-
"""The editing-bay presentation: a reference monitor on a desk in the
445-
dark studio, its screen showing the AUTHENTIC sequencer frame. The pixels
446-
on the screen are the evidence (rendered by the VSE above); the bay is
447-
only the designed presentation around them."""
444+
def build_bay(sc, frame_path, coll):
445+
"""The editing-bay presentation: a reference monitor on an editing desk in
446+
the dark studio, its screen showing the AUTHENTIC sequencer frame, and in
447+
front of it a tilted timeline console whose strip blocks are laid out from
448+
the cut list's own spans (read back through ``strip_span`` — the canonical
449+
accessors the check asserts). The screen pixels are the evidence rendered
450+
by the VSE; the console is the same timeline made physical so the cut list
451+
reads in the thumbnail."""
448452
import bmesh
449453

450-
def box(name, size, loc, mat):
454+
def mesh_box(name, size, loc, mat, bevel=0.0, parent=None, rot=None):
451455
me = bpy.data.meshes.new(name)
452456
bm = bmesh.new()
453457
try:
454458
bmesh.ops.create_cube(bm, size=1.0,
455459
matrix=mathutils.Matrix.Diagonal((*size, 1.0)))
460+
if bevel > 0.0:
461+
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=bevel, segments=2,
462+
profile=0.5, affect='EDGES')
456463
bm.to_mesh(me)
457464
finally:
458465
bm.free()
459466
me.materials.append(mat)
460467
ob = bpy.data.objects.new(name, me)
461468
ob.location = loc
469+
if rot is not None:
470+
ob.rotation_euler = rot
471+
if parent is not None:
472+
ob.parent = parent
462473
sc.collection.objects.link(ob)
463474
return ob
464475

465-
def pbr(name, base, metallic, roughness):
476+
def pbr(name, base, metallic, roughness, emit=None, strength=0.0):
466477
mat = bpy.data.materials.new(name)
467478
mat.use_nodes = True
468479
b = mat.node_tree.nodes["Principled BSDF"]
469480
b.inputs["Base Color"].default_value = (*base, 1.0)
470481
b.inputs["Metallic"].default_value = metallic
471482
b.inputs["Roughness"].default_value = roughness
483+
if emit is not None:
484+
b.inputs["Emission Color"].default_value = (*emit, 1.0)
485+
b.inputs["Emission Strength"].default_value = strength
472486
return mat
473487

474-
gunmetal = pbr("BayMetal", (0.08, 0.09, 0.10), 0.7, 0.40)
475-
deskmat = pbr("DeskTop", (0.05, 0.05, 0.06), 0.0, 0.80)
476-
477-
# desk slab, stand, and the monitor bezel (screen face toward -Y)
478-
# Each part bites the one it rests on. The neck used to butt its top face
479-
# onto the bezel's bottom face (one plane, no joint) with half its depth
480-
# hanging out behind, and the base sat exactly on the desk top.
481-
desk_top, base_h, base_bite = 0.72, 0.06, 0.005
482-
bezel_z, bezel_h, bezel_d = 1.89, 1.30, 0.09
483-
neck_bite, neck_d = 0.10, 0.07
484-
base_z0 = desk_top - base_bite
485-
neck_z0 = base_z0 + base_h - base_bite
486-
neck_z1 = bezel_z - bezel_h / 2 + neck_bite
487-
box("Desk", (2.80, 1.10, desk_top), (0.0, 0.0, desk_top / 2), deskmat)
488-
box("StandBase", (0.70, 0.50, base_h), (0.0, 0.05, base_z0 + base_h / 2), gunmetal)
489-
box("Stand", (0.18, neck_d, neck_z1 - neck_z0),
490-
(0.0, bezel_d / 2 - neck_d / 2 + 0.02, 0.5 * (neck_z0 + neck_z1)), gunmetal)
491-
box("Bezel", (2.06, bezel_d, bezel_h), (0.0, 0.0, bezel_z), gunmetal)
488+
gunmetal = pbr("BayMetal", (0.06, 0.065, 0.075), 0.75, 0.38)
489+
satin = pbr("BayBlack", (0.018, 0.019, 0.022), 0.0, 0.55)
490+
walnut = pbr("Walnut", (0.13, 0.065, 0.03), 0.0, 0.45)
491+
bed = pbr("ConsoleBed", (0.025, 0.027, 0.032), 0.1, 0.6)
492+
lane = pbr("TrackLane", (0.045, 0.048, 0.056), 0.0, 0.7)
493+
494+
# ---- desk: walnut top on a satin-black cabinet
495+
desk_top = 0.74
496+
mesh_box("DeskTop", (3.4, 1.8, 0.06), (0.0, 0.0, desk_top - 0.03), walnut, bevel=0.012)
497+
mesh_box("DeskBody", (3.2, 1.6, desk_top - 0.06), (0.0, 0.05, (desk_top - 0.06) / 2),
498+
satin, bevel=0.01)
499+
500+
# ---- reference monitor: thin bezel, chin, angled stand
501+
scr_w, scr_h = 2.24, 1.26 # 16:9 active area
502+
bez_w, bez_h, bez_d = scr_w + 0.07, scr_h + 0.07, 0.06
503+
mon_y, mon_z = 0.34, desk_top + 0.30 + bez_h / 2
504+
mesh_box("Bezel", (bez_w, bez_d, bez_h), (0.0, mon_y, mon_z), gunmetal, bevel=0.012)
505+
mesh_box("Chin", (bez_w, bez_d + 0.01, 0.05), (0.0, mon_y - 0.005, mon_z - bez_h / 2 - 0.02),
506+
gunmetal, bevel=0.01)
507+
mesh_box("MonitorBack", (bez_w * 0.7, 0.10, bez_h * 0.6),
508+
(0.0, mon_y + 0.07, mon_z - 0.05), satin, bevel=0.03)
509+
mesh_box("StandNeck", (0.16, 0.06, 0.52),
510+
(0.0, mon_y + 0.10, desk_top + 0.28), gunmetal, bevel=0.01)
511+
mesh_box("StandFoot", (0.62, 0.40, 0.03), (0.0, mon_y + 0.10, desk_top + 0.015),
512+
gunmetal, bevel=0.01)
513+
# a thin tally lamp on the chin: the bay is live
514+
mesh_box("Tally", (0.10, 0.012, 0.012), (bez_w / 2 - 0.16, mon_y - bez_d / 2 - 0.004,
515+
mon_z - bez_h / 2 - 0.02),
516+
pbr("Tally", (0.9, 0.2, 0.15), 0.0, 0.4, emit=(1.0, 0.18, 0.1), strength=6.0))
492517

493518
# the screen: one unlit quad sampling the authentic frame end to end —
494519
# emission only, so the VSE pixels read exactly as the sequencer wrote them
@@ -508,8 +533,10 @@ def pbr(name, base, metallic, roughness):
508533
sme = bpy.data.meshes.new("Screen")
509534
bm = bmesh.new()
510535
try:
511-
q = [bm.verts.new(p) for p in ((-0.96, -0.048, 1.35), (0.96, -0.048, 1.35),
512-
(0.96, -0.048, 2.43), (-0.96, -0.048, 2.43))]
536+
y = mon_y - bez_d / 2 - 0.002
537+
x0, x1 = -scr_w / 2, scr_w / 2
538+
z0, z1 = mon_z - scr_h / 2, mon_z + scr_h / 2
539+
q = [bm.verts.new(p) for p in ((x0, y, z0), (x1, y, z0), (x1, y, z1), (x0, y, z1))]
513540
# explicit UVs, not Generated: the quad is flat in Y, so Generated's
514541
# image-V is a constant and the screen samples one strip of the frame
515542
uv_layer = bm.loops.layers.uv.new("UVMap")
@@ -523,34 +550,105 @@ def pbr(name, base, metallic, roughness):
523550
sob = bpy.data.objects.new("Screen", sme)
524551
sc.collection.objects.link(sob)
525552

526-
# a small desk-lip caption, the bay's only typography
527-
cmat = pbr("CaptionGrey", (0.42, 0.44, 0.48), 0.2, 0.6)
528-
cu = bpy.data.curves.new("BayCaption", "FONT")
529-
cu.body = "PROGRAM"
530-
cu.align_x = "CENTER"
531-
cu.size = 0.10
532-
cu.extrude = 0.004
533-
cob = bpy.data.objects.new("BayCaption", cu)
534-
# upright on the desk's front face, set 1 mm into it: laid flat on the
535-
# top it floated 6 mm up and read as a smear at the camera's grazing angle
536-
cob.location = (0.0, -0.55 - cu.extrude + 0.001, desk_top - 0.16)
537-
cob.rotation_euler = (math.radians(90.0), 0.0, 0.0)
538-
cob.data.materials.append(cmat)
539-
sc.collection.objects.link(cob)
553+
# ---- timeline console: the cut list made physical. A tilted bed on the
554+
# desk; one lane per channel; each strip a coloured block spanning its
555+
# [left_handle, right_handle) with raised handle notches at both ends.
556+
console = bpy.data.objects.new("Console", None)
557+
console.location = (0.0, -0.44, desk_top + 0.09)
558+
console.rotation_euler = (math.radians(22.0), 0.0, 0.0)
559+
sc.collection.objects.link(console)
560+
bed_w, bed_d = 2.9, 0.80
561+
mesh_box("ConsoleBed", (bed_w, bed_d, 0.05), (0.0, 0.0, 0.0), bed, bevel=0.012,
562+
parent=console)
563+
mesh_box("ConsoleWedge", (bed_w - 0.04, bed_d - 0.1, 0.10), (0.0, 0.04, -0.06), satin,
564+
parent=console)
565+
f0, f1 = 1, SCENE_END_VIS
566+
x_left, x_right = -bed_w / 2 + 0.26, bed_w / 2 - 0.08
567+
568+
def fx(frame):
569+
return x_left + (x_right - x_left) * (frame - f0) / (f1 - f0)
570+
571+
# lanes front (low channel) to back (high channel)
572+
lanes = (("A", A_RGB), ("B", B_RGB), ("C", C_RGB), ("TXT", (0.86, 0.84, 0.80)),
573+
("GC", None))
574+
lane_pitch = (bed_d - 0.10) / len(lanes)
575+
top = 0.025
576+
for i, (name, rgb) in enumerate(lanes):
577+
ly = -bed_d / 2 + 0.07 + lane_pitch * (i + 0.5)
578+
mesh_box(f"Lane.{name}", (x_right - x_left + 0.04, lane_pitch * 0.80, 0.006),
579+
((x_left + x_right) / 2, ly, top + 0.003), lane, parent=console)
580+
s0, s1, _dur = strip_span(coll[name])
581+
s1 = min(s1, f1)
582+
a, b = fx(s0), fx(s1)
583+
if rgb is None:
584+
# the cross: a ramp from its first input's colour to its second's
585+
gm = bpy.data.materials.new("Strip.GC")
586+
gm.use_nodes = True
587+
gnt = gm.node_tree
588+
gb = gnt.nodes["Principled BSDF"]
589+
gtc = gnt.nodes.new("ShaderNodeTexCoord")
590+
sep = gnt.nodes.new("ShaderNodeSeparateXYZ")
591+
ramp = gnt.nodes.new("ShaderNodeValToRGB")
592+
gin = coll["GC"]
593+
ramp.color_ramp.elements[0].color = (*gin.input_1.color, 1.0)
594+
ramp.color_ramp.elements[1].color = (*gin.input_2.color, 1.0)
595+
gnt.links.new(gtc.outputs["Generated"], sep.inputs["Vector"])
596+
gnt.links.new(sep.outputs["X"], ramp.inputs["Fac"])
597+
gnt.links.new(ramp.outputs["Color"], gb.inputs["Base Color"])
598+
gnt.links.new(ramp.outputs["Color"], gb.inputs["Emission Color"])
599+
gb.inputs["Emission Strength"].default_value = 0.35
600+
gb.inputs["Roughness"].default_value = 0.35
601+
smat_i = gm
602+
else:
603+
smat_i = pbr(f"Strip.{name}", rgb, 0.0, 0.35, emit=rgb, strength=0.35)
604+
h = 0.034
605+
mesh_box(f"Strip.{name}", (b - a - 0.012, lane_pitch * 0.66, h),
606+
((a + b) / 2, ly, top + 0.006 + h / 2), smat_i, bevel=0.006,
607+
parent=console)
608+
# handle notches: lighter raised tabs at the left and right handles
609+
notch = pbr(f"Handle.{name}", (0.75, 0.76, 0.78), 0.6, 0.3)
610+
for hx in (a + 0.018, b - 0.018):
611+
mesh_box(f"Handle.{name}", (0.022, lane_pitch * 0.70, h + 0.014),
612+
(hx, ly, top + 0.006 + (h + 0.014) / 2), notch, bevel=0.004,
613+
parent=console)
614+
# lane label in the left gutter
615+
cu = bpy.data.curves.new(f"Label.{name}", "FONT")
616+
cu.body = name
617+
cu.align_x = "RIGHT"
618+
cu.align_y = "CENTER"
619+
cu.size = 0.075
620+
cu.extrude = 0.002
621+
lab = bpy.data.objects.new(f"Label.{name}", cu)
622+
lab.location = (x_left - 0.05, ly, top + 0.004)
623+
lab.parent = console
624+
cu.materials.append(pbr(f"LabelInk.{name}", (0.55, 0.57, 0.62), 0.1, 0.5))
625+
sc.collection.objects.link(lab)
626+
627+
# frame ruler along the back edge and the playhead at the sampled frame
628+
tick = pbr("Tick", (0.35, 0.36, 0.40), 0.4, 0.5)
629+
for fr in range(f0, f1 + 1, 8):
630+
mesh_box("Tick", (0.006, 0.03 if (fr - 1) % 16 else 0.05, 0.004),
631+
(fx(fr), bed_d / 2 - 0.035, top + 0.002), tick, parent=console)
632+
head = pbr("Playhead", (0.95, 0.30, 0.18), 0.0, 0.3, emit=(1.0, 0.32, 0.16), strength=2.0)
633+
px = fx(SAMPLE_FRAME)
634+
mesh_box("Playhead", (0.006, bed_d - 0.06, 0.05), (px, 0.0, top + 0.025), head,
635+
parent=console)
636+
mesh_box("PlayheadCap", (0.05, 0.03, 0.03), (px, bed_d / 2 - 0.02, top + 0.07), head,
637+
bevel=0.006, parent=console)
540638

541639

542640
def render_bay(sc, path, engine, w, h):
543641
"""Render the editing bay. The camera moves to a three-quarter view of
544642
the monitor; everything else reuses the house stage."""
545643
cam_data = bpy.data.cameras.new("BayCam")
546-
cam_data.lens = 52.0
644+
cam_data.lens = 50.0
547645
cam = bpy.data.objects.new("BayCam", cam_data)
548-
# Pulled back from (2.0, -5.2, 1.72) so the bezel top and desk base
549-
# clear the frame edges (framing gate on the Bay scene).
550-
cam.location = (3.05, -7.9, 2.0)
646+
# Three-quarter and well above desk height, so the tilted timeline
647+
# console in front of the monitor presents its lanes to the lens.
648+
cam.location = (2.4, -7.3, 3.45)
551649
sc.collection.objects.link(cam)
552650
target = bpy.data.objects.new("BayAim", None)
553-
target.location = (0.0, 0.0, 1.20)
651+
target.location = (0.0, -0.05, 0.98)
554652
sc.collection.objects.link(target)
555653
con = cam.constraints.new("TRACK_TO")
556654
con.target = target
@@ -599,7 +697,12 @@ def render_still(path, engine):
599697
"""Two passes: the authentic sequencer frame (evidence), then the
600698
editing-bay presentation with that exact frame on the monitor screen."""
601699
sc = bpy.context.scene
602-
build_cut_list(sc)
700+
# 5.2 COLOR strips bake width/height from the render resolution at
701+
# new_effect time (see check_pixels); building at the factory 1920x1080
702+
# and rendering at 1280x720 put every mosaic cell in the wrong place.
703+
sc.render.resolution_x = RENDER_W
704+
sc.render.resolution_y = RENDER_H
705+
coll = build_cut_list(sc)
603706
build_stage(bpy.data.scenes["Stage"])
604707
tmp = tempfile.mkdtemp(prefix="vse_frame_")
605708
try:
@@ -608,7 +711,7 @@ def render_still(path, engine):
608711
return 11
609712
bay = bpy.data.scenes.new("Bay")
610713
build_stage(bay) # the house dark studio, lights included
611-
build_bay(bay, frame_path)
714+
build_bay(bay, frame_path, coll)
612715
return render_bay(bay, path, engine, RENDER_W, RENDER_H)
613716
finally:
614717
shutil.rmtree(tmp, ignore_errors=True) # pixels live on in the blend

0 commit comments

Comments
 (0)