Rendered headless by the showcase piece itself — click to zoom.
blender --background --python showcase/hay-bale/hay_bale.py --
Copy
@@ -282,27 +341,27 @@
hay-bale
It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).
Intended size: 0.90 × 0.48 × 0.38 m nominal bale. The loaf measures 0.921 × 0.508 × 0.393 m with its pillow bulge, and the envelope is 0.921 × 0.508 × 0.417 m once the belts and knot loops are on it.
-
How the belts are built
+
How the belts are built#
The belt is not a constant rectangle . The loaf's cross-section at a belt station is rounded, bulged and waisted, so a rectangle stands proud at the middle of each face and is swallowed at the corners — which is exactly what the previous build did, leaving the wrap hanging 2.9–8.3 mm above the floor with a visible notch at the bottom edge.
Instead the loaf is built, bevelled and grounded first , then each belt raycasts the finished loaf at its own station (loaf_profile, BELT_SEGS samples) and follows that profile at a named bite. Where the run passes near the ground it is pressed flush and a little past flush (BELT_PRESS_H, BELT_UNDER_SINK) so the loop closes underneath without breaking the Z=0 plane the bale is grounded on.
Every closed-form term in the loaf shaping is even in x . An odd term (sin(x*24)) put the two belt stations on different surface heights and left the wraps 5.4 mm out of step, which no budget could see.
-
Budgets
+
Budgets#
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file. Measurements below are identical on 4.5.11, 5.1.2 and 5.2.1 — construction uses no RNG and no version-dependent operator.
Axis Declared Measured (4.5.11 / 5.1.2 / 5.2.1) Base triangles 1700–2400 2044 on all three LOD1 ratio 0.32–0.62 of base 0.5000 LOD2 ratio 0.10–0.35 of base 0.2192 Materials exactly 2 distinct, ≥24 hay, ≥24 twine 2 slots, 1436 hay, 304 twine UVs in 0..1, AABB overlap ≤ 1e-5 in range, overlap 0 Outer AABB (0.921, 0.508, 0.417) m ± 0.01 (0.9208, 0.5075, 0.4165) Grounded zmin within 1e-4 of 0 0.000000 Hygiene loose/nonman/zero-area/doubles/n-gons/z-fight = 0 all 0 Shell count exactly 5 (loaf, 2 belts, 2 loops) 5 Belt wrap zmin each belt shell ≤ 0.006 m 0.001460 both Belt seat depth every station in [0.002, 0.020] m 0.00400..0.01114 over 20/20 stations, both belts Mirror symmetry belts and loops matched within 5e-4 m 0.000000 Loaf mirror every loaf vertex has an X-mirror partner within 2e-4 m 0.000093 Cinch depth mid-span minus waist in [0.006, 0.020] m 0.01053 Loaf X × Z (0.921, 0.393) m ± 0.02 (0.9208, 0.3931) Hay–twine gap BVH surface < 0.008 m 0.00009 Collider tris ≤ 400 140 Bake texels smallest UV cell ≥ 12 px at the baked resolution 22.43 px at 1024 px Export written, size > 0 203508 / 203508 / 203496 bytes
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity, plus the texel floor. The bake was 256 px, which left 5.6 texels per UV cell and smeared neighbouring cells' normals into streaks across the straw; at 1024 px with a 0.02 m cage (was 0.04, which reached the twine standing proud of the loaf) every cell gets 22. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), which is not a gated axis; every geometric figure above is identical on all three binaries.
The export is deleted once its size is measured. Blender sets TMPDIR from its own preference, which resolves to the working directory on a stock portable build, so tempfile.gettempdir() is the repo root under CI and every run used to leave a .glb behind.
-
Why these budgets and not an AABB
+
Why these budgets and not an AABB#
zmin alone cannot see a belt that stops at the bottom edge: the loaf still grounds the box. A single global hay-twine gap cannot see a wrap that hugs one side and gaps the other. A bounding box cannot see two belts 5.4 mm out of step, or a flank with no waist in it. Each of the five budgets in bold above exists because a defect got past everything else and had to be found by looking at a render.
The z-fight count is taken over cross-shell face pairs, not merely over faces that share no vertex: two quads two steps apart on one flat cap share no vertex and are coplanar by construction, and counting those reported 96 pairs, none of them a hazard.
-
Why the loaf is triangulated along its short diagonals
+
Why the loaf is triangulated along its short diagonals#
The twine used to be a 16 × 24 mm band, and it read as packing tape on a parcel. Thinned to an 8 × 10 mm cord, the two belts came out 0.45 mm out of step, right against the 0.5 mm mirror budget. The loaf's vertices were mirror-symmetric to 71 um. The belt profile is raycast from the loaf at each station, though, and the loaf's quads are not planar: split along a fixed diagonal, the surface at -x and at +x differed by up to 5.6 mm on the same vertices. The old wide band happened to hide that.
The loaf is now triangulated with quad_method="SHORT_EDGE". On mirror-symmetric vertices the shorter diagonal is mirror-symmetric too, and a tie only happens where a quad is planar, where either split is the same surface. The belt mirror error went back to exactly 0 and the seats to identical values at both stations. This is the surface an engine renders, so it is fixed in the mesh, not only in the sampling. Averaging the two stations' profiles was tried first and rejected: it put one belt 0.52 mm into the loaf, below the seat band, because neither station's real surface is the mean.
The loaf's own symmetry is now asserted directly (Loaf mirror , exit 19). Every loaf vertex needs a partner at (-x, y, z) within LOAF_MIRROR_EPS. The belt budget only ever caught an odd shaping term second-hand, through the belts' extents. --odd-loaf adds a 2 mm odd sin displacement to the finished loaf and fails it. It is applied after the bevel because, put into the shaping function, even 0.3 mm of odd term flipped which edges cleared the bevel's 50-degree selection and grew the envelope 16 mm, so the bounding box fired instead.
-
Falsifiers
+
Falsifiers#
Each breaks one stage so a named budget fails and the piece exits that code. All eight proven on 4.5.11, 5.1.2 and 5.2.1.
Flag Target budget Exit --skip-decimateLOD1 ratio band 9 --stray-verthygiene: loose vertices 15 --lift-zgrounded AABB zmin 16 --float-beltsper-belt wrap zmin 16 --slack-beltbanded belt seat depth 18 --skew-beltbelt mirror symmetry 19 --odd-loafloaf X-mirror symmetry 19 --low-bakebake texel density: 256 px bake (under 12 px per cell) 20
--stray-vert parks its loose vertex inside the bale: above it, the bounding-box budget (exit 8) fired first and the run proved nothing about hygiene. --skew-belt shifts one belt along X rather than Z for the same reason — a Z skew landed a belt face coplanar with a loaf face and tripped the z-fight budget (exit 15) instead of its target. --float-belts moves twine only, so the loaf still grounds the AABB and the per-belt gate has to be the thing that fires. With the ridge at 6 mm the knot loops set the envelope top, so the two twine falsifiers are sized to their own budgets and kept inside BBOX_TOL: --float-belts lifts 8 mm (was 20) against a 6 mm wrap gate, and --slack-belt backs off 6 mm (was 10) against a 2 mm seat floor. At the old sizes both exited 8.
-
Run
+
Run#
blender --background --python hay_bale.py --
blender --background --python hay_bale.py -- --skip-decimate
blender --background --python hay_bale.py -- --stray-vert
@@ -314,24 +373,1665 @@ Run
blender --background --python hay_bale.py -- --low-bake
blender --background --python hay_bale.py -- --output bale.png
Smoke does not pass --output or any falsifier flag.
-
Shading
+
Shading#
Decided per part, not globally. The loaf is flat -shaded: straw is a chunky matte mass and the facets read as compressed flakes. Smoothing the whole bale erased every ridge and nap in it and left a featureless pillow that read as a bar of soap. Twine is cord, so the belts and hitch loops stay smooth.
The low mesh carries the form — bulge, waist, ridges, end strata — and the high mesh carries the straw : LOAF_CUTS_HIGH subdivision plus per-band flake offsets, moved onto the low mesh by the bake stage. A low mesh dense enough to model flakes directly would cost thousands of triangles for detail this piece already has a bake for.
-
Surface
+
Surface#
The bale used to read as a wrapped parcel: flat mustard hay, and a wide dark band for twine. The hay is now straw_material:
a dense fibre field stretched along the bale (noise with X compressed), and a weaker crossing field for the strands that lie the other way; colour pulled between pale straw and dark tan by the fibres, with a faint green cast from low-frequency noise; roughness and a bump from the same fibres, so every strand catches light.
The baked normal is chained under that bump (wire_normal). The twine is a 10 × 14 mm orange polypropylene cord standing 6 mm proud. As an 8 × 10 mm dark brown cord it stood 4 mm proud of the waisted loaf and read as a scored cut in every view. The longitudinal ridge dropped from 14 mm to 6 mm, because at 14 mm it crumpled the top edge in profile and the bale read as a stuffed sack. Each knot is a tight loop, KNOT_MAJOR 13 mm, standing in the belt's plane with its tube sized from TWINE_T. A 60 mm ring lying flat on the top read as a printed symbol. The earlier note here, that the surface belonged to the material and bake and not to more geometry, was right. The cord was the other half of the parcel read.
-
Exit codes
+
Exit codes#
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or hay/twine faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert) 16 Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts) 17 Shell count, or hay–twine BVH gap above 8 mm 18 Banded belt seat depth (--slack-belt) 19 Loaf X-mirror symmetry (--odd-loaf), belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size 20 Bake texel density below the floor (--low-bake)
-
+
Source
- """Game-ready bound hay bale — a showcase piece, not an example.
+
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
+693
+694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
+740
+741
+742
+743
+744
+745
+746
+747
+748
+749
+750
+751
+752
+753
+754
+755
+756
+757
+758
+759
+760
+761
+762
+763
+764
+765
+766
+767
+768
+769
+770
+771
+772
+773
+774
+775
+776
+777
+778
+779
+780
+781
+782
+783
+784
+785
+786
+787
+788
+789
+790
+791
+792
+793
+794
+795
+796
+797
+798
+799
+800
+801
+802
+803
+804
+805
+806
+807
+808
+809
+810
+811
+812
+813
+814
+815
+816
+817
+818
+819
+820
+821
+822
+823
+824
+825
+826
+827
+828
+829
+830
+831
+832
+833
+834
+835
+836
+837
+838
+839
+840
+841
+842
+843
+844
+845
+846
+847
+848
+849
+850
+851
+852
+853
+854
+855
+856
+857
+858
+859
+860
+861
+862
+863
+864
+865
+866
+867
+868
+869
+870
+871
+872
+873
+874
+875
+876
+877
+878
+879
+880
+881
+882
+883
+884
+885
+886
+887
+888
+889
+890
+891
+892
+893
+894
+895
+896
+897
+898
+899
+900
+901
+902
+903
+904
+905
+906
+907
+908
+909
+910
+911
+912
+913
+914
+915
+916
+917
+918
+919
+920
+921
+922
+923
+924
+925
+926
+927
+928
+929
+930
+931
+932
+933
+934
+935
+936
+937
+938
+939
+940
+941
+942
+943
+944
+945
+946
+947
+948
+949
+950
+951
+952
+953
+954
+955
+956
+957
+958
+959
+960
+961
+962
+963
+964
+965
+966
+967
+968
+969
+970
+971
+972
+973
+974
+975
+976
+977
+978
+979
+980
+981
+982
+983
+984
+985
+986
+987
+988
+989
+990
+991
+992
+993
+994
+995
+996
+997
+998
+999
+1000
+1001
+1002
+1003
+1004
+1005
+1006
+1007
+1008
+1009
+1010
+1011
+1012
+1013
+1014
+1015
+1016
+1017
+1018
+1019
+1020
+1021
+1022
+1023
+1024
+1025
+1026
+1027
+1028
+1029
+1030
+1031
+1032
+1033
+1034
+1035
+1036
+1037
+1038
+1039
+1040
+1041
+1042
+1043
+1044
+1045
+1046
+1047
+1048
+1049
+1050
+1051
+1052
+1053
+1054
+1055
+1056
+1057
+1058
+1059
+1060
+1061
+1062
+1063
+1064
+1065
+1066
+1067
+1068
+1069
+1070
+1071
+1072
+1073
+1074
+1075
+1076
+1077
+1078
+1079
+1080
+1081
+1082
+1083
+1084
+1085
+1086
+1087
+1088
+1089
+1090
+1091
+1092
+1093
+1094
+1095
+1096
+1097
+1098
+1099
+1100
+1101
+1102
+1103
+1104
+1105
+1106
+1107
+1108
+1109
+1110
+1111
+1112
+1113
+1114
+1115
+1116
+1117
+1118
+1119
+1120
+1121
+1122
+1123
+1124
+1125
+1126
+1127
+1128
+1129
+1130
+1131
+1132
+1133
+1134
+1135
+1136
+1137
+1138
+1139
+1140
+1141
+1142
+1143
+1144
+1145
+1146
+1147
+1148
+1149
+1150
+1151
+1152
+1153
+1154
+1155
+1156
+1157
+1158
+1159
+1160
+1161
+1162
+1163
+1164
+1165
+1166
+1167
+1168
+1169
+1170
+1171
+1172
+1173
+1174
+1175
+1176
+1177
+1178
+1179
+1180
+1181
+1182
+1183
+1184
+1185
+1186
+1187
+1188
+1189
+1190
+1191
+1192
+1193
+1194
+1195
+1196
+1197
+1198
+1199
+1200
+1201
+1202
+1203
+1204
+1205
+1206
+1207
+1208
+1209
+1210
+1211
+1212
+1213
+1214
+1215
+1216
+1217
+1218
+1219
+1220
+1221
+1222
+1223
+1224
+1225
+1226
+1227
+1228
+1229
+1230
+1231
+1232
+1233
+1234
+1235
+1236
+1237
+1238
+1239
+1240
+1241
+1242
+1243
+1244
+1245
+1246
+1247
+1248
+1249
+1250
+1251
+1252
+1253
+1254
+1255
+1256
+1257
+1258
+1259
+1260
+1261
+1262
+1263
+1264
+1265
+1266
+1267
+1268
+1269
+1270
+1271
+1272
+1273
+1274
+1275
+1276
+1277
+1278
+1279
+1280
+1281
+1282
+1283
+1284
+1285
+1286
+1287
+1288
+1289
+1290
+1291
+1292
+1293
+1294
+1295
+1296
+1297
+1298
+1299
+1300
+1301
+1302
+1303
+1304
+1305
+1306
+1307
+1308
+1309
+1310
+1311
+1312
+1313
+1314
+1315
+1316
+1317
+1318
+1319
+1320
+1321
+1322
+1323
+1324
+1325
+1326
+1327
+1328
+1329
+1330
+1331
+1332
+1333
+1334
+1335
+1336
+1337
+1338
+1339
+1340
+1341
+1342
+1343
+1344
+1345
+1346
+1347
+1348
+1349
+1350
+1351
+1352
+1353
+1354
+1355
+1356
+1357
+1358
+1359
+1360
+1361
+1362
+1363
+1364
+1365
+1366
+1367
+1368
+1369
+1370
+1371
+1372
+1373
+1374
+1375
+1376
+1377
+1378
+1379
+1380
+1381
+1382
+1383
+1384
+1385
+1386
+1387
+1388
+1389
+1390
+1391
+1392
+1393
+1394
+1395
+1396
+1397
+1398
+1399
+1400
+1401
+1402
+1403
+1404
+1405
+1406
+1407
+1408
+1409
+1410
+1411
+1412
+1413
+1414
+1415
+1416
+1417
+1418
+1419
+1420
+1421
+1422
+1423
+1424
+1425
+1426
+1427
+1428
+1429
+1430
+1431
+1432
+1433
+1434
+1435
+1436
+1437
+1438
+1439
+1440
+1441
+1442
+1443
+1444
+1445
+1446
+1447
+1448
+1449
+1450
+1451
+1452
+1453
+1454
+1455
+1456
+1457
+1458
+1459
+1460
+1461
+1462
+1463
+1464
+1465
+1466
+1467
+1468
+1469
+1470
+1471
+1472
+1473
+1474
+1475
+1476
+1477
+1478
+1479
+1480
+1481
+1482
+1483
+1484
+1485
+1486
+1487
+1488
+1489
+1490
+1491
+1492
+1493
+1494
+1495
+1496
+1497
+1498
+1499
+1500
+1501
+1502
+1503
+1504
+1505
+1506
+1507
+1508
+1509
+1510
+1511
+1512
+1513
+1514
+1515
+1516
+1517
+1518
+1519
+1520
+1521
+1522
+1523
+1524
+1525
+1526
+1527
+1528
+1529
+1530
+1531
+1532
+1533
+1534
+1535
+1536
+1537
+1538
+1539
+1540
+1541
+1542
+1543
+1544
+1545
+1546
+1547
+1548
+1549
+1550
+1551
+1552
+1553
+1554
+1555
+1556
+1557
+1558
+1559
+1560
+1561
+1562
+1563
+1564
+1565
+1566
+1567
+1568
+1569
+1570
+1571
+1572
+1573
+1574
+1575
+1576
+1577
+1578
+1579
+1580
+1581
+1582
+1583
+1584
+1585
+1586
+1587
+1588
+1589
+1590
+1591
+1592
+1593
+1594
+1595
+1596
+1597
+1598
+1599
+1600
+1601
+1602
+1603
+1604
+1605
+1606
+1607
+1608
+1609
+1610
+1611
+1612
+1613
+1614
+1615
+1616
+1617
+1618
+1619
+1620
+1621
+1622
+1623
+1624
+1625
+1626
+1627
+1628
+1629
+1630
+1631
+1632
+1633
+1634
+1635
+1636
+1637
+1638
+1639
+1640
+1641 """Game-ready bound hay bale — a showcase piece, not an example.
Asserts budget conformance of a procedural straw bale after composing
shipped pipeline pieces: bmesh construction, UVs, two materials,
@@ -1973,7 +3673,41 @@ Source
print(f" FATAL: {e}" , file=sys.stderr)
sys.exit(1 )
+
+
+
Close
@@ -1993,20 +3727,98 @@ Source
var box = document.getElementById('lightbox');
if (hero && box) {
// .showModal() traps focus, closes on Escape, and returns
- // focus to the hero button on close. Any click inside closes it.
- hero.addEventListener('click', function () { box.showModal(); });
- box.addEventListener('click', function () { box.close(); });
+ // focus to the hero button on close. The dialog fills the viewport,
+ // so a click whose target is the dialog itself is a backdrop click.
+ var full = box.querySelector('img');
+ hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); });
+ box.addEventListener('click', function (e) { if (e.target === box) box.close(); });
+ box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); });
+ full.addEventListener('click', function () { box.classList.toggle('native'); });
+ }
+
+ // Clipboard API first; execCommand for non-secure origins (file://,
+ // plain-http previews) where navigator.clipboard is undefined.
+ function copyText(text) {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text);
+ }
+ return new Promise(function (resolve, reject) {
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.setAttribute('readonly', '');
+ ta.style.position = 'fixed'; ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ var ok = false;
+ try { ok = document.execCommand('copy'); } catch (e) {}
+ document.body.removeChild(ta);
+ if (ok) { resolve(); } else { reject(); }
+ });
+ }
+ function wireCopy(btn, getText) {
+ btn.addEventListener('click', function () {
+ copyText(getText()).then(function () {
+ btn.textContent = 'Copied'; btn.classList.add('done');
+ }, function () {
+ btn.textContent = 'Copy failed'; btn.classList.add('fail');
+ }).then(function () {
+ setTimeout(function () {
+ btn.textContent = 'Copy'; btn.classList.remove('done', 'fail');
+ }, 1600);
+ });
+ });
}
var copy = document.getElementById('copyRun');
if (copy) {
- copy.addEventListener('click', function () {
- var cmd = document.getElementById('runCmd').textContent;
- navigator.clipboard.writeText(cmd).then(function () {
- copy.textContent = 'Copied!';
- setTimeout(function () { copy.textContent = 'Copy'; }, 1500);
+ wireCopy(copy, function () { return document.getElementById('runCmd').textContent; });
+ }
+ Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) {
+ var wrap = block;
+ if (block.tagName === 'PRE') {
+ wrap = document.createElement('div');
+ block.parentNode.insertBefore(wrap, block);
+ wrap.appendChild(block);
+ }
+ wrap.classList.add('codewrap');
+ var btn = document.createElement('button');
+ btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy';
+ var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines');
+ btn.setAttribute('aria-label', 'Copy code');
+ wireCopy(btn, function () { return src.textContent; });
+ wrap.appendChild(btn);
+ });
+
+ // Long listings start collapsed; with JS off they render in full.
+ var code = document.querySelector('.src .code');
+ if (code) {
+ var lines = parseInt(code.getAttribute('data-lines'), 10) || 0;
+ if (lines > 120) {
+ code.classList.add('collapsed');
+ var more = document.createElement('button');
+ more.type = 'button'; more.className = 'src-expand';
+ more.setAttribute('aria-expanded', 'false');
+ more.textContent = 'Show all ' + lines + ' lines';
+ code.parentNode.insertBefore(more, code.nextSibling);
+ more.addEventListener('click', function () {
+ var open = code.classList.toggle('collapsed') === false;
+ more.setAttribute('aria-expanded', open ? 'true' : 'false');
+ more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines';
+ if (!open) code.scrollIntoView({ block: 'start' });
});
- });
+ }
}
+
+ // Left/right arrows page between entries of the same kind.
+ document.addEventListener('keydown', function (e) {
+ if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
+ if (box && box.open) return;
+ var ae = document.activeElement;
+ if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return;
+ var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : '';
+ if (!rel) return;
+ var a = document.querySelector('.pager a[rel="' + rel + '"]');
+ if (a) { location.href = a.href; }
+ });
})();
diff --git a/docs/gallery/hitching-post/index.html b/docs/gallery/hitching-post/index.html
index 6a144f11..df09b089 100644
--- a/docs/gallery/hitching-post/index.html
+++ b/docs/gallery/hitching-post/index.html
@@ -161,7 +161,7 @@
.card:hover { border-color: var(--select); outline-color: var(--select); }
.card.hidden { display: none; }
.card-media { display: block; background: var(--bg2); line-height: 0; }
- .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; }
.card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; }
.card-body h2 a { color: var(--text); }
@@ -176,7 +176,7 @@
/* ---- detail page ---- */
.detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; }
- .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; }
@@ -192,7 +192,7 @@
color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer;
font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
.copy-btn:hover { color: var(--select); border-color: var(--select); }
- .detail-section { margin-top: 2.25rem; }
+ .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; }
.detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
@@ -222,11 +222,68 @@
flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; }
.src-meta code { font-family: var(--font-mono); font-size: 0.82rem; }
+ /* ---- detail page: pager, tags, related, anchors, code blocks ---- */
+ .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem;
+ margin: 0 0 1.25rem; font-size: 0.85rem; }
+ .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
+ .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .pager a:hover { color: var(--select); text-decoration: none; }
+ .pager [rel="next"] { text-align: right; }
+ .pager-pos { text-align: center; }
+ @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } }
+ .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border);
+ border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; }
+ .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; }
+ .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px;
+ padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; }
+ .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; }
+ .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
+ @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } }
+ .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; }
+ .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; }
+ .card.mini h3 a { color: var(--text); }
+ .card.mini h3 a:hover { color: var(--select); text-decoration: none; }
+ .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45;
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+ .md h1, .md h2, .md h3 { scroll-margin-top: 60px; }
+ .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0;
+ transition: opacity 0.15s; }
+ .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; }
+ .anchor:hover { color: var(--select); text-decoration: none; }
+ .codewrap { position: relative; }
+ .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem;
+ font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; }
+ .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; }
+ @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } }
+ .copy-btn.done { color: var(--ok); border-color: var(--ok); }
+ .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); }
+ .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
+ overflow: hidden; }
+ .src .code pre { background: none; border: 0; border-radius: 0; }
+ .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right;
+ user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border);
+ background: var(--bg2); overflow: hidden; }
+ .src .code.collapsed { max-height: 42rem; position: relative; }
+ .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem;
+ background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; }
+ .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2);
+ border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius);
+ padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans);
+ transition: color 0.15s, border-color 0.15s; }
+ .src-expand:hover { color: var(--select); border-color: var(--select); }
+
.lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none;
max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; }
- .lightbox[open] { display: flex; align-items: center; justify-content: center; }
+ /* margin:auto (not align/justify-content) centers without clipping the
+ top-left of a native-size image that overflows a small viewport. */
+ .lightbox[open] { display: flex; overflow: auto; }
.lightbox::backdrop { background: transparent; }
- .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); }
+ .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius);
+ cursor: zoom-in; }
+ .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; }
.lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2);
color: var(--text); border: 1px solid var(--border); border-radius: var(--radius);
font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; }
@@ -268,11 +325,13 @@ hitching-post
A procedural hitching post (eaved cap, one rail, cup shoe, rings hung through eyes) through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
+
-
+
Rendered headless by the showcase piece itself — click to zoom.
witnesses Recomputed: 1680 tris, two materials with 114 wood and 742 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.500×0.149×1.246 m, zmin 0, hygiene and coplanar pairs 0, post seated at z 0.010, ring centerline error 0, bands 6.5 mm proud of the post, LOD ratios in band, convex collider 80 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert and --twin-sole exit 15; --lift-z exits 16; --clip-ring and --sunk-bands exit 18; --short-post exits 19.
+ tags mesh export showcase
blender --background --python showcase/hitching-post/hitching_post.py --
Copy
@@ -282,18 +341,18 @@
hitching-post
It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.
Intended size: post 0.125 m square and 1.16 m of timber, cap 0.096 m, cross-rail 0.50 m. Outer AABB 0.500 × 0.149 × 1.246 m.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported). Coplanar-pair counting matches showcase/signpost.
-
Budgets
+
Budgets#
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
Axis Declared Measured (4.5.11 / 5.1.2 / 5.2.1) Base triangles 1400–2400 1680 / 1680 / 1680 LOD1 ratio 0.32–0.62 of base 0.5000 / 0.5000 / 0.5000 LOD2 ratio 0.10–0.35 of base 0.2190 / 0.2190 / 0.2190 Materials exactly 2 distinct, ≥70 wood, ≥560 metal 2 slots, 114 wood, 742 metal UVs in 0..1, AABB overlap ≤ 1e-5 in range, overlap 0 Outer AABB (0.500, 0.149, 1.246) m ± 0.01 (0.5000, 0.1490, 1.2460) Grounded zmin within 1e-4 of 0 0 / 0 / 0 Hygiene loose/nonman/zero-area/doubles/n-gons/coplanar pairs = 0 all 0 Post seat world zmin in (0.004, 0.014), XY centroid within 0.010 of origin, arm axis cos ≥ cos(0.5°) 0.01000, 0, 1.000 Hung ring centerline error ≤ 0.008 m, ring–wood overlap 0, eye–wood overlap 0, shank–ring overlap 0, shank bites the eye 0, 0, 0, 0, 40 Shoe each band overlaps the post, shoe overlaps the sole band gap 0, sole–shoe 8 Band seat each band above the shoe stands ≥ 0.004 m proud of the post faces 0.00650 Wood–metal gap BVH surface < 0.008 m 0.00075 Collider tris ≤ 280 80 Export written, size > 0 127424 / 127424 / 127412 bytes
Base triangles rose from 1598 to 1680 . The old rings were faceted tori clipped into the arm; the new eyes, shanks, and 24-segment hung rings replace that mesh. Cylinder caps are triangulated so the n-gon budget stays at 0.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis. Euler is 12 (six balls and seven tori) and is not gated to 2.
-
Bands, wood and stage
+
Bands, wood and stage#
The two iron bands were sized from half - grip: an inner half-width of 53 mm and an outer one of 61 mm, against a 62.5 mm post face. They sat inside the post, and only the chamfered corners broke the surface, showing as small black slits up the post. They now wrap it: the inner face bites 1.5 mm into the post (BAND_BITE), so each band stands 6.5 mm proud. Band seat asserts it; --sunk-bands restores the old bands and exits 18 (proud −0.0015 m).
The post, rail and cap each carry their own tone and grain, and the iron is rusted. The stage grid grew from 14 to 60 m, because the wall's left edge showed as a bright band in the corner of the hero. The temp .glb is removed after its size is measured.
Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS, 5.1.2, and 5.2.1 LTS (the binaries' own --version):
Falsifier Budget violated Exit Measured failure --skip-decimateLOD1 ratio band 9 ratio 1.0000 --stray-vertMesh hygiene, loose verts 15 loose_v=1--twin-soleCoplanar face pairs 15 zfight=6--lift-zGrounded zmin 16 zmin 0.050000 --clip-ringHung-ring centerline 18 ring_err 0.024, ring–wood overlap 32 --short-postSeated post 19 post zmin 0.030 --sunk-bandsBand seat 18 band proud −0.0015
Default exit is 0 on all three. --clip-ring also overlaps the rail; the centerline gate is the one that fires.
-
Run
+
Run#
blender --background --python hitching_post.py --
blender --background --python hitching_post.py -- --skip-decimate
blender --background --python hitching_post.py -- --stray-vert
@@ -304,17 +363,1573 @@ Run
blender --background --python hitching_post.py -- --sunk-bands
blender --background --python hitching_post.py -- --output preview.webp --engine cycles
Smoke does not pass --output or a falsifier.
-
Exit codes
+
Exit codes#
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene, including coplanar pairs (--stray-vert, --twin-sole) 16 Grounded zmin (--lift-z) 17 Wood–metal BVH gap above 8 mm 18 Hung ring, shoe or band seat (--clip-ring, --sunk-bands) 19 Post plumb, origin, and cup seat (--short-post)
-
+
Source
- """Game-ready hitching post — a showcase piece, not an example.
+
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
+693
+694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
+740
+741
+742
+743
+744
+745
+746
+747
+748
+749
+750
+751
+752
+753
+754
+755
+756
+757
+758
+759
+760
+761
+762
+763
+764
+765
+766
+767
+768
+769
+770
+771
+772
+773
+774
+775
+776
+777
+778
+779
+780
+781
+782
+783
+784
+785
+786
+787
+788
+789
+790
+791
+792
+793
+794
+795
+796
+797
+798
+799
+800
+801
+802
+803
+804
+805
+806
+807
+808
+809
+810
+811
+812
+813
+814
+815
+816
+817
+818
+819
+820
+821
+822
+823
+824
+825
+826
+827
+828
+829
+830
+831
+832
+833
+834
+835
+836
+837
+838
+839
+840
+841
+842
+843
+844
+845
+846
+847
+848
+849
+850
+851
+852
+853
+854
+855
+856
+857
+858
+859
+860
+861
+862
+863
+864
+865
+866
+867
+868
+869
+870
+871
+872
+873
+874
+875
+876
+877
+878
+879
+880
+881
+882
+883
+884
+885
+886
+887
+888
+889
+890
+891
+892
+893
+894
+895
+896
+897
+898
+899
+900
+901
+902
+903
+904
+905
+906
+907
+908
+909
+910
+911
+912
+913
+914
+915
+916
+917
+918
+919
+920
+921
+922
+923
+924
+925
+926
+927
+928
+929
+930
+931
+932
+933
+934
+935
+936
+937
+938
+939
+940
+941
+942
+943
+944
+945
+946
+947
+948
+949
+950
+951
+952
+953
+954
+955
+956
+957
+958
+959
+960
+961
+962
+963
+964
+965
+966
+967
+968
+969
+970
+971
+972
+973
+974
+975
+976
+977
+978
+979
+980
+981
+982
+983
+984
+985
+986
+987
+988
+989
+990
+991
+992
+993
+994
+995
+996
+997
+998
+999
+1000
+1001
+1002
+1003
+1004
+1005
+1006
+1007
+1008
+1009
+1010
+1011
+1012
+1013
+1014
+1015
+1016
+1017
+1018
+1019
+1020
+1021
+1022
+1023
+1024
+1025
+1026
+1027
+1028
+1029
+1030
+1031
+1032
+1033
+1034
+1035
+1036
+1037
+1038
+1039
+1040
+1041
+1042
+1043
+1044
+1045
+1046
+1047
+1048
+1049
+1050
+1051
+1052
+1053
+1054
+1055
+1056
+1057
+1058
+1059
+1060
+1061
+1062
+1063
+1064
+1065
+1066
+1067
+1068
+1069
+1070
+1071
+1072
+1073
+1074
+1075
+1076
+1077
+1078
+1079
+1080
+1081
+1082
+1083
+1084
+1085
+1086
+1087
+1088
+1089
+1090
+1091
+1092
+1093
+1094
+1095
+1096
+1097
+1098
+1099
+1100
+1101
+1102
+1103
+1104
+1105
+1106
+1107
+1108
+1109
+1110
+1111
+1112
+1113
+1114
+1115
+1116
+1117
+1118
+1119
+1120
+1121
+1122
+1123
+1124
+1125
+1126
+1127
+1128
+1129
+1130
+1131
+1132
+1133
+1134
+1135
+1136
+1137
+1138
+1139
+1140
+1141
+1142
+1143
+1144
+1145
+1146
+1147
+1148
+1149
+1150
+1151
+1152
+1153
+1154
+1155
+1156
+1157
+1158
+1159
+1160
+1161
+1162
+1163
+1164
+1165
+1166
+1167
+1168
+1169
+1170
+1171
+1172
+1173
+1174
+1175
+1176
+1177
+1178
+1179
+1180
+1181
+1182
+1183
+1184
+1185
+1186
+1187
+1188
+1189
+1190
+1191
+1192
+1193
+1194
+1195
+1196
+1197
+1198
+1199
+1200
+1201
+1202
+1203
+1204
+1205
+1206
+1207
+1208
+1209
+1210
+1211
+1212
+1213
+1214
+1215
+1216
+1217
+1218
+1219
+1220
+1221
+1222
+1223
+1224
+1225
+1226
+1227
+1228
+1229
+1230
+1231
+1232
+1233
+1234
+1235
+1236
+1237
+1238
+1239
+1240
+1241
+1242
+1243
+1244
+1245
+1246
+1247
+1248
+1249
+1250
+1251
+1252
+1253
+1254
+1255
+1256
+1257
+1258
+1259
+1260
+1261
+1262
+1263
+1264
+1265
+1266
+1267
+1268
+1269
+1270
+1271
+1272
+1273
+1274
+1275
+1276
+1277
+1278
+1279
+1280
+1281
+1282
+1283
+1284
+1285
+1286
+1287
+1288
+1289
+1290
+1291
+1292
+1293
+1294
+1295
+1296
+1297
+1298
+1299
+1300
+1301
+1302
+1303
+1304
+1305
+1306
+1307
+1308
+1309
+1310
+1311
+1312
+1313
+1314
+1315
+1316
+1317
+1318
+1319
+1320
+1321
+1322
+1323
+1324
+1325
+1326
+1327
+1328
+1329
+1330
+1331
+1332
+1333
+1334
+1335
+1336
+1337
+1338
+1339
+1340
+1341
+1342
+1343
+1344
+1345
+1346
+1347
+1348
+1349
+1350
+1351
+1352
+1353
+1354
+1355
+1356
+1357
+1358
+1359
+1360
+1361
+1362
+1363
+1364
+1365
+1366
+1367
+1368
+1369
+1370
+1371
+1372
+1373
+1374
+1375
+1376
+1377
+1378
+1379
+1380
+1381
+1382
+1383
+1384
+1385
+1386
+1387
+1388
+1389
+1390
+1391
+1392
+1393
+1394
+1395
+1396
+1397
+1398
+1399
+1400
+1401
+1402
+1403
+1404
+1405
+1406
+1407
+1408
+1409
+1410
+1411
+1412
+1413
+1414
+1415
+1416
+1417
+1418
+1419
+1420
+1421
+1422
+1423
+1424
+1425
+1426
+1427
+1428
+1429
+1430
+1431
+1432
+1433
+1434
+1435
+1436
+1437
+1438
+1439
+1440
+1441
+1442
+1443
+1444
+1445
+1446
+1447
+1448
+1449
+1450
+1451
+1452
+1453
+1454
+1455
+1456
+1457
+1458
+1459
+1460
+1461
+1462
+1463
+1464
+1465
+1466
+1467
+1468
+1469
+1470
+1471
+1472
+1473
+1474
+1475
+1476
+1477
+1478
+1479
+1480
+1481
+1482
+1483
+1484
+1485
+1486
+1487
+1488
+1489
+1490
+1491
+1492
+1493
+1494
+1495
+1496
+1497
+1498
+1499
+1500
+1501
+1502
+1503
+1504
+1505
+1506
+1507
+1508
+1509
+1510
+1511
+1512
+1513
+1514
+1515
+1516
+1517
+1518
+1519
+1520
+1521
+1522
+1523
+1524
+1525
+1526
+1527
+1528
+1529
+1530
+1531
+1532
+1533
+1534
+1535
+1536
+1537
+1538
+1539
+1540
+1541
+1542
+1543
+1544
+1545
+1546
+1547
+1548
+1549
+1550
+1551
+1552
+1553
+1554
+1555
+1556 """Game-ready hitching post — a showcase piece, not an example.
Asserts budget conformance of a procedural timber hitching post (square
post, pyramidal cap, cross-arm, iron shoe, bands, and hitching rings)
@@ -1871,7 +3486,41 @@ Source
print(f" FATAL: {e}" , file=sys.stderr)
sys.exit(1 )
+
+
+
Close
@@ -1891,20 +3540,98 @@ Source
var box = document.getElementById('lightbox');
if (hero && box) {
// .showModal() traps focus, closes on Escape, and returns
- // focus to the hero button on close. Any click inside closes it.
- hero.addEventListener('click', function () { box.showModal(); });
- box.addEventListener('click', function () { box.close(); });
+ // focus to the hero button on close. The dialog fills the viewport,
+ // so a click whose target is the dialog itself is a backdrop click.
+ var full = box.querySelector('img');
+ hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); });
+ box.addEventListener('click', function (e) { if (e.target === box) box.close(); });
+ box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); });
+ full.addEventListener('click', function () { box.classList.toggle('native'); });
+ }
+
+ // Clipboard API first; execCommand for non-secure origins (file://,
+ // plain-http previews) where navigator.clipboard is undefined.
+ function copyText(text) {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text);
+ }
+ return new Promise(function (resolve, reject) {
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.setAttribute('readonly', '');
+ ta.style.position = 'fixed'; ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ var ok = false;
+ try { ok = document.execCommand('copy'); } catch (e) {}
+ document.body.removeChild(ta);
+ if (ok) { resolve(); } else { reject(); }
+ });
+ }
+ function wireCopy(btn, getText) {
+ btn.addEventListener('click', function () {
+ copyText(getText()).then(function () {
+ btn.textContent = 'Copied'; btn.classList.add('done');
+ }, function () {
+ btn.textContent = 'Copy failed'; btn.classList.add('fail');
+ }).then(function () {
+ setTimeout(function () {
+ btn.textContent = 'Copy'; btn.classList.remove('done', 'fail');
+ }, 1600);
+ });
+ });
}
var copy = document.getElementById('copyRun');
if (copy) {
- copy.addEventListener('click', function () {
- var cmd = document.getElementById('runCmd').textContent;
- navigator.clipboard.writeText(cmd).then(function () {
- copy.textContent = 'Copied!';
- setTimeout(function () { copy.textContent = 'Copy'; }, 1500);
+ wireCopy(copy, function () { return document.getElementById('runCmd').textContent; });
+ }
+ Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) {
+ var wrap = block;
+ if (block.tagName === 'PRE') {
+ wrap = document.createElement('div');
+ block.parentNode.insertBefore(wrap, block);
+ wrap.appendChild(block);
+ }
+ wrap.classList.add('codewrap');
+ var btn = document.createElement('button');
+ btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy';
+ var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines');
+ btn.setAttribute('aria-label', 'Copy code');
+ wireCopy(btn, function () { return src.textContent; });
+ wrap.appendChild(btn);
+ });
+
+ // Long listings start collapsed; with JS off they render in full.
+ var code = document.querySelector('.src .code');
+ if (code) {
+ var lines = parseInt(code.getAttribute('data-lines'), 10) || 0;
+ if (lines > 120) {
+ code.classList.add('collapsed');
+ var more = document.createElement('button');
+ more.type = 'button'; more.className = 'src-expand';
+ more.setAttribute('aria-expanded', 'false');
+ more.textContent = 'Show all ' + lines + ' lines';
+ code.parentNode.insertBefore(more, code.nextSibling);
+ more.addEventListener('click', function () {
+ var open = code.classList.toggle('collapsed') === false;
+ more.setAttribute('aria-expanded', open ? 'true' : 'false');
+ more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines';
+ if (!open) code.scrollIntoView({ block: 'start' });
});
- });
+ }
}
+
+ // Left/right arrows page between entries of the same kind.
+ document.addEventListener('keydown', function (e) {
+ if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
+ if (box && box.open) return;
+ var ae = document.activeElement;
+ if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return;
+ var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : '';
+ if (!rel) return;
+ var a = document.querySelector('.pager a[rel="' + rel + '"]');
+ if (a) { location.href = a.href; }
+ });
})();
diff --git a/docs/gallery/image-pixels-testcard/index.html b/docs/gallery/image-pixels-testcard/index.html
index 7dde54f2..9005d7ff 100644
--- a/docs/gallery/image-pixels-testcard/index.html
+++ b/docs/gallery/image-pixels-testcard/index.html
@@ -161,7 +161,7 @@
.card:hover { border-color: var(--select); outline-color: var(--select); }
.card.hidden { display: none; }
.card-media { display: block; background: var(--bg2); line-height: 0; }
- .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; }
.card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; }
.card-body h2 a { color: var(--text); }
@@ -176,7 +176,7 @@
/* ---- detail page ---- */
.detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; }
- .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; }
@@ -192,7 +192,7 @@
color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer;
font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
.copy-btn:hover { color: var(--select); border-color: var(--select); }
- .detail-section { margin-top: 2.25rem; }
+ .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; }
.detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
@@ -222,11 +222,68 @@
flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; }
.src-meta code { font-family: var(--font-mono); font-size: 0.82rem; }
+ /* ---- detail page: pager, tags, related, anchors, code blocks ---- */
+ .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem;
+ margin: 0 0 1.25rem; font-size: 0.85rem; }
+ .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
+ .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .pager a:hover { color: var(--select); text-decoration: none; }
+ .pager [rel="next"] { text-align: right; }
+ .pager-pos { text-align: center; }
+ @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } }
+ .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border);
+ border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; }
+ .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; }
+ .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px;
+ padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; }
+ .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; }
+ .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
+ @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } }
+ .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; }
+ .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; }
+ .card.mini h3 a { color: var(--text); }
+ .card.mini h3 a:hover { color: var(--select); text-decoration: none; }
+ .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45;
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+ .md h1, .md h2, .md h3 { scroll-margin-top: 60px; }
+ .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0;
+ transition: opacity 0.15s; }
+ .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; }
+ .anchor:hover { color: var(--select); text-decoration: none; }
+ .codewrap { position: relative; }
+ .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem;
+ font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; }
+ .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; }
+ @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } }
+ .copy-btn.done { color: var(--ok); border-color: var(--ok); }
+ .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); }
+ .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
+ overflow: hidden; }
+ .src .code pre { background: none; border: 0; border-radius: 0; }
+ .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right;
+ user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border);
+ background: var(--bg2); overflow: hidden; }
+ .src .code.collapsed { max-height: 42rem; position: relative; }
+ .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem;
+ background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; }
+ .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2);
+ border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius);
+ padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans);
+ transition: color 0.15s, border-color 0.15s; }
+ .src-expand:hover { color: var(--select); border-color: var(--select); }
+
.lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none;
max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; }
- .lightbox[open] { display: flex; align-items: center; justify-content: center; }
+ /* margin:auto (not align/justify-content) centers without clipping the
+ top-left of a native-size image that overflows a small viewport. */
+ .lightbox[open] { display: flex; overflow: auto; }
.lightbox::backdrop { background: transparent; }
- .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); }
+ .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius);
+ cursor: zoom-in; }
+ .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; }
.lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2);
color: var(--text); border: 1px solid var(--border); border-radius: var(--radius);
font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; }
@@ -268,11 +325,13 @@ image-pixels-testcard
The Image pixel-buffer contract — a procedural broadcast test card written into bpy.data.images.new() with one pixels.foreach_set (589,824 floats), byte vs float_buffer storage, scale() reallocation, and the save() vs save_render() lifecycle.
+
-
+
Rendered headless by the example itself — click to zoom.
witnesses pixels is always flat RGBA (channels == 4 even with alpha=False), byte storage quantizes at exactly ≤ 0.5/255 and strictly > 0, stale-size bulk reads raise after scale() — and save() silently flips source to FILE and drops the buffer, so later pixels reads come from whatever sits on disk (proven with an imposter file).
+ tags images performance rendering
blender --background --python examples/image-pixels-testcard/image_pixels_testcard.py --
Copy
@@ -285,7 +344,7 @@
image-pixels-testcard
Buffer geometry — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a W*H*3 write raises and the check exits 3).Byte/float round-trip vs the closed-form card — any stride, orientation, or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196.Quantization floor — byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.Reallocation — scale() no longer reallocating (stale-size read succeeding).save() lifecycle — the source-flip/buffer-drop behavior changing (falsified: substituting save_render() for save() exits 7 because source stays GENERATED).Disk round-trip — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.
Version divergence: none — every contract above, including the save() trap, was probed and asserts identically on Blender 4.5.11 LTS and 5.1.2. The only gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).
Render hazard worth knowing: a bmesh-built plane has no UV map, and bmesh.ops.create_grid(..., calc_uvs=True) silently creates none unless a UV layer already exists — without one, an Image Texture samples texel (0,0) for every fragment and the screen renders as one flat color. The render path creates the layer explicitly.
-
Run
+
Run#
# Cheap correctness check (no render) — the CI check:
blender --background --python image_pixels_testcard.py --
@@ -295,19 +354,421 @@ Run
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python image_pixels_testcard.py -- --output card.png
blender --background --python image_pixels_testcard.py -- --output card.png --engine cycles
-
Exit codes
+
Exit codes#
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Pixel buffer is not always RGBA 4 Byte round-trip vs closed-form card (--wrong-origin lands here) 5 Float-buffer round-trip failed 6 scale() did not reallocate, or stale-size read succeeded7 save() source/buffer-drop contract drifted8 save_render() flipped source or disturbed the buffer9 Byte PNG save/reload error 10 --output produced no file
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --wrong-origin.
In the render, Closest interpolation keeps the pixel grid honest — the jagged circle edge is the 512 × 288 buffer itself, and the white marker in the PLUGE row sits at the bottom-left because that is where pixel (0, 0) lives. The monitor is staged as a designed object — beveled dark-polymer case, machined metal stand, teal power LED — on the dark studio stage from docs/VISUAL-STYLE.md (Standard view transform; warm key, cool fill and rim; a warm pool raking the back wall). The screen stays emissive and matte — specular off, emission strength 1.0 — so the card's values read exactly.
-
+
Source
- """A procedural broadcast test card written with one foreach_set — a runnable example.
+
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402 """A procedural broadcast test card written with one foreach_set — a runnable example.
Witnesses the `bpy.types.Image` pixel-buffer contract. `Image.pixels` is a flat,
row-major, bottom-left-origin float buffer that is ALWAYS RGBA: `channels == 4`
@@ -710,7 +1171,41 @@ Source
except Exception as e:
import traceback; traceback.print_exc(); print(f" FATAL: {e}" , file=sys.stderr); sys.exit(1 )
+
+
+
Close
@@ -730,20 +1225,98 @@ Source
var box = document.getElementById('lightbox');
if (hero && box) {
// .showModal() traps focus, closes on Escape, and returns
- // focus to the hero button on close. Any click inside closes it.
- hero.addEventListener('click', function () { box.showModal(); });
- box.addEventListener('click', function () { box.close(); });
+ // focus to the hero button on close. The dialog fills the viewport,
+ // so a click whose target is the dialog itself is a backdrop click.
+ var full = box.querySelector('img');
+ hero.addEventListener('click', function () { box.classList.remove('native'); box.showModal(); });
+ box.addEventListener('click', function (e) { if (e.target === box) box.close(); });
+ box.querySelector('.lightbox-close').addEventListener('click', function () { box.close(); });
+ full.addEventListener('click', function () { box.classList.toggle('native'); });
+ }
+
+ // Clipboard API first; execCommand for non-secure origins (file://,
+ // plain-http previews) where navigator.clipboard is undefined.
+ function copyText(text) {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text);
+ }
+ return new Promise(function (resolve, reject) {
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.setAttribute('readonly', '');
+ ta.style.position = 'fixed'; ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ var ok = false;
+ try { ok = document.execCommand('copy'); } catch (e) {}
+ document.body.removeChild(ta);
+ if (ok) { resolve(); } else { reject(); }
+ });
+ }
+ function wireCopy(btn, getText) {
+ btn.addEventListener('click', function () {
+ copyText(getText()).then(function () {
+ btn.textContent = 'Copied'; btn.classList.add('done');
+ }, function () {
+ btn.textContent = 'Copy failed'; btn.classList.add('fail');
+ }).then(function () {
+ setTimeout(function () {
+ btn.textContent = 'Copy'; btn.classList.remove('done', 'fail');
+ }, 1600);
+ });
+ });
}
var copy = document.getElementById('copyRun');
if (copy) {
- copy.addEventListener('click', function () {
- var cmd = document.getElementById('runCmd').textContent;
- navigator.clipboard.writeText(cmd).then(function () {
- copy.textContent = 'Copied!';
- setTimeout(function () { copy.textContent = 'Copy'; }, 1500);
+ wireCopy(copy, function () { return document.getElementById('runCmd').textContent; });
+ }
+ Array.prototype.forEach.call(document.querySelectorAll('.md pre, .src .code'), function (block) {
+ var wrap = block;
+ if (block.tagName === 'PRE') {
+ wrap = document.createElement('div');
+ block.parentNode.insertBefore(wrap, block);
+ wrap.appendChild(block);
+ }
+ wrap.classList.add('codewrap');
+ var btn = document.createElement('button');
+ btn.type = 'button'; btn.className = 'copy-btn'; btn.textContent = 'Copy';
+ var src = block.tagName === 'PRE' ? block : block.querySelector('.code-lines');
+ btn.setAttribute('aria-label', 'Copy code');
+ wireCopy(btn, function () { return src.textContent; });
+ wrap.appendChild(btn);
+ });
+
+ // Long listings start collapsed; with JS off they render in full.
+ var code = document.querySelector('.src .code');
+ if (code) {
+ var lines = parseInt(code.getAttribute('data-lines'), 10) || 0;
+ if (lines > 120) {
+ code.classList.add('collapsed');
+ var more = document.createElement('button');
+ more.type = 'button'; more.className = 'src-expand';
+ more.setAttribute('aria-expanded', 'false');
+ more.textContent = 'Show all ' + lines + ' lines';
+ code.parentNode.insertBefore(more, code.nextSibling);
+ more.addEventListener('click', function () {
+ var open = code.classList.toggle('collapsed') === false;
+ more.setAttribute('aria-expanded', open ? 'true' : 'false');
+ more.textContent = open ? 'Collapse source' : 'Show all ' + lines + ' lines';
+ if (!open) code.scrollIntoView({ block: 'start' });
});
- });
+ }
}
+
+ // Left/right arrows page between entries of the same kind.
+ document.addEventListener('keydown', function (e) {
+ if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
+ if (box && box.open) return;
+ var ae = document.activeElement;
+ if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return;
+ var rel = e.key === 'ArrowLeft' ? 'prev' : e.key === 'ArrowRight' ? 'next' : '';
+ if (!rel) return;
+ var a = document.querySelector('.pager a[rel="' + rel + '"]');
+ if (a) { location.href = a.href; }
+ });
})();
diff --git a/docs/gallery/index.html b/docs/gallery/index.html
index 8166a1b3..59c0754e 100644
--- a/docs/gallery/index.html
+++ b/docs/gallery/index.html
@@ -161,7 +161,7 @@
.card:hover { border-color: var(--select); outline-color: var(--select); }
.card.hidden { display: none; }
.card-media { display: block; background: var(--bg2); line-height: 0; }
- .card-media img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .card-media img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.card-body { padding: 1.15rem 1.4rem 1.45rem; display: flex; flex-direction: column; flex: 1 1 auto; }
.card-body h2 { font-family: var(--font-mono); font-size: 1rem; font-weight: 400; margin-bottom: 0.5rem; }
.card-body h2 a { color: var(--text); }
@@ -176,7 +176,7 @@
/* ---- detail page ---- */
.detail-hero { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
background: var(--bg2); padding: 0; display: block; width: 100%; cursor: zoom-in; line-height: 0; }
- .detail-hero img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
+ .detail-hero img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.zoom-hint { color: var(--text-dim); font-size: 0.78rem; margin-top: 0.4rem; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; }
@@ -192,7 +192,7 @@
color: var(--text-dim); border-radius: var(--radius); padding: 0 0.85rem; cursor: pointer;
font-family: var(--font-sans); font-size: 0.82rem; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
.copy-btn:hover { color: var(--select); border-color: var(--select); }
- .detail-section { margin-top: 2.25rem; }
+ .detail-section { margin-top: 2.25rem; scroll-margin-top: 60px; }
.detail-section > h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
font-size: 1.45rem; letter-spacing: 0.005em; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
@@ -222,11 +222,68 @@
flex-wrap: wrap; margin-bottom: 0.6rem; color: var(--text-dim); font-size: 0.85rem; }
.src-meta code { font-family: var(--font-mono); font-size: 0.82rem; }
+ /* ---- detail page: pager, tags, related, anchors, code blocks ---- */
+ .pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem;
+ margin: 0 0 1.25rem; font-size: 0.85rem; }
+ .pager-foot { margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
+ .pager a { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .pager a:hover { color: var(--select); text-decoration: none; }
+ .pager [rel="next"] { text-align: right; }
+ .pager-pos { text-align: center; }
+ @media (max-width: 559px), (hover: none) { .pager-keys { display: none; } }
+ .pager kbd { font-family: var(--font-mono); font-size: 0.65rem; border: 1px solid var(--border);
+ border-radius: 3px; padding: 0 0.3rem; margin: 0 0.1rem; }
+ .taglist { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: -0.5rem 0 1.5rem; }
+ .taglist a { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 3px;
+ padding: 0.12rem 0.6rem; transition: color 0.15s, border-color 0.15s; }
+ .taglist a:hover { color: var(--select); border-color: var(--select); text-decoration: none; }
+ .related-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
+ @media (min-width: 560px) { .related-grid { grid-template-columns: repeat(3, 1fr); } }
+ .card.mini .card-body { padding: 0.65rem 0.9rem 0.75rem; }
+ .card.mini h3 { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 400; margin-bottom: 0.2rem; }
+ .card.mini h3 a { color: var(--text); }
+ .card.mini h3 a:hover { color: var(--select); text-decoration: none; }
+ .card.mini p { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45;
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+ .md h1, .md h2, .md h3 { scroll-margin-top: 60px; }
+ .anchor { color: var(--text-dim); margin-left: 0.4rem; font-weight: 400; opacity: 0;
+ transition: opacity 0.15s; }
+ .md h1:hover .anchor, .md h2:hover .anchor, .md h3:hover .anchor, .anchor:focus-visible { opacity: 1; }
+ .anchor:hover { color: var(--select); text-decoration: none; }
+ .codewrap { position: relative; }
+ .codewrap > .copy-btn { position: absolute; top: 0.45rem; right: 0.45rem; padding: 0.2rem 0.55rem;
+ font-size: 0.72rem; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s; }
+ .codewrap:hover > .copy-btn, .codewrap > .copy-btn:focus-visible, .codewrap > .copy-btn.done { opacity: 1; }
+ @media (hover: none) { .codewrap > .copy-btn { opacity: 1; } }
+ .copy-btn.done { color: var(--ok); border-color: var(--ok); }
+ .copy-btn.fail { color: var(--code-k); border-color: var(--code-k); }
+ .src .code { display: grid; grid-template-columns: auto minmax(0, 1fr);
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
+ overflow: hidden; }
+ .src .code pre { background: none; border: 0; border-radius: 0; }
+ .src .code .gutter { color: color-mix(in srgb, var(--text-dim) 60%, transparent); text-align: right;
+ user-select: none; padding-right: 0.75rem; border-right: 1px solid var(--border);
+ background: var(--bg2); overflow: hidden; }
+ .src .code.collapsed { max-height: 42rem; position: relative; }
+ .src .code.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 5rem;
+ background: linear-gradient(transparent, var(--surface-2)); pointer-events: none; }
+ .src-expand { display: block; margin: 0.6rem auto 0; background: var(--surface-2);
+ border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius);
+ padding: 0.4rem 0.9rem; cursor: pointer; font: 500 0.8rem var(--font-sans);
+ transition: color 0.15s, border-color 0.15s; }
+ .src-expand:hover { color: var(--select); border-color: var(--select); }
+
.lightbox { border: 0; margin: 0; padding: 2rem; width: 100%; height: 100%; max-width: none;
max-height: none; background: rgba(0,0,0,0.85); cursor: zoom-out; }
- .lightbox[open] { display: flex; align-items: center; justify-content: center; }
+ /* margin:auto (not align/justify-content) centers without clipping the
+ top-left of a native-size image that overflows a small viewport. */
+ .lightbox[open] { display: flex; overflow: auto; }
.lightbox::backdrop { background: transparent; }
- .lightbox img { max-width: 100%; max-height: 100%; border-radius: var(--radius); }
+ .lightbox img { margin: auto; max-width: 100%; max-height: 100%; border-radius: var(--radius);
+ cursor: zoom-in; }
+ .lightbox.native img { max-width: none; max-height: none; cursor: zoom-out; }
.lightbox-close { position: absolute; top: 0.9rem; right: 0.9rem; background: var(--surface-2);
color: var(--text); border: 1px solid var(--border); border-radius: var(--radius);
font: 500 0.8rem var(--font-sans); padding: 0.4rem 0.75rem; cursor: pointer; }
@@ -343,7 +400,7 @@ Examples and Showcase