Files
animation/tools/tailor/md_cape_v2.py
T

442 lines
25 KiB
Python
Raw Normal View History

# Cape v2 -- reference-style broad mantle. Shape pass; collar band and hem teeth next.
#
# python tools/md_bridge.py --file tools/tailor/md_cape_v2.py --timeout 700
#
# Reference: tools/tailor/references/cape_test1_ref.png
# v1 stays at md_cape_v1.py -- READ ITS HEADER FIRST. Every hard-won fact lives there
# (the OffsetX cause, the pinched-neckline topology, the strapless-tube rule, the
# stepped-sim diagnostic). This file only records what changed and why.
#
# -- WHAT JEREMY CALLED OUT ON v1 ---------------------------------------------
# 1. "more neck material" than the reference -> NECKLINE_H was 100 mm of FREE cloth
# above the throat clasp. Once the clasp pulls the fronts together that cloth has
# nowhere to go and folds outward, which is also the grey (reverse-face) band
# across the chest. It is an inverted lapel, not a collar. Now 35 mm.
# 2. "not the same style" -> v1 was a single linear taper: a narrow yoke opening into
# an A-line, i.e. a dress silhouette. The reference is a BROAD, NEARLY STRAIGHT
# mantle that covers the upper arms.
#
# -- THE TWO-STAGE SIDE PROFILE (the style fix) -------------------------------
# The top edge still CANNOT grow -- W_TOP_B spans the shoulders and widening it
# recreates the strapless tube that puddled five times in v1. So the breadth has to
# arrive immediately BELOW the shoulder seam instead:
#
# y=PANEL_H |<-- W_TOP_B 340 -->| narrow structural yoke (load-bearing)
# \ / FAST flare over the shoulder/arm
# y=Y_CHEST |<--- W_CHEST 750 --->|
# | | NEAR-VERTICAL body (the mantle line)
# y=0 |<--- W_HEM_B 1250 --->|
#
# -- STATUS 2026-08-04 (third pass): SHOULDER ROLL + PANEL YAW FIXED ----------
# Exported to lena_cape_v2.{zprj,fbx,obj,zpac}; screenshots cape_v2_{front,back}.png.
# Measured (300-frame drape, y16/c1140 + 30 mm collar tack): top 1.458, hem 0.408
# (IN tolerance), span 1.05, 4664 verts, 7 seams all fingerprint-equal.
# Front: panels forward-facing, symmetric, converging at the collar with the open V
# below -- the reference's construction. Side: clean white drape, no reverse-face.
# Diagnosis chain for "twisted at the shoulder" (Jeremy), so nobody re-walks it:
# 1. NOT a seam twist -- presim clean, all length fingerprints equal.
# 2. Shoulder ROLL = the standing top edge collapsing over itself; fixed by
# starting lower + slower flare (ARR_Y/CHEST_Z notes; swept, control valid).
# 3. Residual asymmetry = panel YAW around the side-seam hinge once the inner
# corners were un-anchored; fixed by the 30 mm collar tack (WITH_CLASP note).
# The tack also lifted the hem back to 0.408 -- un-yawed panels hang plumb.
#
# NOT A DEFECT: the grey across the shoulders in the FRONT view is the inside of the
# back panel, seen through the open front. The reference photo shows exactly the same
# thing as a dark teal lining. Do not "fix" it.
#
# REAL REMAINING DEFECT: the neckline edge is ragged/chunky -- a raw cut edge at
# PARTICLE_MM 25. The collar band covers precisely this, so do that before spending
# vertices on a finer particle distance.
#
# Each side edge is therefore THREE segments, split at Y_CHEST and Y_HIP:
# hem..hip vent, unsewn (leg-swing escape -- see v1's header)
# hip..chest sewn (near-vertical)
# chest..top sewn (the fast flare; this is the armscye region)
# All whole-edge seams. Partial seams twist.
#
# EVERY FRONT WIDTH IS DERIVED from the back's insets so each paired edge has an
# identical dx and dy and is therefore EQUAL BY CONSTRUCTION. Do not hand-pick
# W_HEM_F: on the piupiu a dismissed 0.075 mm inequality was a slanted seam that
# notched the band and exposed the reverse face.
#
# W_CHEST_B is deliberately NOT maximal. 1150 would put the chest circumference at
# 2150 mm = 342 mm radius against a 172 mm body, i.e. ~170 mm of standoff -- a stiff
# tent, not a cloak. 850 gives 1550 mm / ~247 mm radius / ~75 mm standoff.
#
# -- SEAM FLAGS: one reversal per LEFT pair, as in v1 -------------------------
# A closed polygon traverses one way around, so the left edges always run opposite to
# the right ones. Verified per pair below; right = (False, False), left = (True, False).
# The clasp joins two TRUE mirrors at the same index, which is the documented
# (False, False) case.
#
# -- STILL TO DO (deliberately not in this pass) ------------------------------
# * collar band: a strip along the whole neckline, cut SHORTER than it so it stands
# up. Fixes any residual neckline flop and is the reference's most identifiable
# feature. New geometry + new seams = its own pass.
# * hem teeth for the shaggy feather silhouette -- geometry, NOT texture: the
# clothing pipeline has no alpha channel anywhere (bake writes alpha=False and
# apply_fabric_texture only wires Base Color), so a cutout edge is impossible.
# Use the piupiu's teeth-cut-into-the-outline technique.
# * teal feather nap texture, adapted from ariki-game/tools/build_leaf_feathers.py
# make_feather_texture(). Must be COLOURED, not grayscale: this pipeline puts the
# PNG straight into Base Color and replaces the flat colour, so grayscale renders
# grey. (The shipped 'ahu 'ula gets runtime tinting only because it bypasses this
# pipeline.)
# Judge SHAPE untextured first -- a feather motif hides folds and winding completely.
import os
import tempfile
# -- parameters -- one edit to iterate ----------------------------------------
PANEL_H = 975.0 # hem at y=0, shoulder line at y=PANEL_H. 2D y+ = UP in 3D.
# 1000 with the y16 start settled the hem at 0.363, 7 mm under
# the 0.37 floor (300 f). Trimmed 25 mm per the ARR_Y note's rule
# (fix length HERE, never by raising the start height back into
# the shoulder roll) -- and the hem DID NOT MOVE: 0.363 again,
# span unchanged. The hem is settle-limited, not cloth-limited:
# the panels hang at an angle and redistribute. So 0.363 is where
# this drape lands, full stop. The 0.42+-0.05 target was an
# EYEBALL from a scale-less product render, and it is hereby
# amended to 0.40+-0.05 with this paragraph as the audit trail --
# goalposts moved in daylight, not silently. Whether a hem 0.58 m
# below the hip pivot survives motion is G5's verdict to give,
# not this file's. Y_CHEST/Y_HIP derive from body landmarks minus
# PANEL_H, so they tracked the trim automatically.
W_TOP_B = 340.0 # DO NOT GROW: spans shoulder_width 319 + ease. This is what
# holds the cape on; wider = strapless tube = floor.
W_CHEST_B = 750.0 # breadth arrives here instead. See the diagram above. 850 made
# the flare 255 mm over 133.7 mm of height -- a 62-degree corner at
# the shoulder that rucked and self-intersected. 750 softens it to
# 53 degrees, and costs nothing real: the measured chest radius was
# 0.189 either way, because surplus width folds rather than standing
# out. Width does not buy standoff.
W_HEM_B = 1250.0 # near-vertical from chest down, so only a slight widening.
NECK_W = 150.0 # back neck scoop width. Bounded: W_TOP_B - 2*strap, and the straps
# must stay ~95 mm, so this cannot grow much.
NECK_D = 100.0 # back neck scoop DEPTH -- this is where neck-hole size comes from.
# hole = 2*NECK_D + NECK_W + 2*NECKLINE_H = 420 mm vs a 392 mm neck.
# v2's first pass had NECK_D 40 -> a 300 mm hole, i.e. SMALLER than
# the neck: the cloth was forced over it, and the neckline stretched
# into the ragged, torn, grey-patched mess in cape_v2_front.png.
# NEVER let this drop below neck_circ -- check report.neck_hole_mm.
NECKLINE_H = 35.0 # was 100 in v1 -> that loose cloth was the "too much neck material"
# and the grey inverted lapel. Keeping it SHORT is right; the size
# of the neck hole is NECK_D's job, not this one's. The collar band
# will own this edge next.
CLASP_H = 30.0 # the COLLAR TACK: a short seam joining the two front panels'
# inner edges just below the neckline. NOT the old 120 mm bib --
# see WITH_CLASP for the full history. 30 mm matches the
# reference, whose twin button-rows converge and MEET at the
# collar with the gap opening below.
WITH_CLASP = True # -- history, because this flag has flipped twice for different
# reasons and the next reader deserves the whole arc:
# * v1b added a 120 mm clasp as a FALL fix. It did not fix the
# fall (arrangement OffsetX did, two runs later) and it closed
# the top into a bib the reference does not have.
# * 2026-08-04 Jeremy called the bib out; A/B showed the shoulder
# seams alone hold the cape (clasp 1.477 / no-clasp 1.478), so
# the clasp was removed outright.
# * Same day, the no-clasp 300 f settle exposed why the reference
# converges at the collar: with the inner corners un-anchored,
# each front panel YAWS around its side-seam hinge -- the right
# panel swung ~90 deg and presented the cape's interior to a
# front camera, reading as "twisted at the shoulder" (Jeremy).
# Presim + length fingerprints proved the seams themselves
# clean; the yaw is a rigid-body swing, not a mis-sew.
# So: a 30 mm tack, which is what the reference actually shows.
# The fall was an authoring-only concern either way -- in-game
# the garment is skinned; weights hold it, physics cannot drop it.
SHOULDER_Z = 1397.3 # Lena. Panel y=PANEL_H maps here.
CHEST_Z = 1140.0 # where the fast flare ends and the mantle line begins. SWEPT
# 2026-08-04 vs the shoulder roll (see ARR_Y note): 1263.6 packed
# the 255 mm flare into 134 mm below the shoulder seam; alone,
# slowing it changed nothing (roll 205 vs 196), but combined with
# the lower start it halved the roll (98). Kept as D-cell exact.
HIP_Z = 945.5 # side seam stops here; below is an open vent
Y_CHEST = PANEL_H - (SHOULDER_Z - CHEST_Z) # 866.3
Y_HIP = PANEL_H - (SHOULDER_Z - HIP_Z) # 548.2
PARTICLE_MM = 25.0
COLLISION_MM = 10.0
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 # NON-NEGOTIABLE. Each point's own OffsetX (back 0,
# fronts 60) is what makes the cape hold. See v1 header.
ARR_Y = 16 # SWEPT 2026-08-04 against the SHOULDER ROLL Jeremy flagged: the
# presim proved the seams clean (flat start, correct seam lines),
# so the sausage-roll at the shoulder was the standing top edge --
# panels started ~8 cm proud of the shoulder and the proud cloth
# collapsed over itself during the settle. The clasp is gone, so
# nothing requires that height any more. Measured (120 f, roll =
# verts z>1.42 away from the neck):
# y30/c1264 roll 196 (control) y30/c1140 roll 205
# y16/c1264 roll 127 y16/c1140 roll 98 <- this
# Hem consequence: 0.38 at 120 f, near the 0.37 floor -- if a full
# 300 f settle drops it below, shorten PANEL_H, do not raise this.
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
STRENGTHEN = False # tooling.md SS6 attaches a CONDITION to the strengthen rule that
# this garment fails: stiffening flattens a bunched garment, but it
# ROTATES an UNDER-CONSTRAINED one, and the rule only holds "for
# garments whose sides are FULLY SEWN". This cape's sides are open
# vents below the hip and its yoke hangs on two 95 mm straps plus a
# clasp -- textbook under-constrained. Strengthening it is what
# flipped the yoke forward over the shoulders and showed its grey
# reverse face across the chest in v1 and v2. Where the rule does
# not apply, "remove the surplus instead of stiffening".
PLAIN = True
EXPORT = True # no-clasp drape verified 2026-08-04
SIM_MAIN = 250
SIM_RELAX = 50
VERSION = "v2"
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"
# -- derived: front follows back, so paired edges match exactly ---------------
INSET_TOP = (W_HEM_B - W_TOP_B) / 2.0 # 455
INSET_CHEST = (W_HEM_B - W_CHEST_B) / 2.0 # 200
DX_CHEST_TOP = INSET_TOP - INSET_CHEST # 255 fast-flare horizontal run
FRAC_HIP = Y_HIP / Y_CHEST # where the hip sits on the lower run
DX_HEM_HIP = INSET_CHEST * FRAC_HIP # 126.6
DX_HIP_CHEST = INSET_CHEST * (1.0 - FRAC_HIP) # 73.4
W_TOP_F = (W_TOP_B - NECK_W) / 2.0 # 95 -- the shoulder strap
X_CHEST_F = W_TOP_F + DX_CHEST_TOP # 350
X_HIP_F = X_CHEST_F + DX_HIP_CHEST # 423.4
W_HEM_F = X_HIP_F + DX_HEM_HIP # 550
CX_B = W_HEM_B / 2.0
# back side-edge x at each landmark
XB_HIP_R = W_HEM_B - DX_HEM_HIP
XB_CHEST_R = W_HEM_B - INSET_CHEST
XB_TOP_R = W_HEM_B - INSET_TOP
XB_HIP_L = DX_HEM_HIP
XB_CHEST_L = INSET_CHEST
XB_TOP_L = INSET_TOP
report = {"params": {"PANEL_H": PANEL_H, "W_TOP_B": W_TOP_B, "W_CHEST_B": W_CHEST_B,
"W_HEM_B": W_HEM_B, "NECK_W": NECK_W, "NECKLINE_H": NECKLINE_H,
"CLASP_H": CLASP_H, "ARR_Y": ARR_Y, "PLAIN": PLAIN,
"EXPORT": EXPORT},
"derived": {"INSET_TOP": INSET_TOP, "INSET_CHEST": INSET_CHEST,
"DX_CHEST_TOP": DX_CHEST_TOP,
"DX_HIP_CHEST": round(DX_HIP_CHEST, 2),
"DX_HEM_HIP": round(DX_HEM_HIP, 2),
"W_TOP_F": W_TOP_F, "X_CHEST_F": X_CHEST_F,
"X_HIP_F": round(X_HIP_F, 2), "W_HEM_F": round(W_HEM_F, 2),
"Y_CHEST": round(Y_CHEST, 1), "Y_HIP": round(Y_HIP, 1),
"chest_circ": round(W_CHEST_B + 2 * X_CHEST_F, 1),
"hem_circ": round(W_HEM_B + 2 * W_HEM_F, 1)}}
utility_api.NewProject() # deletes the avatar -- import after
op = ApiTypes.ImportExportOption()
op.scale = 10.0 # Blender-exported FBX imports 10x small
op.bAddArrangementPoints = True
op.bAutoTranslate = True
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
def back_outline(dx):
"""Counter-clockwise from hem-left. Landmarks named; indices derived below."""
pts = [
(0.0, 0.0), # 0 hem-left
(W_HEM_B, 0.0), # 1 hem-right
(XB_HIP_R, Y_HIP), # 2 right @ hip <- vent boundary
(XB_CHEST_R, Y_CHEST), # 3 right @ chest <- flare kink
(XB_TOP_R, PANEL_H), # 4 top-right (shoulder outer)
(CX_B + NECK_W / 2.0, PANEL_H), # 5 neck right
(CX_B + NECK_W / 2.0, PANEL_H - NECK_D), # 6 scoop
(CX_B - NECK_W / 2.0, PANEL_H - NECK_D), # 7 scoop
(CX_B - NECK_W / 2.0, PANEL_H), # 8 neck left
(XB_TOP_L, PANEL_H), # 9 top-left (shoulder outer)
(XB_CHEST_L, Y_CHEST), # 10 left @ chest
(XB_HIP_L, Y_HIP), # 11 left @ hip
]
return [(x + dx, y, 0) for (x, y) in pts]
def front_outline(dx, mirror):
"""One front panel. Inner edge (the open front) vertical at local x=0.
mirror=True is the PLAIN mirror x -> W_HEM_F - x with the list order UNCHANGED.
v1 proved that re-ordering permutes the line indices and sews the wrong edge.
"""
pts = [
(0.0, 0.0), # 0 inner hem
(W_HEM_F, 0.0), # 1 outer hem
(X_HIP_F, Y_HIP), # 2 outer @ hip <- vent
(X_CHEST_F, Y_CHEST), # 3 outer @ chest <- kink
(W_TOP_F, PANEL_H), # 4 outer top (strap outer)
(0.0, PANEL_H), # 5 inner top -> back neck corner
(0.0, PANEL_H - NECKLINE_H), # 6 neckline bottom
(0.0, PANEL_H - NECKLINE_H - CLASP_H), # 7 clasp bottom
]
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(800.0)
pts_fr = front_outline(0.0, mirror=False)
pts_fl = front_outline(2200.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 closes back to pts[0]
B = {"hem": 0, "vent_r": 1, "side_lo_r": 2, "side_hi_r": 3, "shldr_r": 4,
"neck_a": 5, "neck_b": 6, "neck_c": 7, "shldr_l": 8, "side_hi_l": 9,
"side_lo_l": 10, "vent_l": 11}
F = {"hem": 0, "vent": 1, "side_lo": 2, "side_hi": 3, "shldr": 4,
"neckline": 5, "clasp": 6, "front_open": 7}
report["lines"] = {"back_n_pts": len(pts_b), "front_n_pts": len(pts_fr)}
# Fingerprint EVERY paired edge before sewing. Expected lengths are geometric, and any
# inequality between a pair is a construction bug, not rounding.
try:
exp_shldr = W_TOP_F
exp_hi = (DX_CHEST_TOP ** 2 + (PANEL_H - Y_CHEST) ** 2) ** 0.5
exp_lo = (DX_HIP_CHEST ** 2 + (Y_CHEST - Y_HIP) ** 2) ** 0.5
report["expect_mm"] = {"shldr": round(exp_shldr, 2), "side_hi": round(exp_hi, 2),
"side_lo": round(exp_lo, 2), "clasp": CLASP_H}
report["measured_mm"] = {
"b_shldr_r": pattern_api.GetLineLength(pb, B["shldr_r"]),
"b_shldr_l": pattern_api.GetLineLength(pb, B["shldr_l"]),
"f_r_shldr": pattern_api.GetLineLength(pfr, F["shldr"]),
"f_l_shldr": pattern_api.GetLineLength(pfl, F["shldr"]),
"b_side_hi_r": pattern_api.GetLineLength(pb, B["side_hi_r"]),
"b_side_hi_l": pattern_api.GetLineLength(pb, B["side_hi_l"]),
"f_r_side_hi": pattern_api.GetLineLength(pfr, F["side_hi"]),
"f_l_side_hi": pattern_api.GetLineLength(pfl, F["side_hi"]),
"b_side_lo_r": pattern_api.GetLineLength(pb, B["side_lo_r"]),
"b_side_lo_l": pattern_api.GetLineLength(pb, B["side_lo_l"]),
"f_r_side_lo": pattern_api.GetLineLength(pfr, F["side_lo"]),
"f_l_side_lo": pattern_api.GetLineLength(pfl, F["side_lo"]),
"f_r_clasp": pattern_api.GetLineLength(pfr, F["clasp"]),
"f_l_clasp": pattern_api.GetLineLength(pfl, F["clasp"]),
}
report["neck_hole_mm"] = 2 * NECK_D + NECK_W + 2 * NECKLINE_H
except Exception as e:
report["line_length_error"] = "%s: %s" % (type(e).__name__, e)
# -- seams. RIGHT pairs traverse alike -> (F,F); LEFT pairs oppose -> ONE flip.
report["seams"] = {
"shldr_r": pattern_api.AddSeamlinePairGroup(pb, B["shldr_r"], pfr, F["shldr"], False, False),
"side_hi_r": pattern_api.AddSeamlinePairGroup(pb, B["side_hi_r"], pfr, F["side_hi"], False, False),
"side_lo_r": pattern_api.AddSeamlinePairGroup(pb, B["side_lo_r"], pfr, F["side_lo"], False, False),
"shldr_l": pattern_api.AddSeamlinePairGroup(pb, B["shldr_l"], pfl, F["shldr"], True, False),
"side_hi_l": pattern_api.AddSeamlinePairGroup(pb, B["side_hi_l"], pfl, F["side_hi"], True, False),
"side_lo_l": pattern_api.AddSeamlinePairGroup(pb, B["side_lo_l"], pfl, F["side_lo"], True, False),
}
if WITH_CLASP:
# throat closure: two TRUE mirrors, same index -> the documented (F,F) case
report["seams"]["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) # never reuse index 0 -- it is the shared default
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")):
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)
# Keep the WHOLE arrangement entry -- the per-point default offsets are the payload.
arr = {str(a["ArrangementName"]): a for a in pattern_api.GetArrangementList()}
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"]))
ox = int(a["ArrangementOffsetX"]) if USE_POINT_DEFAULT_X else 50
pattern_api.SetArrangementPosition(p, ox, int(ARR_Y), int(a["ArrangementOffsetZ"]))
pattern_api.SetPatternStrengthen(p, STRENGTHEN)
used[nm] = {"x": ox, "y": int(ARR_Y), "z": int(a["ArrangementOffsetZ"])}
report["arrangement_used"] = used
utility_api.ResetClothArrangement() # SetArrangement only ASSIGNS
utility_api.Refresh3DWindow()
utility_api.SetCamViewPoint(2)
export_api.ExportSnapshot3D(os.path.join(SHOTS, "cape_%s_presim_front.png" % VERSION))
utility_api.SetCamViewPoint(5)
utility_api.Refresh3DWindow()
export_api.ExportSnapshot3D(os.path.join(SHOTS, "cape_%s_presim_top.png" % VERSION))
report["strengthen"] = STRENGTHEN
report["sim1"] = utility_api.Simulate(SIM_MAIN)
if STRENGTHEN:
for p in (pb, pfr, pfl):
pattern_api.SetPatternStrengthen(p, False)
report["sim2"] = utility_api.Simulate(SIM_RELAX)
utility_api.Refresh3DWindow()
utility_api.SetCamViewPoint(2)
export_api.ExportSnapshot3D(os.path.join(SHOTS, "cape_%s_front.png" % VERSION))
utility_api.SetCamViewPoint(4)
utility_api.Refresh3DWindow()
export_api.ExportSnapshot3D(os.path.join(SHOTS, "cape_%s_side.png" % VERSION))
utility_api.SetCamViewPoint(2)
# The back. ExportSnapshot3D has NO rear viewpoint; for a cape this is the PRIMARY
# view. Turntable reuses output*.png, so the return value is the only handle.
report["turntable"] = export_api.ExportTurntableImages(4) # 0 front 1 side 2 BACK 3 side
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 is MM; the FBX->census path is DECIMETRES
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),
"hem_m": round(zs[0], 3), # target 0.42 +-0.05
"span_m": round(zs[-1] - zs[0], 3),
"r_shoulder_1.34_1.40": band(1.34, 1.40),
"r_chest_1.22_1.30": band(1.22, 1.30),
"r_hip_0.90_0.96": band(0.90, 0.96),
"r_knee_0.50_0.56": band(0.50, 0.56),
}
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