# 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