tailor(voyager): strapless bandeau lane -- elastic-free zero-g recipe + 23-variant work log

Female voyager bandeau top from the fc-voyager-kilt-gpt refs. Winning
construction (md_final_v16/v23): seamless-loop doctrine -- NO edge elastic
(gathering is what notched the armpit junctions), body-snug cut, zero-g wrap
(SetSimulationGravity(0) BEFORE ResetClothArrangement -- one full-gravity
materialization tick drops the bistable tube into the low basin), stiff 40f
positioning then soft 110f conform, self-measuring retry loop.

Hard-won facts logged in the scripts: strapless garments never survive full
gravity (frictionless skin + stretchy elastic); arrangement x IS azimuth
(Body_Back_Center_1 native x=0, forcing 50/50 sews both panels into a front
pouch); SetArrangementOrientation flips panel facing but mirrors it L/R;
frozen cloth renders cyan; back panel textured face points at the skin --
fix is manual Flip Normal (no API).

Texture: positioned plait/rope/fringe tile spanning the 170mm panel once
(hem anchors at v=0.5 and wraps). manual_fit_backup.zprj preserves Can's
hand-fitted drape. Exports still pending final drape approval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Can Narin
2026-08-18 22:46:31 +03:00
parent fea96bf582
commit 0fae2b514c
38 changed files with 3410 additions and 0 deletions
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
make_voyager_bandeau.py -- golden plaited-weave band with black rope trim and
fringe for the female voyager bandeau top (lena_voyager_bandeau_v1).
python tools/tailor/textures/make_voyager_bandeau.py
Derived from the reference renders
`fc-voyager-kilt-gpt-piece-top{,-front,-back}.png` (docs repo): a diagonal
basket plait in warm golden tan, a black braided-rope band near each edge, and
a short strand fringe extending to both the top and bottom edges.
POSITIONED TILE, same doctrine as make_kilt_cloth.py: spans the bandeau
panel's full 170 mm height exactly once (dpi = SIZE / (170 / 25.4)), so every
zone lands at a fixed garment height:
garment y (mm, hem=0) zone
0-15 bottom fringe strands (painted, not geometry -- v1)
15-27 black rope band (braided chevrons)
27-143 diagonal basket plait
143-155 black rope band
155-170 top fringe strands (tips ragged toward y=170)
Horizontally the 170 mm tile repeats ~3.2x across the 550 mm panel; the plait
pitch (17 mm) and rope chevron pitch (32 px) both divide the tile exactly, so
the wrap is seamless.
IF THE ROPE BANDS RENDER SWAPPED TOP-FOR-BOTTOM, MD anchored the texture the
other way up -- regenerate with FLIP=True rather than touching the pattern.
(Anchor per probe 2026-08-18: pattern y=0 (hem) maps to image v=0.5 and wraps.)
Albedo baked in (pipeline has no alpha, texture replaces base color), warm
ramp r>g>b, deterministic LCG noise.
"""
import os
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "voyager_bandeau.png")
SIZE = 1024 # px, square
CLOTH_MM = 170.0 # spans the bandeau panel height exactly once
DPI = SIZE / (CLOTH_MM / 25.4)
FLIP = False # set True if MD anchors the texture upside-down
PX_PER_MM = SIZE / CLOTH_MM
# zone boundaries in garment mm (hem = 0)
BOT_FRINGE_TOP = 15.0
BOT_ROPE_TOP = 27.0
TOP_ROPE_BOT = 143.0
TOP_FRINGE_BOT = 155.0
PLAIT_PITCH_MM = 17.0 # strand pitch; 170/17 = 10 cells -> seamless wrap
ROPE_CHEVRON_PX = 32 # divides 1024 -> seamless wrap
BASE = (203, 165, 110) # golden tan plait (reference lit mean ~(200,165,115))
ROPE = (46, 40, 32) # near-black rope, warm
ROPE_HI = (92, 80, 62) # braid ridge highlight
FRINGE = (191, 152, 100)
FRINGE_GAP = (140, 108, 70)
SHADOW = (74, 60, 44) # past a fringe tip: painted shadow (no alpha in lane)
OVER_LIFT = 14
UNDER_DROP = 18
ROUND_SHADE = 12
SLUB_RANGE = 8
MOTTLE = 10
MOTTLE_CELL = 64
def lcg(seed):
state = seed
while True:
state = (1103515245 * state + 12345) % (2 ** 31)
yield state / float(2 ** 31)
def value_noise(cells, seed):
g = lcg(seed)
grid = [[(next(g) * 2.0 - 1.0) for _ in range(cells + 1)] for _ in range(cells + 1)]
def sample(x, y):
fx, fy = x * cells / SIZE, y * cells / SIZE
x0, y0 = int(fx), int(fy)
tx, ty = fx - x0, fy - y0
tx, ty = tx * tx * (3 - 2 * tx), ty * ty * (3 - 2 * ty)
a = grid[y0][x0] * (1 - tx) + grid[y0][x0 + 1] * tx
b = grid[y0 + 1][x0] * (1 - tx) + grid[y0 + 1][x0 + 1] * tx
return a * (1 - ty) + b * ty
return sample
def mm_from_row(y):
"""Garment height in mm for an image row (anchor: hem -> v=0.5, wraps)."""
v = (y + 0.5) / SIZE
return CLOTH_MM - CLOTH_MM * ((v - 0.5) % 1.0)
def plait(xm, ym, slub_a, slub_b):
"""Diagonal basket plait: two +/-45-degree strand families, over-under.
Returns a brightness delta to apply on BASE."""
u = (xm + ym) / PLAIT_PITCH_MM
w = (xm - ym) / PLAIT_PITCH_MM
a, b = int(u // 1), int(w // 1)
fu, fw = u - a, w - b
over_a = ((a + b) % 2) == 0
if over_a:
# family A on top: rounded across its width (fw), slub along it
c = abs(fw - 0.5) * 2.0
d = OVER_LIFT - int(ROUND_SHADE * c * c)
d += slub_a[a % len(slub_a)]
if fw < 0.06 or fw > 0.94: # crevice against the under strand
d -= 26
else:
c = abs(fu - 0.5) * 2.0
d = OVER_LIFT - int(ROUND_SHADE * c * c) - UNDER_DROP // 2
d += slub_b[b % len(slub_b)]
if fu < 0.06 or fu > 0.94:
d -= 26
return d
def main():
g = lcg(20260818)
slub_a = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)]
slub_b = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)]
tip_noise = [next(g) for _ in range(64)]
mottle = value_noise(SIZE // MOTTLE_CELL, seed=777)
img = Image.new("RGB", (SIZE, SIZE))
px = img.load()
strand_w = 16 # fringe strand width px (~2.7 mm)
for y in range(SIZE):
mm = mm_from_row(y)
ym = mm
for x in range(SIZE):
xm = x / PX_PER_MM
wear = int(mottle(x, y) * MOTTLE)
if mm < BOT_FRINGE_TOP or mm > TOP_FRINGE_BOT:
# fringe: vertical strands, ragged tips toward the edge
sid = (x // strand_w) % len(tip_noise)
ragged = tip_noise[sid] * 8.0
past_tip = (mm < ragged if mm < BOT_FRINGE_TOP
else mm > CLOTH_MM - ragged)
if past_tip:
base, d = SHADOW, wear // 2
else:
u = x % strand_w
base = FRINGE_GAP if u < 2 else FRINGE
d = slub_a[sid] + wear
elif mm < BOT_ROPE_TOP or mm > TOP_ROPE_BOT:
# braided rope: diagonal chevron ridges
phase = ((x + int(mm * PX_PER_MM) * 2) % ROPE_CHEVRON_PX) \
/ float(ROPE_CHEVRON_PX)
base = ROPE_HI if phase < 0.30 else ROPE
d = wear // 2
else:
base = BASE
d = plait(xm, ym, slub_a, slub_b) + wear
r = base[0] + d
gg = base[1] + int(d * 0.82)
b = base[2] + int(d * 0.66)
px[x, y] = (max(0, min(255, r)), max(0, min(255, gg)),
max(0, min(255, b)))
if FLIP:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
img.save(OUT, dpi=(DPI, DPI))
print("wrote %s (%dx%d, dpi %.2f -> %.0f mm of cloth; ropes %d-%d & %d-%d mm, "
"fringe 0-%d & %d-%d mm)"
% (OUT, SIZE, SIZE, DPI, CLOTH_MM, BOT_FRINGE_TOP, BOT_ROPE_TOP,
TOP_ROPE_BOT, TOP_FRINGE_BOT, BOT_FRINGE_TOP, TOP_FRINGE_BOT, CLOTH_MM))
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB