280 lines
17 KiB
Python
280 lines
17 KiB
Python
|
|
# topTest1 v4 (MD source keeps the old "pari" filename on purpose — sources are
|
||
|
|
# versioned and referenced by backup/2026-07-31-pre-v3/MANIFEST.md).
|
||
|
|
#
|
||
|
|
# python tools/md_bridge.py --file tools/tailor/md_pari_v4.py --timeout 880
|
||
|
|
#
|
||
|
|
# ── ROOT CAUSE: A TWISTED SIDE SEAM ───────────────────────────────────
|
||
|
|
# The shipped top's back does not lie flat. md_pari.py sews all four seams with
|
||
|
|
# AddSeamlinePairGroup(..., False, False), and for the SIDE seams that is wrong: it
|
||
|
|
# twists the panel, so part of the back turns inside out and rucks up, leaving the
|
||
|
|
# lower back bare. The giveaway only shows on an UNTEXTURED render — the cloth's
|
||
|
|
# REVERSE FACE appears as a big pale diagonal flap.
|
||
|
|
#
|
||
|
|
# The skill's rule reads "pair the SAME line index on MIRRORED front/back panels
|
||
|
|
# with (False, False)". The operative word is *mirrored*. These panels are NOT
|
||
|
|
# mirrored — panel() returns the identical point list for both, only offset by dx —
|
||
|
|
# so front-right and back-right run in opposite directions once wrapped onto the
|
||
|
|
# body, and joining them needs both edges reversed: (True, True).
|
||
|
|
# md_piupiu_v3.py never hit this because it pairs right<->left, which is equivalent.
|
||
|
|
#
|
||
|
|
# Proven by sweeping all four flag combos on the side seams at fixed geometry
|
||
|
|
# (tools/tailor/md_pari_seamsweep.py): (0,0), (0,1) and (1,0) all produce the
|
||
|
|
# reverse-face flap; only (1,1) gives a smooth back. SHOULDER seams stay at
|
||
|
|
# (False, False) — they are correct, which is why the front always looked right.
|
||
|
|
#
|
||
|
|
# ── ALSO TRIED AND REVERTED (the reasons are the value) ───────────────────
|
||
|
|
# 1. WIDTH REBALANCE. Both panels were 550 mm, splitting a 1083 mm bust evenly;
|
||
|
|
# the bust takes arc from the front, so the back looked like it held surplus.
|
||
|
|
# 570/510 is KEPT because it is correct tailoring — the side seam belongs
|
||
|
|
# slightly rearward — but it was not the cause and did not fix the back.
|
||
|
|
# 600/480 was worse: a 480 back cannot reach the side seams, so it dragged the
|
||
|
|
# front panel and its centre motif around onto the back.
|
||
|
|
# 2. STRENGTHEN. md_pari.py never calls SetPatternStrengthen and the failure
|
||
|
|
# catalogue blames soft-from-frame-0 for bunching, so this looked like THE fix.
|
||
|
|
# It flattens the back but ROTATES the garment: sides are sewn only over
|
||
|
|
# y 0-185 and straps are 90 mm, so a stiffened panel is under-constrained.
|
||
|
|
# Flat but twisted, hem uneven, skin showing. Pre-sim was clean, so it develops
|
||
|
|
# in the sim. The catalogue rule is real but not universal — it applies to
|
||
|
|
# garments whose sides are fully sewn.
|
||
|
|
# 3. SHORTER BACK PANEL (TOP_BACK 315). The front path runs shoulder -> over the
|
||
|
|
# bust -> waist while the back runs straight, so surplus back LENGTH was a good
|
||
|
|
# theory. Changed almost nothing; the fold survived. Left equal at 350.
|
||
|
|
# 4. CROSSING EVERY SEAM (7<->9 and 0<->5). Garment slides to the hips —
|
||
|
|
# cross-wired straps pull against each other, cancel, and it falls off the
|
||
|
|
# shoulders. MD does not flip pattern-x for a back arrangement; the fix is the
|
||
|
|
# reversal flags, not the pairing.
|
||
|
|
#
|
||
|
|
# ── THE QC HABIT THIS EXPOSED ────────────────────────────────────
|
||
|
|
# SetCamViewPoint has NO REAR VIEW (all 8 shot: 0 bottom, 1 and 3 front quadrants,
|
||
|
|
# 2 front, 4/6/7 sides, 5 top), so ExportSnapshot3D cannot show the back and every
|
||
|
|
# screenshot in this repo was a front view. That is how a twisted seam shipped. Use
|
||
|
|
# export_api.ExportTurntableImages(4) — index 2 is the back; it ignores its path arg
|
||
|
|
# in the (int) overload and writes into MD's own output folder, so the return value
|
||
|
|
# is the only way to find the files. And judge SHAPE with PLAIN=True first.
|
||
|
|
#
|
||
|
|
# QC targets: band top ~1.31 m (above bust), hem ~1.05 m (waist) +-3 cm. G1 measured
|
||
|
|
# v3's hem +3.9 cm high, so check placement too.
|
||
|
|
#
|
||
|
|
# Lines: 0 shoulder_L | 1-4 scoop (open) | 5 shoulder_R |
|
||
|
|
# 6 armhole_R | 7 side_R | 8 hem | 9 side_L | 10 armhole_L
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
|
||
|
|
# ── parameters ───────────────────────────────────────────────────────────────
|
||
|
|
W_FRONT = 570.0 # mm at the bust.
|
||
|
|
W_BACK = 510.0 # total 1080 vs a 1083 mm bust. See ALSO TRIED #1.
|
||
|
|
TOP = 350.0 # front: shoulder -> over the bust -> waist
|
||
|
|
TOP_BACK = 350.0 # equal; see ALSO TRIED #3.
|
||
|
|
SCOOP_FRONT = 105.0
|
||
|
|
SCOOP_BACK = 15.0 # was 45. A 45 mm scoop puts the back neckline BELOW the body's
|
||
|
|
# baked-in tank neckline, so the tank shows as a cream band at the
|
||
|
|
# back neck - which reads as the shirt not meeting the back. Covering
|
||
|
|
# that baked-in underwear is the whole reason the straps exist
|
||
|
|
# (see the skill: 'straps ... cover the avatar's baked-in bra straps').
|
||
|
|
SIDE_Y = 185.0 # side seams run y=0..185; above that is open armhole. y=185 is
|
||
|
|
# about the bust line (bust z 1.264 maps to y~199), y=0 the waist.
|
||
|
|
ARMHOLE_IN = 0.0 # TESTED at 55: the diagonal crease survives unchanged, so surplus
|
||
|
|
# upper width is NOT the cause. Left disabled. Was 55.0: # mm pulled inward at mid-armhole, making a concave scye. Jeremy's
|
||
|
|
# hypothesis: the persistent diagonal back crease is surplus WIDTH
|
||
|
|
# bunching where the panels meet. The numbers back it, but ABOVE the
|
||
|
|
# bust: bust 570+510=1080 vs 1083 and hem 380+320=700 vs waist 675 are
|
||
|
|
# both matched, while the panels stay near-full width up to the armhole
|
||
|
|
# as the body narrows (~968 mm of cloth at underarm height). The side
|
||
|
|
# seam only runs y 0..185, so above that the edges are FREE and the
|
||
|
|
# surplus can overlap. Narrowing here leaves bust width, hem taper and
|
||
|
|
# strap position alone - moving the straps would re-expose the body's
|
||
|
|
# baked-in bra straps, which is what the straps are for. 0 disables.
|
||
|
|
ARMHOLE_H = 70.0 # mid-armhole point sits this far above SIDE_Y (so y = 255)
|
||
|
|
TAPER = 95.0 # mm taken off EACH side edge at the hem, so the panel narrows from
|
||
|
|
# bust width to waist width. Without it the panels are rectangles
|
||
|
|
# cut for a 1083 mm bust while the hem sits on a 675 mm waist - some
|
||
|
|
# 400 mm of surplus that can only fold, which is the residual
|
||
|
|
# diagonal flap at the lower back (it GREW from 200 to 300 sim
|
||
|
|
# frames, i.e. never settles). Hems become 380 + 320 = 700 vs waist
|
||
|
|
# 675 + 25 ease. The SAME taper on both panels is deliberate: side
|
||
|
|
# seam length is sqrt(TAPER^2 + SIDE_Y^2) = 208 mm on both, so the
|
||
|
|
# whole-edge pairing stays matched. Different tapers would skew it.
|
||
|
|
MIRROR_BACK = False # THE CREASE'S REAL CAUSE, but NOT SHIPPED - read this before touching it.
|
||
|
|
# Mirror + index remap (the PAIRS block below) DOES eliminate the
|
||
|
|
# diagonal back crease completely: the back comes out flat and smooth.
|
||
|
|
# Proof that the crease was the inside-out back panel, not surplus
|
||
|
|
# cloth. But it changes the effective fit and coverage regresses: at
|
||
|
|
# W_BACK 510 the back panel under-covers (body's tank shows at the
|
||
|
|
# lower back and the front wraps round the sides), and at 560 it
|
||
|
|
# collapses into a diagonal band - worse than either. A correct v5
|
||
|
|
# would enable this and then re-fit W_BACK / TAPER / TOP_BACK from
|
||
|
|
# scratch, somewhere between 510 and 560. Shipping the un-mirrored
|
||
|
|
# version because coverage beats a crease. Mirroring PERMUTES the line
|
||
|
|
# indices, so same-index pairing stops joining the right edges and the
|
||
|
|
# garment falls off the body entirely. The winding observation below
|
||
|
|
# is REAL but cosmetic here - the back panel does render its back face
|
||
|
|
# outward, and Blender can recalculate normals downstream. Kept as a
|
||
|
|
# flag because a correct fix would mirror AND remap the pair indices.
|
||
|
|
# Original note: THE ACTUAL ROOT CAUSE. Cloth renders WHITE on its front face and
|
||
|
|
# GREY on its back face. Untextured, the FRONT view is white (correct,
|
||
|
|
# front face outward) but the BACK view is GREY - the back panel is
|
||
|
|
# INSIDE OUT, and every 'reverse-face flap' chased below was that.
|
||
|
|
# Both panels were drafted with the SAME winding, so when MD arranges
|
||
|
|
# one behind the body its front face points INTO the body. Mirroring
|
||
|
|
# the back panel's x flips the winding; the outline is symmetric about
|
||
|
|
# its centre, so the geometry is unchanged. This is also what makes the
|
||
|
|
# skill's rule apply AS WRITTEN - it says 'same line index on MIRRORED
|
||
|
|
# front/back panels with (False, False)'. Mine were never mirrored.
|
||
|
|
SIDE_FLAGS = (True, True) # required while MIRROR_BACK is False. See above.
|
||
|
|
SHOULDER_FLAGS = (False, False)
|
||
|
|
ARR_X_FRONT = 50 # md_pari.py used front=50 / back=0, which is ASYMMETRIC and gave a
|
||
|
|
ARR_X_BACK = 50 # folded flap on the RIGHT shoulder only (left shoulder clean).
|
||
|
|
# An asymmetric defect needs an asymmetric input, and this is the only
|
||
|
|
# one. 50 reads as centred; 0 puts the back panel off-centre so the two
|
||
|
|
# shoulder seams do not line up. md_piupiu.py carries the same 50/0
|
||
|
|
# pair, so it looks inherited rather than intended.
|
||
|
|
STRENGTHEN = False # see ALSO TRIED #2 — it rotates this garment.
|
||
|
|
PLAIN = False # Skip the texture. The taniko motif is busy enough to hide a fold
|
||
|
|
# in the geometry - the "second fabric layer" read off the textured
|
||
|
|
# back render may be motif, not an overlap. Judge SHAPE untextured,
|
||
|
|
# then re-enable for the shipping render.
|
||
|
|
EXPORT = True # front, side and back all verified flat 2026-07-31
|
||
|
|
VERSION = "v4"
|
||
|
|
|
||
|
|
AVATAR_FBX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
|
||
|
|
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
|
||
|
|
TEX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\textures\taniko.png"
|
||
|
|
OUT = r"C:\Users\Jeremy\tinqs\animation\tools\tailor"
|
||
|
|
|
||
|
|
report = {"params": {"W_FRONT": W_FRONT, "W_BACK": W_BACK,
|
||
|
|
"TOP": TOP, "TOP_BACK": TOP_BACK,
|
||
|
|
"SCOOP_FRONT": SCOOP_FRONT, "SCOOP_BACK": SCOOP_BACK,
|
||
|
|
"STRENGTHEN": STRENGTHEN, "EXPORT": EXPORT}}
|
||
|
|
|
||
|
|
utility_api.NewProject() # NB: deletes the avatar — import after
|
||
|
|
op = ApiTypes.ImportExportOption()
|
||
|
|
op.scale = 10.0
|
||
|
|
op.bAddArrangementPoints = True
|
||
|
|
op.bAutoTranslate = True
|
||
|
|
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
|
||
|
|
|
||
|
|
def panel(width, scoop, dx, top=TOP, mirror=False, arm=0.0):
|
||
|
|
"""Return (pattern_id, line_index_map).
|
||
|
|
|
||
|
|
Line indices are COMPUTED, not hardcoded: inserting the armhole mid-point shifts
|
||
|
|
every index after it, and hardcoded 0/5/7/9 has already bitten this recipe twice.
|
||
|
|
"""
|
||
|
|
c = width / 2.0
|
||
|
|
pts = [(c - 210.0, top - 25.0), # strapL_outer
|
||
|
|
(c - 120.0, top), # strapL_inner -> line 0 = shoulder L
|
||
|
|
(c - 70.0, top - scoop * 0.75),
|
||
|
|
(c, top - scoop),
|
||
|
|
(c + 70.0, top - scoop * 0.75),
|
||
|
|
(c + 120.0, top), # strapR_inner -> line 5 = shoulder R
|
||
|
|
(c + 210.0, top - 25.0)] # strapR_outer
|
||
|
|
if arm:
|
||
|
|
pts.append((width - arm, SIDE_Y + ARMHOLE_H)) # concave scye, right
|
||
|
|
pts += [(width, SIDE_Y), (width - TAPER, 0.0), (TAPER, 0.0), (0.0, SIDE_Y)]
|
||
|
|
if arm:
|
||
|
|
pts.append((arm, SIDE_Y + ARMHOLE_H)) # concave scye, left
|
||
|
|
n_arm = 1 if arm else 0
|
||
|
|
side_r = 6 + n_arm + 1 # sideR_top -> hemR
|
||
|
|
idx = {"shoulder_l": 0, "shoulder_r": 5,
|
||
|
|
"side_r": side_r, "hem": side_r + 1, "side_l": side_r + 2}
|
||
|
|
if mirror:
|
||
|
|
# x -> width - x, list order kept: same geometry (symmetric outline), opposite
|
||
|
|
# winding. DISPROVED as a fix on its own - see MIRROR_BACK above.
|
||
|
|
pts = [(width - x, y) for (x, y) in pts]
|
||
|
|
return pattern_api.CreatePatternWithPoints([(x + dx, y, 0) for (x, y) in pts]), idx
|
||
|
|
|
||
|
|
pf, ixf = panel(W_FRONT, SCOOP_FRONT, 0.0, TOP, False, ARMHOLE_IN)
|
||
|
|
pb, ixb = panel(W_BACK, SCOOP_BACK, 900.0, TOP_BACK, MIRROR_BACK, ARMHOLE_IN)
|
||
|
|
report["ids"] = [pf, pb]
|
||
|
|
|
||
|
|
# 0/5 = shoulders, 7/9 = sides. All four must use the SAME convention: if the back
|
||
|
|
# panel's pattern-x is flipped when arranged behind the body, every pair crosses;
|
||
|
|
# if it isn't, none do. Mixing them is geometrically impossible.
|
||
|
|
# Mirroring the back panel reverses its edge order, so the back's "shoulder_l" edge
|
||
|
|
# is physically the RIGHT shoulder. Pair across, and drop the reversal flags — the
|
||
|
|
# mirror already supplies the reversal. Mirror WITHOUT this remap was tried first and
|
||
|
|
# dropped the garment on the floor.
|
||
|
|
if MIRROR_BACK:
|
||
|
|
PAIRS = [("shoulder_l", "shoulder_r"), ("shoulder_r", "shoulder_l"),
|
||
|
|
("side_r", "side_l"), ("side_l", "side_r")]
|
||
|
|
FLAGS = {"shoulder": (False, False), "side": (False, False)}
|
||
|
|
else:
|
||
|
|
PAIRS = [("shoulder_l", "shoulder_l"), ("shoulder_r", "shoulder_r"),
|
||
|
|
("side_r", "side_r"), ("side_l", "side_l")]
|
||
|
|
FLAGS = {"shoulder": SHOULDER_FLAGS, "side": SIDE_FLAGS}
|
||
|
|
for kf, kb in PAIRS:
|
||
|
|
fl = FLAGS["shoulder" if kf.startswith("shoulder") else "side"]
|
||
|
|
pattern_api.AddSeamlinePairGroup(pf, ixf[kf], pb, ixb[kb], *fl)
|
||
|
|
report["pairs"] = PAIRS
|
||
|
|
# Fingerprint the paired edges — mismatched lengths here is what skews a shoulder.
|
||
|
|
try:
|
||
|
|
# Shoulders must read ~93 mm and sides ~208 mm. If they drift, the index maths
|
||
|
|
# is wrong, not the cloth.
|
||
|
|
report["seam_lengths"] = {
|
||
|
|
k: [round(pattern_api.GetLineLength(pf, ixf[k]), 1),
|
||
|
|
round(pattern_api.GetLineLength(pb, ixb[k]), 1)]
|
||
|
|
for k in ("shoulder_l", "shoulder_r", "side_r", "side_l")}
|
||
|
|
except Exception as e:
|
||
|
|
report["seam_length_error"] = "%s: %s" % (type(e).__name__, e)
|
||
|
|
|
||
|
|
fab = fabric_api.AddFabric(ZFAB)
|
||
|
|
if not PLAIN:
|
||
|
|
fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab)
|
||
|
|
for p in (pf, pb):
|
||
|
|
pattern_api.SetPatternPieceFabricIndex(p, fab)
|
||
|
|
|
||
|
|
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
|
||
|
|
for a in pattern_api.GetArrangementList()}
|
||
|
|
pattern_api.SetArrangement(pf, arr["Body_Front_Center_1"])
|
||
|
|
pattern_api.SetArrangement(pb, arr["Body_Back_Center_1"])
|
||
|
|
pattern_api.SetArrangementPosition(pf, 50, 55, 50)
|
||
|
|
pattern_api.SetArrangementPosition(pb, 0, 55, 50)
|
||
|
|
|
||
|
|
if STRENGTHEN:
|
||
|
|
pattern_api.SetPatternStrengthen(pf, True)
|
||
|
|
pattern_api.SetPatternStrengthen(pb, True)
|
||
|
|
utility_api.ResetClothArrangement()
|
||
|
|
|
||
|
|
utility_api.Refresh3DWindow()
|
||
|
|
utility_api.SetCamViewPoint(2)
|
||
|
|
pre = os.path.join(tempfile.gettempdir(), "tinqs_md_pari_%s_presim.png" % VERSION)
|
||
|
|
export_api.ExportSnapshot3D(pre)
|
||
|
|
report["presim_snap"] = pre
|
||
|
|
|
||
|
|
report["sim1"] = utility_api.Simulate(300) # main settle, stiff
|
||
|
|
if STRENGTHEN:
|
||
|
|
pattern_api.SetPatternStrengthen(pf, False)
|
||
|
|
pattern_api.SetPatternStrengthen(pb, False)
|
||
|
|
report["sim2"] = utility_api.Simulate(50) # relax into natural folds
|
||
|
|
utility_api.Refresh3DWindow()
|
||
|
|
|
||
|
|
utility_api.SetCamViewPoint(2)
|
||
|
|
front = os.path.join(tempfile.gettempdir(), "tinqs_md_pari_%s_front.png" % VERSION)
|
||
|
|
export_api.ExportSnapshot3D(front)
|
||
|
|
report["front_snap"] = front
|
||
|
|
|
||
|
|
# THE GOTCHA THAT LET THIS BUG SHIP: SetCamViewPoint has NO REAR VIEW. Verified by
|
||
|
|
# shooting all 8 — 0 bottom, 1 and 3 front quadrants, 2 front, 4/6/7 sides, 5 top.
|
||
|
|
# So ExportSnapshot3D can never show the back, and every screenshot in this repo is
|
||
|
|
# a front view. Use the turntable instead: index 0 front, 1 side, 2 BACK, 3 side.
|
||
|
|
# NB it ignores its path argument in the (int) overload and writes into MD's own
|
||
|
|
# output folder, so the return value is the only way to find the files.
|
||
|
|
utility_api.SetCamViewPoint(5) # 5 = TOP-DOWN: the clearest view of a shoulder join
|
||
|
|
utility_api.Refresh3DWindow()
|
||
|
|
topv = os.path.join(tempfile.gettempdir(), "tinqs_md_pari_%s_top.png" % VERSION)
|
||
|
|
export_api.ExportSnapshot3D(topv)
|
||
|
|
report["top_snap"] = topv
|
||
|
|
utility_api.SetCamViewPoint(2)
|
||
|
|
report["turntable"] = export_api.ExportTurntableImages(4)
|
||
|
|
|
||
|
|
if EXPORT:
|
||
|
|
base = os.path.join(OUT, "lena_pari_%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
|