Files
settled-reach/tooling/scripts/blender/blender_author_cargo_pants.py
T
jpmschweitzerandClaude Opus 5.5 c597ec9131 docs(tooling): T-1253 — sweep the live references to retired tool paths
A script scanned every tracked doc, rule, skill, agent, hook and source file
for tooling/ paths that no longer exist, skipping historical records (sprints,
discussions, workshops, governance, generated wiki pages). It found 62. The
ones that tell a reader what to RUN now name the reach verb:

- The atlas skill still sent agents to tooling/atlas, atlas-verify,
  atlas-update-field and atlas-commit-and-sync — about forty lines, all
  retired in T-1285. They now name the `reach atlas` verbs, and the skill
  records that commit-and-sync STAGES by default (--commit to commit) and
  takes --corridor as an option.
- The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si
  and clerk briefings sent those agents to the retired tooling/db/decision
  and sqlite-query CLIs and to decisions/*.md paths that moved to
  governance/ in the pql migration. They now name pql.
- The ticket-cli rule documented `pql decisions read`, which does not exist;
  `show` already includes the body.
- The culture authoring guide and the RON sources name
  `reach validate ron`, with the same arguments as before.
- The 41 Blender payloads' usage lines ran the retired tooling/blender
  wrapper, and the docstrings still cited pre-carve-out paths. They now read
  `reach blender run <payload>`.
- Doc comments in server/, client/, wiki TOMLs and the domain modules.

What is left is deliberate: "Formerly …" provenance, dated plans and findings
docs, the retired-pipeline doc, and a build-artefact path.

project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched
files are in the canvas-version registry (trait_catalog_reader.rs, since
T-1289, canvas_sources.py itself, and two client files). The gate is
path-based and has no override. The previous push was rejected on exactly
this.

Three of the edits are stamped ledger sources, so systems.db is regenerated
and the stamp is fresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:13:58 +02:00

502 lines
20 KiB
Python

"""
blender_author_cargo_pants.py (T-1089 wave 2, cargo_pants)
Authors full-length CARGO pants as per-body offset shells. Reuses
blender_author_offset_shell.py as the base library (scene build, join,
offset, solidify, GLB export) and blender_author_denim_pants.py as the
proven bottoms companion (boundary WELD of coincident segment-seam rings,
open-rim FLATTENING onto clean planes, per-body LegLandmarks, waist flare,
hem cut, parked logo UV2) — the denim script's utilities are imported as a
module, not re-derived.
What cargo adds, as reusable parameters:
* --straight: STRAIGHT-FIT boost — extra radial stand-off away from the
per-z leg axis, ramping 0 at the knee to the full value at the ankle, so
the lower leg does not taper with the calf (straight silhouette; also
buys lower-leg clip clearance). Applied post-offset, pre-solidify.
* TEXEL-level cargo feature painting (same one-field-drives-both design as
denim: painted albedo and region mask always agree):
- albedo: large outer-thigh CARGO POCKETS with button-down FLAPS
(filled panels, border stitching, centre pleat, two flap
buttons), outseam/inseam side seams, centre-front fly
stitch, belt loops + waist button, waistband and hem border
stitching — flat tone-on-tone utility look, identity
carried by the texture (style pin: modern only).
- mask: waistband -> R, legs -> G, pockets + flaps -> B
(spec: waistband=R, legs=G, pockets+flaps=B).
A SIGNED azimuth arc around the per-z leg axis (0 at the outer side,
positive toward the front) places the pockets slightly forward-of-side
and the flap buttons symmetrically — the same analytic-field approach as
the denim script, generalised from unsigned to signed arc.
* pocket proportions anchor to each body's own THIGH span (crotch ->
knee) and hip half-width, so the pockets stay proportional across all
11 bodies (child included) — the base.derive_thresholds philosophy.
Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r; natural
boundaries give the waist opening and ankle hems. Regions are painted, not
modelled — no 3D pocket geometry (texture carries identity).
Usage (cargo_pants reference invocation):
reach blender run blender_author_cargo_pants \
client/assets/characters/bodies \
client/assets/characters/clothing/cargo_pants \
[--bodies average_m,child,...] [--offset 0.013] [--hem-frac 1.0] \
[--waist-flare 0.007] [--straight 0.010] [--base-rgb 0.36,0.37,0.29] \
[--plain]
Writes per body: <out_dir>/<body>.glb (skinned, albedo embedded)
<out_dir>/<body>_mask.png (RGBA region mask, UV0)
<out_dir>/<body>_base_albedo.png
Plus: <out_dir>/base_albedo.png (average_m's, shared sidecar)
<out_dir>/reference_mask.png (average_m's, runtime fallback)
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house
wardrobe), Q-060 (per-body offset shells).
"""
import importlib.util
import os
import shutil
import sys
import bmesh
import bpy
import numpy as np
# --------------------------------------------------------------------------
# Import the base offset-shell module + the denim bottoms companion
# (weld / rim-flatten / flare / landmarks utilities live there)
# --------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
def _load(name, fname):
spec = importlib.util.spec_from_file_location(
name, os.path.join(_HERE, fname))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
base = _load("offset_shell_base", "blender_author_offset_shell.py")
denim = _load("denim_pants_lib", "blender_author_denim_pants.py")
log = base.log
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
# --------------------------------------------------------------------------
# Parameters
# --------------------------------------------------------------------------
COVERED_SEGMENTS = [
"seg_hips",
"seg_leg_upper_l", "seg_leg_upper_r",
"seg_leg_lower_l", "seg_leg_lower_r",
]
HEM_FRAC = 1.0 # 1.0 = full leg to the ankle
OFFSET_M = 0.013 # cargo sits a touch looser than jeans (0.012)
STRAIGHT_M = 0.010 # straight-fit radial boost at the ankle (0 at knee)
WAIST_FLARE_M = 0.007 # waistband rim stand-off (deep-crouch mitigation,
# proven on jeans)
TEX_SIZE = 1024 # painted pockets/seams need >512
# Vertical proportions — fractions of the garment span (waist_z - hem_z),
# via denim.LegLandmarks.finalize (BAND/CUFF/SEAM fractions proven on jeans).
# Pocket proportions — fractions of the THIGH span (crotch_z - knee_z).
FLAP_TOP_FRAC = 0.18 # flap top below the crotch
FLAP_H_FRAC = 0.14 # flap height
POCKET_BOT_FRAC = 0.74 # pocket bottom below the crotch
# Horizontal proportions — signed azimuth ARC around the per-z leg axis
# (metres on the reference body, scaled by the body's hip ratio lm.sh).
POCKET_HALF_ARC_M = 0.058 # pocket half-width
FLAP_EXTRA_ARC_M = 0.007 # flap overhangs the pocket by this much per side
POCKET_FWD_BIAS_M = 0.010 # pocket centre sits slightly forward of the outseam
BUTTON_R_M = 0.0075 # flap button radius
LOOP_X_FRACS = (0.55, 1.30) # belt-loop |x| positions (fractions of hip_x)
LOOP_W_M = 0.016 # belt loop width (m, scaled by hip ratio)
# Utility olive/grey style (sRGB floats; flat toon-friendly, tone-on-tone).
CARGO_RGB = (0.360, 0.370, 0.290) # utility olive-grey
THREAD_RGB = (0.225, 0.235, 0.185) # tone-on-tone darker stitching
BUTTON_RGB = (0.170, 0.160, 0.140) # matte button
ALBEDO_NOISE = 0.020 # +/- woven jitter
POCKET_SHADE = 0.93 # pocket panel albedo darkening
FLAP_SHADE = 0.85 # flap panel albedo darkening
LOOP_SHADE = 0.80 # belt-loop albedo darkening
PLAIN = False # --plain: skip pockets/stitch/button paint
NOISE_SEED = 3089
# --------------------------------------------------------------------------
# Geometry: straight-fit boost (cargo's reusable fit parameter)
# --------------------------------------------------------------------------
def straight_boost(shell, lm, boost):
"""Extra RADIAL stand-off away from the per-z leg axis below the knee,
ramping linearly from 0 at the knee to `boost` at the ankle. The body
calf tapers; pushing the shell out progressively keeps the pant leg
straight (cargo silhouette) instead of hugging the calf. Applied
post-offset, pre-solidify; weights/UVs are untouched."""
if boost <= 0.0:
return
knee_z = float(lm.leg_z_pts[1])
ankle_z = float(lm.leg_z_pts[0])
span = max(knee_z - ankle_z, 1e-6)
bm = bmesh.new()
bm.from_mesh(shell.data)
n = 0
for v in bm.verts:
z = v.co.z
if z >= knee_z:
continue
t = min((knee_z - z) / span, 1.0)
side = 1.0 if v.co.x >= 0.0 else -1.0
cx = side * float(np.interp(z, lm.leg_z_pts, lm.leg_x_pts))
cy = float(np.interp(z, lm.leg_z_pts, lm.leg_y_pts))
dx = v.co.x - cx
dy = v.co.y - cy
r = (dx * dx + dy * dy) ** 0.5
if r > 1e-6:
v.co.x += boost * t * dx / r
v.co.y += boost * t * dy / r
n += 1
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"straight-fit boost: {n} verts, +{boost * 1000:.1f} mm radial at "
f"ankle (ramped from knee z={knee_z:.3f})")
# --------------------------------------------------------------------------
# Cargo feature field (texel-level; drives albedo AND mask together)
# --------------------------------------------------------------------------
def _cargo_field(px, py, pz, lm):
"""Evaluate cargo features at texel 3D positions (numpy arrays).
Returns a dict of bool arrays. `pocket`/`flap` feed the mask B channel;
the stitch/fill features feed the albedo only.
"""
w2 = lm.seam_w * 0.5
front = py * FRONT_Y_SIGN > 0.004
in_band = pz >= lm.band_z
mid = (~in_band) & (pz > lm.hem_z + 0.5 * lm.cuff_h)
# Per-z leg axis, mirrored by x sign; SIGNED azimuth arc around it
# (0 at the outer side, positive toward the front, +/-pi*r at the inseam).
side = np.where(px >= 0.0, 1.0, -1.0)
cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts)
cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts)
dx = px - cx
dy = py - cy
r = np.hypot(dx, dy) + 1e-9
theta = np.arctan2(dy * FRONT_Y_SIGN / r, np.clip(dx * side / r, -1.0, 1.0))
sarc = theta * r # signed arc distance from the outer direction
# Side seams: outseam on the outer azimuth; inseam opposite, below crotch.
outseam = mid & (np.abs(sarc) < w2)
arc_in = (np.pi - np.abs(theta)) * r
inseam = mid & (arc_in < w2) \
& (pz < lm.crotch_z - 0.01 * lm.span / denim._REF_SPAN)
# Centre-front fly stitch (slightly off-centre, like the jeans J-front).
fly = front & mid & (np.abs(px - 0.012 * lm.sh) < w2) \
& (pz > lm.crotch_z + 0.015 * lm.span / denim._REF_SPAN)
# --- cargo pocket + flap (outer thigh, slightly forward-of-side) --------
knee_z = float(lm.leg_z_pts[1])
thigh = max(lm.crotch_z - knee_z, 1e-6)
flap_top = lm.crotch_z - FLAP_TOP_FRAC * thigh
flap_bot = flap_top - FLAP_H_FRAC * thigh
pocket_bot = lm.crotch_z - POCKET_BOT_FRAC * thigh
p_arc = POCKET_HALF_ARC_M * lm.sh
f_arc = p_arc + FLAP_EXTRA_ARC_M * lm.sh
a = sarc - POCKET_FWD_BIAS_M * lm.sh # pocket-centred signed arc
pocket = (np.abs(a) < p_arc) & (pz <= flap_top) & (pz >= pocket_bot)
flap = (np.abs(a) < f_arc) & (pz <= flap_top) & (pz >= flap_bot)
# Pocket border stitching (sides + bottom), centre pleat, flap edge.
p_side = (np.abs(np.abs(a) - p_arc) < w2) & (pz <= flap_top) \
& (pz >= pocket_bot)
p_bottom = (np.abs(pz - pocket_bot) < w2) & (np.abs(a) < p_arc)
pleat = (np.abs(a) < w2) & (pz < flap_bot - 2.0 * w2) & (pz >= pocket_bot)
f_edge = flap & ((np.abs(np.abs(a) - f_arc) < w2)
| (np.abs(pz - flap_bot) < w2))
pkt_stitch = p_side | p_bottom | pleat | f_edge
# Two flap buttons, symmetric about the pocket centre.
btn_r = BUTTON_R_M * lm.sh
btn_z = flap_bot + 0.30 * (flap_top - flap_bot)
buttons = np.zeros_like(front)
for bx in (-0.45 * f_arc, 0.45 * f_arc):
buttons |= np.hypot(a - bx, pz - btn_z) < btn_r
# Border stitching: waistband seam + hem stitch.
wstitch = np.abs(pz - lm.band_z) < w2
hemstitch = np.abs(pz - lm.cuff_top) < w2
return {
"front": front, "in_band": in_band,
"seams": outseam | inseam | fly,
"wstitch": wstitch, "hemstitch": hemstitch,
"pocket": pocket, "flap": flap,
"pkt_stitch": pkt_stitch, "buttons": buttons,
}
def _paint_texels(px, py, pz, noise, lm):
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
n = px.shape[0]
f = _cargo_field(px, py, pz, lm)
# --- albedo -------------------------------------------------------------
alb = np.empty((n, 4), dtype=np.float32)
for c in range(3):
alb[:, c] = CARGO_RGB[c] + noise
alb[:, 3] = 1.0
if not PLAIN:
# Belt loops: darkened bands on the waistband.
loop_w = LOOP_W_M * lm.sh
loops = np.zeros(n, dtype=bool)
for fx in LOOP_X_FRACS:
loops |= np.abs(np.abs(px) - fx * lm.hip_x) < loop_w * 0.5
loops |= (~f["front"]) & (np.abs(px) < loop_w * 0.5) # centre-back
loops &= f["in_band"]
alb[loops, :3] *= LOOP_SHADE
# Panel fills first, stitch lines on top.
alb[f["pocket"] & ~f["flap"], :3] *= POCKET_SHADE
alb[f["flap"], :3] *= FLAP_SHADE
thread = f["seams"] | f["wstitch"] | f["hemstitch"] | f["pkt_stitch"]
alb[thread, 0] = THREAD_RGB[0]
alb[thread, 1] = THREAD_RGB[1]
alb[thread, 2] = THREAD_RGB[2]
# Flap buttons + waist button.
btn = f["buttons"] | (f["front"] & (
np.hypot(px, pz - (lm.band_z + 0.5 * lm.band_h)) < 0.009 * lm.sh))
alb[btn, 0] = BUTTON_RGB[0]
alb[btn, 1] = BUTTON_RGB[1]
alb[btn, 2] = BUTTON_RGB[2]
# --- region mask: waistband R / legs G / pockets+flaps B -----------------
mask = np.zeros((n, 4), dtype=np.float32)
is_b = (f["pocket"] | f["flap"]) & ~f["in_band"]
is_g = ~(f["in_band"] | is_b)
mask[f["in_band"], 0] = 1.0
mask[is_b, 2] = 1.0
mask[is_g, 1] = 1.0
return alb, mask
def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H):
"""Barycentric texel fill of one UV triangle: interpolate 3D positions,
evaluate the cargo field, write albedo + mask together (same rasterizer
contract as the denim companion, pointed at the cargo painter)."""
a, b, c = uvs
A, B, C = cos
ax, ay = a.x * (W - 1), a.y * (H - 1)
bx, by = b.x * (W - 1), b.y * (H - 1)
cx, cy = c.x * (W - 1), c.y * (H - 1)
minx = max(int(np.floor(min(ax, bx, cx))), 0)
maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1)
miny = max(int(np.floor(min(ay, by, cy))), 0)
maxy = min(int(np.ceil(max(ay, by, cy))), H - 1)
if minx > maxx or miny > maxy:
return
denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
if abs(denom) < 1e-9:
return
ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1]
pxg = xs + 0.5
pyg = ys + 0.5
w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom
w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom
w2 = 1.0 - w0 - w1
inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4)
if not inside.any():
return
w0i, w1i, w2i = w0[inside], w1[inside], w2[inside]
px3 = w0i * A.x + w1i * B.x + w2i * C.x
py3 = w0i * A.y + w1i * B.y + w2i * C.y
pz3 = w0i * A.z + w1i * B.z + w2i * C.z
ysin = ys[inside]
xsin = xs[inside]
alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm)
alb_buf[ysin, xsin] = alb
mask_buf[ysin, xsin] = mask
def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body):
"""Rasterize all UV0 triangles once, producing the painted albedo and the
region mask from one shared feature-field evaluation per texel."""
W = H = TEX_SIZE
rng = np.random.default_rng(NOISE_SEED)
noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5)
* 2.0 * ALBEDO_NOISE)
alb_buf = np.empty((H, W, 4), dtype=np.float32)
for c in range(3):
alb_buf[:, :, c] = CARGO_RGB[c] + noise_buf
alb_buf[:, :, 3] = 1.0
mask_buf = np.zeros((H, W, 4), dtype=np.float32)
mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe)
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
if not len(bm.loops.layers.uv):
raise RuntimeError("no UV layer for albedo/mask paint")
uv_layer = bm.loops.layers.uv[0]
tri_count = 0
for face in bm.faces:
loops = face.loops[:]
uvs = [loop[uv_layer].uv.copy() for loop in loops]
cos = [loop.vert.co.copy() for loop in loops]
for i in range(1, len(uvs) - 1):
_raster_tri_paint(
alb_buf, mask_buf, noise_buf,
(uvs[0], uvs[i], uvs[i + 1]),
(cos[0], cos[i], cos[i + 1]),
lm, W, H)
tri_count += 1
bm.free()
log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})")
def _save(buf, name, path):
img = bpy.data.images.new(name, W, H, alpha=True)
img.pixels.foreach_set(buf.reshape(-1))
img.update()
img.filepath_raw = path
img.file_format = 'PNG'
img.save()
return img
albedo_img = _save(alb_buf, f"cargo_albedo_{body}", albedo_path)
_save(mask_buf, f"cargo_mask_{body}", mask_path)
log(f"saved albedo -> {albedo_path}")
log(f"saved mask -> {mask_path}")
return albedo_img
# --------------------------------------------------------------------------
# Per-body authoring
# --------------------------------------------------------------------------
def author_cargo_shell(body_dir, out_dir, body, offset, hem_frac):
crotch_z, _hips_top = denim.probe_hips_bounds(body_dir)
base.clear_scene()
base.COVERED_SEGMENTS = COVERED_SEGMENTS
shell, armature = base.build_covered_mesh(body_dir)
denim.weld_boundaries(shell)
zs = [v.co.z for v in shell.data.vertices]
waist_z, ankle_z = max(zs), min(zs)
lm = denim.LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005)
hem_z = denim.hem_cut(shell, lm, hem_frac)
ankle_plane = float(lm.leg_z_pts[0]) if hem_frac >= 0.999 else hem_z
waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane)
base.offset_outward(shell, offset)
straight_boost(shell, lm, STRAIGHT_M)
denim.waist_flare(shell, waist_plane,
denim.BAND_FRAC * (waist_plane - ankle_plane),
WAIST_FLARE_M)
base.solidify(shell, base.CLOTH_THICKNESS_M)
denim.clamp_waist_residue(shell, waist_plane)
# Landmarks reference the CLEAN rims (band under the flattened waist edge,
# hem stitch above the flattened ankle rim).
lm.waist_z = waist_plane
lm.finalize(ankle_plane)
denim.author_parked_uv2(shell)
albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png")
mask_path = os.path.join(out_dir, f"{body}_mask.png")
albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body)
base.assign_fabric_material(shell, albedo_img)
base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb"))
def main():
global HEM_FRAC, CARGO_RGB, PLAIN, WAIST_FLARE_M, STRAIGHT_M
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if len(argv) < 2:
print("Usage: -- <bodies_root> <out_dir> [--bodies a,b,c] "
"[--offset M] [--hem-frac F] [--waist-flare M] [--straight M] "
"[--base-rgb r,g,b] [--plain]")
sys.exit(1)
bodies_root = argv[0]
out_dir = argv[1]
offset = OFFSET_M
if "--offset" in argv:
offset = float(argv[argv.index("--offset") + 1])
if "--hem-frac" in argv:
HEM_FRAC = float(argv[argv.index("--hem-frac") + 1])
if "--waist-flare" in argv:
WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1])
if "--straight" in argv:
STRAIGHT_M = float(argv[argv.index("--straight") + 1])
if "--base-rgb" in argv:
CARGO_RGB = tuple(
float(v) for v in argv[argv.index("--base-rgb") + 1].split(","))
if "--plain" in argv:
PLAIN = True
bodies = base.BODY_TYPES
if "--bodies" in argv:
bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")]
os.makedirs(out_dir, exist_ok=True)
log(f"cargo per-body mode: {len(bodies)} bodies, offset "
f"{offset * 1000:.0f} mm, straight {STRAIGHT_M * 1000:.0f} mm, "
f"hem-frac {HEM_FRAC}, plain={PLAIN}")
results = []
for body in bodies:
body_dir = os.path.join(bodies_root, body)
log(f"=== {body} ===")
if not os.path.isdir(body_dir):
results.append((body, "skipped: body dir missing"))
continue
try:
author_cargo_shell(body_dir, out_dir, body, offset, HEM_FRAC)
results.append((body, "ok"))
except Exception as exc:
log(f"ERROR {body}: {exc}")
import traceback
traceback.print_exc()
results.append((body, f"error: {exc}"))
ref = base.REFERENCE_BODY
ref_mask = os.path.join(out_dir, f"{ref}_mask.png")
if os.path.isfile(ref_mask):
shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png"))
log(f"copied {ref}_mask.png -> reference_mask.png (fallback)")
ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png")
if os.path.isfile(ref_alb):
shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png"))
log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)")
log("=" * 50)
for body, status in results:
log(f" {body:12s} {status}")
ok = sum(1 for _, s in results if s == "ok")
log(f"OK={ok}/{len(results)}")
if ok != len(results):
sys.exit(1)
log("DONE")
if __name__ == "__main__":
main()