Files
animation/tools/tailor/md_cape_v1.py
T
jeremy 3ba86b2ea8 feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:55:43 -07:00

577 lines
33 KiB
Python

# Cape v1 -- open-front feathered cloak, 3 panels, hung from the shoulders.
#
# python tools/md_bridge.py --file tools/tailor/md_cape_v1.py --timeout 700
#
# Reference: tools/tailor/references/cape_test1_ref.png (5 views incl. the BACK,
# which matters because ExportSnapshot3D structurally cannot show a rear -- see
# the skill's references/tooling.md SS1.1. A cape is a back garment, so the
# turntable is the PRIMARY view here, not an afterthought.)
#
# -- WHAT THE REFERENCE ACTUALLY IS -------------------------------------------
# A feather cloak: densely layered teal/turquoise feathers, a stand-up red collar
# with a woven diamond band, and twin vertical bands with shell-button rows down
# both front edges. Full length, open down the front.
#
# Per doctrine (SKILL.md, "textiles not tailoring"): the feathers are the DESIGN,
# not the tailoring. ~500 feathers as geometry would blow the game tri budget
# outright. So v1 models the SHAPE only -- a plain cloak -- and the feathers,
# collar band and button rows become a generated texture in v2. Judge shape with
# the texture OFF (PLAIN=True): cloth renders white on its front face and grey on
# its back, so grey-from-outside is inside out and a white streak is a fold. A
# busy feather motif hides both completely.
#
# -- CONSTRUCTION -------------------------------------------------------------
# Three panels: one back, two fronts. Topology is the pari's (front+back+shoulder
# seams) extended to cloak length, with the front split down the middle and no
# armholes.
#
# SIDE SEAMS RUN HIP->SHOULDER ONLY. Below the hip both sides are open vents.
# This is the one deliberate departure from a coat and it is load-bearing:
# - Fully sewn sides would make a closed tube around the legs at knee length,
# which is precisely the piupiu defect (hem 0.5 m below the hip pivot, 30 deg
# of hip flexion sweeps the leg ~25 cm forward -- no silhouette clears that).
# - Open front + side vents let the leg-swing envelope escape through a gap
# instead of driving through cloth. Leg visible through a vent is CORRECT.
# - Sewn hip->shoulder still constrains the garment enough that
# SetPatternStrengthen is safe (tooling.md SS6: strengthening ROTATES an
# under-constrained garment -- a shoulder-only cape would be exactly that).
# These are whole-edge seams, not partial ones: the vent boundary is a real point
# inserted in the outline at hip height, which splits the side into two distinct
# LINES. Partial seams twist unpredictably; cutting features into the outline is
# how the tee's neck gap and the piupiu's 32 teeth were done.
#
# -- SEAM FLAG DERIVATION (do not "simplify" this) ----------------------------
# A closed polygon must traverse one way around, so a symmetric garment cut from
# one outline ALWAYS has its left edges running opposite to its right edges. That
# is why the flags differ per side, and it is not a copy-paste artefact:
#
# RIGHT pairs -> (False, False)
# back L_SIDE_R (hip->shoulder) vs frontR L_SIDE (hip->shoulder) same dir
# back L_SHLDR_R (outer->inner) vs frontR L_SHLDR(outer->inner) same dir
# LEFT pairs -> (True, False) <- exactly ONE reversal
# back L_SIDE_L (shoulder->hip) vs frontL L_SIDE (hip->shoulder) opposed
# back L_SHLDR_L (inner->outer) vs frontL L_SHLDR(outer->inner) opposed
#
# Reversing BOTH flags is equivalent to reversing NEITHER in terms of which
# endpoints meet ((T,T) == (F,F) for correspondence), so a pair whose traversals
# oppose needs exactly one flip. Getting this wrong pairs shoulder-to-hip and
# twists the panel -- and a twisted seam and surplus cloth look IDENTICAL once
# textured, which is why PLAIN=True comes first.
#
# The front-left panel is drafted as an explicit mirror with its point list
# ordered to match, rather than reusing the front-right list offset by dx.
# tooling.md SS3.3: mirrored coords with the list order kept makes the garment
# FALL OFF, because mirroring permutes the indices.
#
# -- v1a: THE CAPE MUST HANG FROM STRAPS, NOT BE A TUBE (fixed 2026-08-04) ----
# First run put W_TOP_B at 700 mm and the garment PUDDLED ON THE FLOOR -- probe
# measured span 0.198 m with 100% of verts below the knee. Cause is geometric, not
# physical: the shoulder seams sit at the back panel's top corners, so a 700 mm top
# edge places them 700 mm apart on a body whose shoulder_width is 319 mm. Both
# seams hung in mid-air ~190 mm outboard of the arms, the garment caught on
# nothing, and a tube wider than the 1095 mm hips has nothing to stop it.
#
# This is SKILL.md's "straps beat tubes / a strapless tube slides down to the
# narrowest catch" arriving from an unfamiliar direction -- it reads as a skirt
# rule, but it governs any garment whose support is a seam rather than a closure.
# So: W_TOP_B now spans the SHOULDERS (340 mm = 319 + ease) and the panel flares to
# a 950 mm hem below. The shoulder seams become 95 mm straps landing on the
# acromions -- the same strap width the pari settled on.
#
# It also matches the reference better than the wide version did: a narrow yoke
# flaring outward IS what the photo shows, including the deep V at the chest.
#
# SIDE-SEAM LENGTHS ARE NOW DERIVED, NOT CHOSEN. The front panel's outer edge is
# built with the SAME dx/dy as the back's side edge, so the paired edges are equal
# BY CONSTRUCTION. Choosing W_HEM_F independently made them 472.3 vs 469.8 -- a
# 2.5 mm inequality, and on the piupiu a dismissed 0.075 mm discrepancy turned out
# to be a slanted seam that notched the band and exposed the reverse face.
#
# MIRROR: do NOT re-order the mirrored point list. v1's "clever" reorder
# ([pts[0]] + pts[1:][::-1]) permuted the left panel's line indices, so index 3 was
# its VENT (552 mm) instead of its shoulder (95 mm) and the left shoulder seam was
# sewn to the wrong edge. The plain mirror x -> W_HEM_F - x already yields the
# intended traversal. The length fingerprint caught this before any render did.
#
# -- v1b: THE COLLAR IS STRUCTURAL, NOT DECORATION (fixed 2026-08-04) ---------
# With the straps corrected to 95 mm at shoulder width the seam fingerprints came
# out exact (95.0 x4, 472.347 x4) and it STILL puddled -- span 0.211 m. The pre-sim
# showed why: head through the neck slot, straps sitting at the right width, and
# nothing whatsoever holding them there. An open-front cape hanging from two narrow
# shoulder straps has no closed loop anywhere. Gravity pulls the back panel
# backwards and the fronts forwards, the straps rotate off the acromion, and the
# whole thing slides down.
#
# A real cloak does not hang from its shoulders. It hangs from a CLASP AT THE
# THROAT. v1 deferred the reference's stand-up collar as "decoration, do it in v2"
# and that was the actual error: the collar is the load-bearing element, and no
# amount of strap tuning substitutes for it.
#
# Fix: CLASP_H of each front panel's inner edge is sewn to its opposite number,
# closing the throat into a ring of (back neckline 230 mm + 2 x CLASP_H). That ring
# is far smaller than the shoulder girth, so the garment physically cannot slide
# down -- the same "cut it smaller than what is below it" trick the piupiu waistband
# uses against the hips, applied at the neck.
#
# The clasp is a whole-edge seam: a point inserted in the outline at
# PANEL_H - CLASP_H splits each inner edge into a CLASP line and a FRONT_OPEN line.
# The two front panels ARE true mirrors of each other, so this one pairs same-index
# with (False, False) -- the documented case, unlike the front-to-back seams.
#
# v2 can replace the clasp with the reference's actual collar band (a strip cut
# shorter than the neckline so it stands up and cinches). The clasp is the minimum
# that makes the physics correct.
#
# -- v1c: IT STARTED AROUND HER FOREHEAD (fixed 2026-08-04) -------------------
# The clasp did not stop the fall either, so the failure was traced rather than
# guessed at again. A stepped sim measuring z-span per step separated two
# hypotheses cleanly:
# H1 seams never created -> panels fall as independent sheets REFUTED
# all 5 AddSeamlinePairGroup calls returned True, GetSeamlinePairGroupCount
# == 5, and GetSeamlinePairGroupListInPattern showed back{0,1,2,3}
# front_r{0,1,4} front_l{2,3,4} -- exactly the intended wiring.
# H2 correctly sewn, nothing supports it -> slides as a unit CONFIRMED
# span held 1.000 -> 0.989 -> 0.978 while top fell
# 1.644 -> 1.589 -> 1.121 -> 0.598. An intact garment sliding off.
#
# And the trace handed over the actual cause: step_000 top = 1.644 m. The garment
# was being MATERIALISED AROUND HER FOREHEAD (head top 1.777), so the 470 mm throat
# ring began clamped around the skull, interpenetrating it. MD resolves a frame-0
# intersection by shoving the cloth out of the body -- downward here -- and once the
# stiffened assembly had that momentum it plowed straight past the shoulders. The
# ring never got a chance to seat.
#
# ALWAYS CAPTURE THE RETURN VALUES. v1a/v1b threw away every AddSeamlinePairGroup
# result, so "the seams took" stayed an assumption through two failed sims when one
# boolean would have settled it.
#
# ARR_Y was then swept with zero simulation frames -- step_000 is measurable
# straight after ResetClothArrangement, so the whole question cost seconds instead
# of one sim per guess. The control (ARR_Y=50) reproduced 1.644 exactly, so the
# harness was valid. Also learned: the arrangement POINT does not move the panel
# vertically at all (Body_Back_Center_2 registered identically to _1); only ARR_Y
# does. Neck_Collar stretched the span to 1.31 m -- do not use it for a panel.
#
# -- v1d: THE "RING" WAS PINCHED SHUT (fixed 2026-08-04) ----------------------
# Correcting the start height did not stop the fall either, because the throat ring
# I thought I had built did not exist. The clasp ran from the VERY TOP of each front
# panel's inner edge -- but that top point is already sewn to a back neck corner. So
# sewing front-R's top to front-L's top forced the back's two neck corners onto each
# other, collapsing the 230 mm back neckline into a pinched slit. There was no neck
# hole at all: the garment was a closed bag hanging beside the body, and a bag has
# nothing to hang ON.
#
# A cloak neckline needs THREE segments on that inner edge, not two:
# [NECKLINE_H free] the neck hole, wrapping from the back neck corner to the throat
# [CLASP_H sewn] the throat closure, at the collarbone
# [rest free] the open front
# Neck hole = back neckline 230 + 2 x NECKLINE_H = 430 mm around a 392 mm neck, and
# the torso immediately below it is 1083 mm, so it cannot pass.
#
# GENERAL LESSON worth more than this garment: when a seam endpoint is ALREADY
# seamed to something else, sewing it again constrains that other thing too. Two
# seams sharing an endpoint are one constraint on three panels. Check what a seam's
# ENDPOINTS are already attached to before adding it -- the length fingerprint
# cannot see this class of error, because every individual edge length was correct.
#
# -- v1e: THE ACTUAL CAUSE -- OffsetX (fixed 2026-08-04) ----------------------
# THIS is what made it fall, through five runs and four wrong hypotheses. Every
# arrangement point carries its OWN default ArrangementOffsetX/Y/Z, readable from
# GetArrangementList(), and for this garment they differ BY DESIGN:
# Body_Back_Center_1 X=0 Y=90 Z=50
# Body_Front_1_L / _R X=60 Y=90 Z=50
# Every run up to here forced all three to X=50. With the point defaults restored
# the cape HOLDS -- top 1.586 m against 0.20 m for the X=50 control, same geometry,
# same seams, same everything else.
#
# The piupiu had already written this lesson down -- "the x arg selects which half
# each panel wraps and the split is load-bearing" -- and its header even warns not to
# harmonise x across recipes. I harmonised it. The generalisation the repo was
# missing, now recorded: DO NOT INVENT ARRANGEMENT OFFSETS. Read the point's own
# defaults and override only what you specifically need (here, the height).
#
# Cost of getting there: 5 failed 300-frame sims. What would have caught it in one:
# dumping GetArrangementList()'s full entries BEFORE the first drape instead of
# projecting them down to {name: index}. The index was the only field being read and
# four other fields were carrying the answer.
#
# Also disproved along the way, so nobody re-tries them: seam wiring was NOT crossed
# (swapping arrangement points and swapping the seam pairing both still fell -- the X
# in the top-down view was a symptom, not the cause), and the avatar collides fine.
#
# -- LENGTH IS THE OPEN RISK --------------------------------------------------
# The reference is a product shot on an invisible mannequin, so it carries no
# absolute scale and capes read longer without a body in them. HEM_Z 0.42 m
# (just below Lena's 0.517 m knee) is a judgement call matched to the reference's
# drama, and it sits 0.53 m below the hip pivot -- the same envelope that beat the
# piupiu. The vents are the mitigation; G5 is the judge. If G5 reports real
# penetration, SHORTENING is one parameter here, not a re-author.
#
# QC targets (measured off the probe OBJ below, not eyeballed -- eyeballing is how
# garments shipped 8-14 cm off their own written targets):
# top edge 1.40 m +-3 cm (shoulder line, 1.3973)
# hem 0.42 m +-5 cm
#
# -- STATUS 2026-08-04: SHAPE HOLDS, DECORATION NOT STARTED -------------------
# Measured on the exported drape: top 1.508 m, hem 0.449 m (IN tolerance), span
# 1.060, hem circumference 2350 mm, 4315 verts (already game budget). Radius by
# height: chest 0.188, hip 0.213, knee 0.228 -- it flares outward instead of
# clinging, which is what the reference does. Exported to lena_cape_v1.{zprj,fbx,
# obj,zpac}; screenshots front + back in screenshots/cape_v1_*.png.
#
# top 1.508 is 10 cm above the 1.40 target and that is FINE, arguably better: the
# garment self-seats at the neck rather than the shoulder line, which reads as the
# reference's stand-up collar. Do not "correct" it downward without looking.
#
# OPEN DEFECT -- INVERTED LAPEL. The front view shows a GREY band across the upper
# chest, and grey from outside means inside out. The NECKLINE_H free cloth above the
# clasp has nowhere to go once the clasp pulls the panels together, so it folds
# outward and presents its reverse face. Options, in order of preference:
# 1. Build the reference's actual COLLAR BAND -- a strip sewn along the whole
# neckline, cut shorter than it so it stands up. Fixes the fold by giving the
# neckline a structural edge, and is needed for fidelity anyway.
# 2. Shrink NECKLINE_H so there is less loose cloth to fold.
# Do NOT reach for a seam-flag change: the clasp pairs same-index on TRUE mirrors,
# which is the documented (False, False) case, and the fold is slack, not a twist.
#
# STILL TO DO for reference fidelity (all of it is texture/trim, not tailoring):
# feather field, stand collar band, twin front bands with shell-button rows, shaggy
# hem (the piupiu's teeth-cut-into-the-outline technique transfers directly).
import os
import tempfile
# -- parameters -- this block exists so iteration is one edit -----------------
PANEL_H = 1000.0 # mm, hem (y=0) to shoulder line (y=PANEL_H). 2D y+ = UP in 3D;
# drafting y-down drapes the garment upside-down over the head.
W_TOP_B = 340.0 # mm, back panel at the shoulder line. MUST span the shoulders
# (shoulder_width 319 + ease), NOT the cloak's width -- the
# shoulder seams live at this edge's corners and are the only
# thing holding the garment up. 700 here puddled it on the floor.
W_HEM_B = 1250.0 # mm, back panel at the hem. ALL the volume lives here -- the top
# edge cannot grow (that is the strapless-tube failure), so drama
# has to come from flare. 950 read as a close-fitting A-line dress
# in the back view rather than the reference's voluminous cloak.
# Everything downstream of this is DERIVED, so the paired seam
# edges stay equal automatically when you change it.
NECK_W = 150.0 # mm, back neck scoop width (neck_circ 392 -> ~125 dia + ease)
NECK_D = 40.0 # mm, back neck scoop depth
FRONT_GAP = 60.0 # mm, the open front. Reference shows a narrow gap showing lining.
NECKLINE_H = 100.0 # mm of each front inner edge left FREE at the top. This is the
# neck hole, and it must exist -- see the v1d note. Neck hole
# perimeter = back neckline (NECK_D + NECK_W + NECK_D = 230)
# + 2*NECKLINE_H = 430 mm against a 392 mm neck: snug.
CLASP_H = 120.0 # mm sewn to the opposite front panel, BELOW the neckline, closing
# the throat at the collarbone. THIS IS WHAT HOLDS THE CAPE ON.
# Below the neck hole the torso is 1083 mm around, so a 430 mm
# hole physically cannot pass it -- the same "cut it smaller than
# what is below it" trick the piupiu waistband uses on the hips.
SHOULDER_Z = 1397.3 # mm, Lena's shoulder. Panel y=PANEL_H maps here.
HIP_Z = 945.5 # mm, hip pivot -- where the side seam STOPS and the vent starts.
Y_HIP = PANEL_H - (SHOULDER_Z - HIP_Z) # = 548.2 in panel space
# -- DERIVED, so paired seam edges are equal BY CONSTRUCTION ------------------
INSET_B = (W_HEM_B - W_TOP_B) / 2.0 # 305.0 -- back's flare per side
SLOPE = INSET_B / PANEL_H # dx/dy of the back's side edge
SIDE_RUN = SLOPE * (PANEL_H - Y_HIP) # horizontal run of the sewn side seam
W_TOP_F = (W_TOP_B - NECK_W) / 2.0 # 95.0 -- the shoulder STRAP width
W_HEM_F = W_TOP_F + INSET_B # 400.0 -- same slope over the full height
X_HIP_F = W_TOP_F + SIDE_RUN # front outer edge at the hip
PARTICLE_MM = 25.0 # This is a BIG garment: back 950x1000 + two 380x1000 fronts.
# 20 mm (the piupiu value) puts it at ~4.2k verts; 25 gives
# ~2.7k, inside a sane game budget with room for the collar.
COLLISION_MM = 10.0 # MD resolves the standoff, so it holds everywhere. Better than
# the downstream Blender normal-push, which self-intersects in
# concave regions.
ARR_BACK = "Body_Back_Center_1"
ARR_FRONT_R = "Body_Front_1_R"
ARR_FRONT_L = "Body_Front_1_L"
USE_POINT_DEFAULT_X = True # DO NOT SET THIS FALSE. See the v1e note: each
# arrangement point carries its OWN default OffsetX, and they
# differ BY DESIGN -- Body_Back_Center_1 is 0, Body_Front_1_L/R
# are 60. Forcing all three to a single value is what made the
# cape fall on the floor five runs in a row.
ARR_Y = 30 # Height. Swept with a valid control; only Y moves the panel
# vertically (the arrangement POINT does not -- Body_Back_Center_2
# registers identically to _1). With the correct X, Y trades off
# against bunching rather than moving rigidly:
# Y=90 -> top 1.631 hem 0.827 span 0.804 (bunched at the neck)
# Y=50 -> top 1.586 hem 0.618 span 0.969
# The TOP self-seats at the neck/shoulders either way; lower Y
# just bunches less, so the hem drops. 30 for a longer line.
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
# fabric_api has NO physics setters -- stiffness comes from the .zfab. A feather
# cloak wants body, so if this reads limp climb the ladder one step at a time:
# V2_Woven_Canvas_1.zfab < V2_Woven_Denim_1.zfab < V2_Non-Fabric_Tyvek_1.zfab
PLAIN = True # SHAPE FIRST. See the header.
EXPORT = True # Guarded until the drape held and was measured (2026-08-04).
SIM_MAIN = 250
SIM_RELAX = 50
VERSION = "v1"
AVATAR_FBX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
OUT = r"C:\Users\Jeremy\tinqs\animation\tools\tailor"
SHOTS = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\screenshots"
report = {"params": {"PANEL_H": PANEL_H, "W_TOP_B": W_TOP_B, "W_HEM_B": W_HEM_B,
"W_TOP_F": W_TOP_F, "W_HEM_F": W_HEM_F, "NECK_W": NECK_W,
"Y_HIP": round(Y_HIP, 1), "PARTICLE_MM": PARTICLE_MM,
"COLLISION_MM": COLLISION_MM, "PLAIN": PLAIN,
"EXPORT": EXPORT, "zfab": os.path.basename(ZFAB)}}
utility_api.NewProject() # NB: deletes the avatar -- import after
op = ApiTypes.ImportExportOption()
op.scale = 10.0 # Blender-exported FBX avatars import 10x small
op.bAddArrangementPoints = True # else there is nowhere to hang cloth
op.bAutoTranslate = True
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
# -- outlines ----------------------------------------------------------------
# Named landmarks, indices DERIVED. Hardcoded line indices broke this repo's
# bodice recipe twice; inserting a point shifts every index after it.
CX_B = W_HEM_B / 2.0 # 475.0
x_side_b_r = W_HEM_B - SLOPE * Y_HIP # right side edge x at hip
x_side_b_l = SLOPE * Y_HIP # left side edge x at hip
def back_outline(dx):
"""Back panel, counter-clockwise from the hem-left corner.
Points, in order: hem, right side (via the HIP VENT POINT), right shoulder,
neck scoop, left shoulder, left side (via the hip vent point).
"""
pts = [
(0.0, 0.0), # 0 hem-left
(W_HEM_B, 0.0), # 1 hem-right
(x_side_b_r, Y_HIP), # 2 right @ hip <- vent boundary
(INSET_B + W_TOP_B, PANEL_H), # 3 top-right (shoulder outer)
(CX_B + NECK_W / 2.0, PANEL_H), # 4 neck right
(CX_B + NECK_W / 2.0, PANEL_H - NECK_D), # 5 scoop
(CX_B - NECK_W / 2.0, PANEL_H - NECK_D), # 6 scoop
(CX_B - NECK_W / 2.0, PANEL_H), # 7 neck left
(INSET_B, PANEL_H), # 8 top-left (shoulder outer)
(x_side_b_l, Y_HIP), # 9 left @ hip <- vent boundary
]
return [(x + dx, y, 0) for (x, y) in pts]
def front_outline(dx, mirror):
"""One front panel. Inner edge (the open front) is vertical at local x=0.
mirror=False -> body's RIGHT: outer edge on the high-x side.
mirror=True -> body's LEFT : the plain mirror x -> W_HEM_F - x. Do NOT
re-order the list afterwards. v1 did, and the reorder permuted
the line indices so index 3 was the 552 mm VENT instead of the
95 mm shoulder strap -- the left shoulder seam was then sewn to
the wrong edge. The plain mirror already yields the intended
traversal (hem, vent, side hip->shoulder, shoulder outer->inner,
front opening), just with outer now on the LOW-x side.
"""
pts = [
(0.0, 0.0), # inner hem
(W_HEM_F, 0.0), # outer hem
(X_HIP_F, Y_HIP), # outer @ hip <- vent boundary, derived
(W_TOP_F, PANEL_H), # outer top (shoulder strap outer)
(0.0, PANEL_H), # inner top -- meets the BACK's neck corner
(0.0, PANEL_H - NECKLINE_H), # neckline bottom / clasp top
(0.0, PANEL_H - NECKLINE_H - CLASP_H), # clasp bottom <- throat closure
]
if mirror:
pts = [(W_HEM_F - x, y) for (x, y) in pts]
return [(x + dx, y, 0) for (x, y) in pts]
pts_b = back_outline(600.0)
pts_fr = front_outline(0.0, mirror=False)
pts_fl = front_outline(1700.0, mirror=True)
pb = pattern_api.CreatePatternWithPoints(pts_b)
pfr = pattern_api.CreatePatternWithPoints(pts_fr)
pfl = pattern_api.CreatePatternWithPoints(pts_fl)
report["ids"] = {"back": pb, "front_r": pfr, "front_l": pfl}
# Line i runs pts[i] -> pts[i+1]; the last line closes back to pts[0].
B = {"hem": 0, "vent_r": 1, "side_r": 2, "shldr_r": 3,
"neck_a": 4, "neck_b": 5, "neck_c": 6, "shldr_l": 7, "side_l": 8, "vent_l": 9}
F = {"hem": 0, "vent": 1, "side": 2, "shldr": 3,
"neckline": 4, "clasp": 5, "front_open": 6}
report["lines"] = {"back_n_pts": len(pts_b), "front_n_pts": len(pts_fr),
"back": B, "front": F}
# Fingerprint by LENGTH before sewing. Paired edges must be EQUAL, not close --
# a dismissed 0.075 mm discrepancy on the piupiu turned out to be a slanted seam
# that notched the waistband and exposed the reverse face.
try:
# Both panels' side edges share SIDE_RUN and (PANEL_H - Y_HIP), so this single
# expected length applies to all four -- that is the point of deriving them.
side_len = (SIDE_RUN ** 2 + (PANEL_H - Y_HIP) ** 2) ** 0.5
report["derived_mm"] = {"INSET_B": round(INSET_B, 2), "SLOPE": round(SLOPE, 4),
"SIDE_RUN": round(SIDE_RUN, 2), "W_TOP_F": W_TOP_F,
"W_HEM_F": W_HEM_F, "X_HIP_F": round(X_HIP_F, 2),
"hem_circ": round(W_HEM_B + 2 * W_HEM_F, 1)}
report["expect_mm"] = {"shoulder": W_TOP_F, "side_all_four": round(side_len, 2)}
report["measured_mm"] = {
"back_shldr_r": pattern_api.GetLineLength(pb, B["shldr_r"]),
"back_shldr_l": pattern_api.GetLineLength(pb, B["shldr_l"]),
"front_r_shldr": pattern_api.GetLineLength(pfr, F["shldr"]),
"front_l_shldr": pattern_api.GetLineLength(pfl, F["shldr"]),
"back_side_r": pattern_api.GetLineLength(pb, B["side_r"]),
"back_side_l": pattern_api.GetLineLength(pb, B["side_l"]),
"front_r_side": pattern_api.GetLineLength(pfr, F["side"]),
"front_l_side": pattern_api.GetLineLength(pfl, F["side"]),
"front_r_clasp": pattern_api.GetLineLength(pfr, F["clasp"]),
"front_l_clasp": pattern_api.GetLineLength(pfl, F["clasp"]),
"front_r_neckline": pattern_api.GetLineLength(pfr, F["neckline"]),
"front_l_neckline": pattern_api.GetLineLength(pfl, F["neckline"]),
}
report["neck_hole_mm"] = 2 * NECK_D + NECK_W + 2 * NECKLINE_H # vs neck_circ 392
except Exception as e:
report["line_length_error"] = "%s: %s" % (type(e).__name__, e)
# -- seams -- flags derived in the header, ONE reversal on the left ----------
# CAPTURE THE RETURN VALUES. v1a/v1b discarded them and "the seams took" stayed an
# assumption through two failed sims when one boolean would have settled it.
report["seams"] = {
"shldr_r": pattern_api.AddSeamlinePairGroup(pb, B["shldr_r"], pfr, F["shldr"], False, False),
"side_r": pattern_api.AddSeamlinePairGroup(pb, B["side_r"], pfr, F["side"], False, False),
"shldr_l": pattern_api.AddSeamlinePairGroup(pb, B["shldr_l"], pfl, F["shldr"], True, False),
"side_l": pattern_api.AddSeamlinePairGroup(pb, B["side_l"], pfl, F["side"], True, False),
# The throat clasp -- the seam that actually holds the cape on. The two front
# panels are TRUE mirrors of each other, so this is the documented same-index
# (False, False) case, and both clasp lines run top -> down. It sits BELOW the
# free neckline: clasping at the top pinches the neck hole shut (v1d).
"clasp": pattern_api.AddSeamlinePairGroup(pfr, F["clasp"], pfl, F["clasp"], False, False),
}
try:
report["seam_count"] = pattern_api.GetSeamlinePairGroupCount()
except Exception as e:
report["seam_count_err"] = "%s: %s" % (type(e).__name__, e)
fab = fabric_api.AddFabric(ZFAB) # index 0 is the SHARED default -- colouring
# it dyes every garment in the scene
for p in (pb, pfr, pfl):
pattern_api.SetPatternPieceFabricIndex(p, fab)
for setter, val, key in ((pattern_api.SetParticleDistanceOfPattern, PARTICLE_MM, "particle"),
(pattern_api.SetAddlThicknessCollision, COLLISION_MM, "collision")):
if not val:
continue
try:
for p in (pb, pfr, pfl):
setter(p, val)
report[key] = val
except Exception as e:
report[key + "_error"] = "%s: %s" % (type(e).__name__, e)
# Arrangement indices REGENERATE per avatar import -- always look up by name. Keep
# the WHOLE entry, not just the index: the per-point default offsets are the payload.
arr = {str(a["ArrangementName"]): a for a in pattern_api.GetArrangementList()}
missing = [n for n in (ARR_BACK, ARR_FRONT_R, ARR_FRONT_L) if n not in arr]
report["arr_missing"] = missing
if not missing:
used = {}
for p, nm in ((pb, ARR_BACK), (pfr, ARR_FRONT_R), (pfl, ARR_FRONT_L)):
a = arr[nm]
pattern_api.SetArrangement(p, int(a["ArrangementIndex"]))
# Take the point's own X and Z; override only the height. SetArrangementPosition
# takes 4 INTS -- a float raises TypeError.
ox = int(a["ArrangementOffsetX"]) if USE_POINT_DEFAULT_X else 50
oz = int(a["ArrangementOffsetZ"])
pattern_api.SetArrangementPosition(p, ox, int(ARR_Y), oz)
used[nm] = {"x": ox, "y": int(ARR_Y), "z": oz,
"orientation": str(a["ArrangementOrientation"]),
"type": str(a["ArrangementType"])}
report["arrangement_used"] = used
for p in (pb, pfr, pfl):
pattern_api.SetPatternStrengthen(p, True) # stiff so cloth wraps, not crumples
utility_api.ResetClothArrangement() # SetArrangement only ASSIGNS
# THE DIAGNOSTIC THAT UNSTICKS EVERYTHING: zero simulation frames, right after the
# arrangement is applied. Shows where the panels actually start before physics
# muddies it. Seven "seam bug" iterations on the tee were really an upside-down
# garment; one pre-sim snapshot would have caught it immediately.
utility_api.Refresh3DWindow()
utility_api.SetCamViewPoint(2) # front -- always set, so shots compare
pre = os.path.join(SHOTS, "cape_%s_presim_front.png" % VERSION)
export_api.ExportSnapshot3D(pre)
report["presim_front"] = pre
utility_api.SetCamViewPoint(5) # top-down: the ONLY view that shows
utility_api.Refresh3DWindow() # whether front and back meet over
pret = os.path.join(SHOTS, "cape_%s_presim_top.png" % VERSION)
export_api.ExportSnapshot3D(pret) # the shoulder
report["presim_top"] = pret
report["sim1"] = utility_api.Simulate(SIM_MAIN) # main settle, still stiff
for p in (pb, pfr, pfl):
pattern_api.SetPatternStrengthen(p, False) # relax into natural folds
report["sim2"] = utility_api.Simulate(SIM_RELAX)
utility_api.Refresh3DWindow()
utility_api.SetCamViewPoint(2)
snap = os.path.join(SHOTS, "cape_%s_front.png" % VERSION)
export_api.ExportSnapshot3D(snap)
report["front"] = snap
utility_api.SetCamViewPoint(4)
utility_api.Refresh3DWindow()
side = os.path.join(SHOTS, "cape_%s_side.png" % VERSION)
export_api.ExportSnapshot3D(side)
report["side"] = side
utility_api.SetCamViewPoint(2)
# THE BACK. ExportSnapshot3D cannot show it at any viewpoint -- this is the only
# way, and for a cape it is the primary view. Turntable REUSES output*.png names,
# so the return value is the only handle on the files: copy them out immediately.
report["turntable"] = export_api.ExportTurntableImages(4) # 0 front 1 side 2 BACK 3 side
# -- measurement -- numbers, not eyeballs ------------------------------------
mop = ApiTypes.ImportExportOption()
mop.bExportGarment = True
mop.bExportAvatar = False
probe = os.path.join(tempfile.gettempdir(), "tinqs_md_cape_%s_probe.obj" % VERSION)
export_api.ExportOBJ(probe, mop)
vs = []
with open(probe) as fh:
for line in fh:
if line.startswith("v "):
_, x, y, z = line.split()[:4]
vs.append((float(x), float(y), float(z)))
if vs:
S = 0.001 # ExportOBJ writes MM. The FBX->census path reads DECIMETRES
# (that is what align.scale_z 0.1 is for) -- they disagree.
zs = sorted(v[1] * S for v in vs)
cx = sum(v[0] for v in vs) / len(vs)
cz = sum(v[2] for v in vs) / len(vs)
def band(lo, hi):
r = [(((v[0] - cx) ** 2 + (v[2] - cz) ** 2) ** 0.5) * S
for v in vs if lo <= v[1] * S <= hi]
if not r:
return None
r.sort()
return {"n": len(r), "r_med": round(r[len(r) // 2], 3), "r_max": round(r[-1], 3)}
report["measured"] = {
"verts": len(vs),
"top_m": round(zs[-1], 3), # target 1.40 +-0.03 (shoulder line)
"hem_m": round(zs[0], 3), # target 0.42 +-0.05
"span_m": round(zs[-1] - zs[0], 3),
"radius_at_chest_1.22_1.30": band(1.22, 1.30),
"radius_at_hip_0.90_0.96": band(0.90, 0.96),
"radius_at_knee_0.50_0.56": band(0.50, 0.56),
"frac_below_knee_0517": round(sum(1 for z in zs if z < 0.517) / len(zs), 3),
}
if EXPORT:
base = os.path.join(OUT, "lena_cape_%s" % VERSION)
eop = ApiTypes.ImportExportOption()
eop.bExportGarment = True
eop.bExportAvatar = False
report["zprj"] = export_api.ExportZPrj(base + ".zprj")
report["fbx"] = export_api.ExportFBX(base + "_garment.fbx", eop)
report["obj"] = export_api.ExportOBJ(base + "_garment.obj", eop)
report["zpac"] = export_api.ExportZPac(base + ".zpac")
result = report