@helix3/helix-cli 0.1.18-helix3.326 → 0.1.18-helix3.328

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,6 +66,7 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
66
66
  | `helix assets credits\|check-loaders` | Render asset credits from provenance and reject models requiring unsupported loaders. |
67
67
  | `helix character import <body.glb> [--map <json>] [--fit source\|canonical] [--provenance <json>]` | Convert one self-contained third-party humanoid through the public `helix-humanoid@1` pipeline. The command rejects external/remote resources and resource bombs before parsing, generates bounded LODs, and emits a source/license/transformation/checksum receipt beside the output. `--fit canonical` aligns the rest skeleton/proportions; it does not prove that Base Male-targeted garments fit a different body surface. |
68
68
  | `helix character fit <addon.glb> --body <base.glb> -o <out.glb> (--slot-type <character.x@1>\|--hidden-regions <globs\|none>) [--clearance-mm 3]` | Pose-aware poke-through fit for a packaged character add-on (every LOD): across the canonical pose set, pushes garment geometry out along the body-facing normal to a minimum clearance, smoothed so hems and silhouettes keep their shape. Gated inside the stage by the poke-through measurement: the output is the fitted mesh only when it is strictly better (exit 0), otherwise a byte-identical copy of the input (exit 3). Bridge add-on imports run the same stage automatically; `helix item publish-version --fit-body <base.glb>` runs it before shipping a corrected mesh. |
69
+ | `helix character ear-clearance <addon.glb> --body <base.glb> (-o <out.glb>\|--check) [--target-mm 4] [--trigger-mm 1.5] [--max-push-mm 18] [--json]` | Head-zone check and fix for headwear / mask / hair / eyewear: finds the base body's ears, measures the clearance to the OUTERMOST garment layer around each (horizontal ray from the head axis, per LOD; only samples the garment wraps on all four sides count, so a mask that stops in front of the ear or a cap above it is not a defect) and, when a wrapped ear has < 1.5 mm, pushes the garment smoothly outward (Gaussian field, sigma 22 mm, horizontal only, <= 18 mm total, every LOD). Kept only when no ear pokes afterwards. `--check` writes nothing and exits 3 when an ear pokes through. `helix character fit` and `helix item publish-version --fit-body` run it automatically for head-zone slot types (`--no-ear-clearance` skips it; `--ear-clearance` forces it); `publish-version` refuses a mesh whose ears still poke after the fix. The pose-aware fit cannot do this job on a thin head shell. |
69
70
  | `helix character derive-hide <addon.glb> --body <base.glb> --slot-type <character.x@1> [--threshold 98] [--ops-out ops.json] [--json]` | Measures which of the slot's granted body regions the garment fully occludes (bind pose; a body vertex counts when garment is in front of it along its outward normal within 60 mm; the least-covered LOD decides) and prints the `hide` ops that follow, with the measured share of every region left visible. A region is hidden only at >= 98 %; partly covered regions stay visible and the pose-aware fit keeps their skin from poking through. Bridge add-on conform does this automatically when the profile names no `hideRegions`; use this command with `helix item publish-version --ops <ops.json>` to correct an already-published garment. |
70
71
  | `helix character retarget-animation <clip.fbx\|clip.glb>` | Retarget a raw Mixamo/Meshy clip directly to `helix-humanoid@1`. FBX conversion is deterministic and runs inside the CLI; no hidden Blender/DCC pre-export is required. |
71
72
  | `helix pack scaffold\|validate\|publish <dir>` | Locomotion packs — the replacement movement clips a player equips as a character add-on in the `character.locomotion@1` clip-set slot. `scaffold` writes `anims/clip-meta.json` plus a README carrying the per-clip contract; `validate` runs the whole gate offline in **one** pass (clip-meta schema, one GLB per entry with exactly one animation, measured length against the declared duration, the gait-only vocabulary, the `strideSpeed` bands, and walk < run < sprint); `publish` uploads each clip GLB as a public Vault animation, records the returned pins in `anims/pins.json`, then publishes the **meshless** add-on whose ops pin those versions — a re-run reuses every unchanged pin, so an interrupted publish resumes. The governing rule of the slot: a pack may change how a walk *looks*; it may never change how far that walk *gets you*. Partial packs are valid products down to a single clip. `--dry-run` stops before any network call and works logged out. |
@@ -1,3 +1,4 @@
1
+ import { type EarClearanceOptions, type EarClearanceReport } from './stages/earClearance';
1
2
  import { type PoseAwareFitOptions, type PoseAwareFitReport } from './stages/poseAwareFit';
2
3
  /**
3
4
  * File-level entry point for the pose-aware fit stage — what `helix character fit` and
@@ -6,6 +7,11 @@ import { type PoseAwareFitOptions, type PoseAwareFitReport } from './stages/pose
6
7
  * Output contract, so a batch driver never has to guess:
7
8
  * - outcome 'improved': `output` holds the fitted GLB (only POSITION data differs from the input).
8
9
  * - any other outcome: `output` is a BYTE-IDENTICAL copy of the input — the gate kept the original.
10
+ *
11
+ * Head-zone slots (headwear, mask, hair, eyewear) get a second stage after the pose-aware fit: ear clearance
12
+ * (`stages/earClearance.ts`) — the pose-aware fit cannot work on a thin head shell, and the ears poking through
13
+ * it is what the visual-QA gate fails them for. It runs automatically for those slot types, or on demand with
14
+ * `earClearance: true`; `earClearance: false` turns it off. `fitted` is true when EITHER stage improved the mesh.
9
15
  */
10
16
  export interface FitGarmentFileOptions extends Omit<PoseAwareFitOptions, 'hiddenRegions'> {
11
17
  garment: string;
@@ -17,9 +23,14 @@ export interface FitGarmentFileOptions extends Omit<PoseAwareFitOptions, 'hidden
17
23
  hiddenRegions?: readonly string[];
18
24
  /** Also write the report JSON here. */
19
25
  reportPath?: string;
26
+ /** Ear-clearance stage: undefined = on for head-zone slot types only; true = always; false = never. */
27
+ earClearance?: boolean;
28
+ earClearanceOptions?: EarClearanceOptions;
20
29
  }
21
30
  export interface FitGarmentFileResult {
22
31
  report: PoseAwareFitReport;
32
+ /** The ear-clearance stage's report; null when the stage did not run. */
33
+ ears: EarClearanceReport | null;
23
34
  output: string;
24
35
  /** True when `output` is the fitted geometry; false when it is a byte-identical copy of the input. */
25
36
  fitted: boolean;
@@ -8,6 +8,7 @@ const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const meshoptimizer_1 = require("meshoptimizer");
10
10
  const bodyRegionHide_1 = require("./bodyRegionHide");
11
+ const earClearance_1 = require("./stages/earClearance");
11
12
  const poseAwareFit_1 = require("./stages/poseAwareFit");
12
13
  function resolveFitScope(slotType, hiddenRegions) {
13
14
  if (hiddenRegions !== undefined)
@@ -32,19 +33,21 @@ async function fitGarmentFile(opts) {
32
33
  const garmentBytes = (0, node_fs_1.readFileSync)(opts.garment);
33
34
  const garment = await io.readBinary(garmentBytes);
34
35
  const body = await io.read(opts.body);
35
- const { garment: _g, body: _b, output: _o, slotType: _s, reportPath: _r, ...stageOptions } = opts;
36
+ const { garment: _g, body: _b, output: _o, slotType: _s, reportPath: _r, earClearance: _e, earClearanceOptions: _eo, ...stageOptions } = opts;
36
37
  const report = (0, poseAwareFit_1.poseAwareFit)(garment, body, { ...stageOptions, hiddenRegions });
38
+ const runEars = opts.earClearance ?? (opts.slotType !== undefined && (0, earClearance_1.isEarRelevantSlotType)(opts.slotType));
39
+ const ears = runEars ? (0, earClearance_1.clearEars)(garment, body, { onProgress: opts.onProgress, ...opts.earClearanceOptions }) : null;
37
40
  const output = (0, node_path_1.resolve)(opts.output);
38
41
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(output), { recursive: true });
39
- const fitted = report.outcome === 'improved';
42
+ const fitted = report.outcome === 'improved' || ears?.outcome === 'improved';
40
43
  if (fitted)
41
44
  (0, node_fs_1.writeFileSync)(output, await io.writeBinary(garment));
42
45
  else if ((0, node_path_1.resolve)(opts.garment) !== output)
43
46
  (0, node_fs_1.copyFileSync)(opts.garment, output);
44
47
  if (opts.reportPath !== undefined) {
45
48
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, node_path_1.resolve)(opts.reportPath)), { recursive: true });
46
- (0, node_fs_1.writeFileSync)(opts.reportPath, `${JSON.stringify({ garment: opts.garment, body: opts.body, output, fitted, ...report }, null, 2)}\n`);
49
+ (0, node_fs_1.writeFileSync)(opts.reportPath, `${JSON.stringify({ garment: opts.garment, body: opts.body, output, fitted, ...report, ears }, null, 2)}\n`);
47
50
  }
48
- return { report, output, fitted };
51
+ return { report, ears, output, fitted };
49
52
  }
50
53
  //# sourceMappingURL=fitGarmentFile.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fitGarmentFile.js","sourceRoot":"","sources":["../../src/character/fitGarmentFile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fitGarmentFile.js","sourceRoot":"","sources":["../../src/character/fitGarmentFile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ import type { Document } from '@gltf-transform/core';
2
+ /**
3
+ * EAR CLEARANCE — the head-zone conform stage that stops the base body's ears poking through headwear, masks
4
+ * and hair, and moves the garment smoothly clear of them when they do.
5
+ *
6
+ * ── WHY ────────────────────────────────────────────────────────────────────────────────────────────────────
7
+ * The pose-aware fit (`poseAwareFit.ts`) cannot help here: on a thin head shell it measures hundreds of body
8
+ * vertices "poking" at the forehead and the hairline and refuses its own result (Panama Hat: 408 → 979+
9
+ * violations). The visual-QA gate (2026-10-02) then failed Panama Hat and Bandana — "the ear pokes through the
10
+ * brim / the ragged edge" — and a census of the 28 Base Male head-zone add-ons found the same defect in seven
11
+ * more (Maska, Hell X Balaclava, Gamer Mask, Eye Hole Mask 1 and 2, Joker Mask, and the Bandana) with the ear
12
+ * 3-9 mm OUTSIDE the cloth. Every one is the same shape: a close-fitting shell that wraps the ear with no
13
+ * standoff, because nobody modelled the ear when the shell was authored around the skull.
14
+ *
15
+ * ── MEASURE ────────────────────────────────────────────────────────────────────────────────────────────────
16
+ * The ears are found from the BODY, not hard-coded: head-region triangles (dominant joint `head`) above the
17
+ * head joint whose |x - centreX| exceeds 88% of the head's widest point are the ears (the lateral extremes of
18
+ * the head; on Base Male 1.3.0 that is |x| > 86 mm, 9 000 samples at 3 mm). For every ear sample a horizontal
19
+ * ray runs from the head axis through the sample; the OUTERMOST garment hit along it decides the clearance:
20
+ * `clearance = tOutermost - distanceToSample` (positive = cloth outside the ear). A sample counts as a
21
+ * poke-through only when the garment WRAPS it — the four rays 12 mm above, below, ahead of and behind it also
22
+ * hit the garment — so a mask whose rear edge simply stops in front of the ear, or a cap that sits above it,
23
+ * is not an ear defect.
24
+ *
25
+ * ── FIX ────────────────────────────────────────────────────────────────────────────────────────────────────
26
+ * Panama Hat's hand fix, generalised: a smooth, bounded, lateral push. Each poking sample asks for
27
+ * `target - clearance` (capped), the garment's own vertices take a Gaussian-weighted maximum of those requests
28
+ * (sigma 22 mm, a smooth ramp in |x| so nothing near the centre line moves), and the vertices move along the
29
+ * horizontal radial direction from the head axis. The loop re-measures and repeats (<= 8 passes; a vertex never
30
+ * moves more than `maxPushMm` in total). Only POSITION changes — skin weights, UVs, normals, materials and
31
+ * topology are untouched. Every LOD level is fitted on its own; a POSITION accessor shared by two LODs moves once.
32
+ *
33
+ * ── GATE ───────────────────────────────────────────────────────────────────────────────────────────────────
34
+ * The fix is kept only when the poke count ends at ZERO (clearance >= `triggerMm` everywhere the garment
35
+ * wraps an ear) in every LOD that had any; otherwise the garment document is left byte-for-byte untouched and
36
+ * the report says why. A garment that already clears (or never covers the ears) is reported as such.
37
+ */
38
+ export interface EarClearanceOptions {
39
+ /** Clearance the fix aims for, in mm: the cloth ends at least this far outside the ear (default 4). */
40
+ targetMm?: number;
41
+ /** A wrapped ear sample with less clearance than this is a poke-through (default 1.5). */
42
+ triggerMm?: number;
43
+ /** Largest total move for any garment vertex, in mm (default 18). */
44
+ maxPushMm?: number;
45
+ /** Falloff radius of the push, in mm (default 22). */
46
+ sigmaMm?: number;
47
+ /** Measure -> push passes (default 8). */
48
+ iterations?: number;
49
+ /** Ear sample spacing, in mm (default 3). */
50
+ sampleMm?: number;
51
+ onProgress?: (message: string) => void;
52
+ }
53
+ export interface EarClearanceStats {
54
+ /** Ear surface samples measured. */
55
+ samples: number;
56
+ /** Samples with any garment hit along their ray. */
57
+ covered: number;
58
+ /** Covered samples the garment wraps on all four sides. */
59
+ wrapped: number;
60
+ /** Wrapped samples with less than `triggerMm` clearance. */
61
+ pokes: number;
62
+ /** Smallest clearance among wrapped samples (and covered samples that clear), in mm; null when the garment covers no ear. */
63
+ minMm: number | null;
64
+ p5Mm: number | null;
65
+ }
66
+ export type EarClearanceOutcome = 'improved' | 'unchanged-already-clear' | 'unchanged-no-improvement' | 'skipped';
67
+ export interface EarClearanceUnitReport {
68
+ mesh: string;
69
+ before: EarClearanceStats;
70
+ after: EarClearanceStats;
71
+ passes: number;
72
+ maxMoveMm: number;
73
+ verticesMoved: number;
74
+ }
75
+ export interface EarClearanceReport {
76
+ outcome: EarClearanceOutcome;
77
+ reason: string | null;
78
+ ear: {
79
+ samples: number;
80
+ axisX: number;
81
+ axisZ: number;
82
+ earMinAbsX: number;
83
+ } | null;
84
+ settings: Required<Omit<EarClearanceOptions, 'onProgress'>>;
85
+ units: EarClearanceUnitReport[];
86
+ /** Worst-case (smallest min, summed pokes) across units. */
87
+ before: EarClearanceStats | null;
88
+ after: EarClearanceStats | null;
89
+ }
90
+ /** Slots whose geometry can sit over the ears. Eyewear is included: temples/straps wrap the ear. */
91
+ export declare const EAR_RELEVANT_SLOTS: readonly ["headwear", "mask", "hair", "eyewear", "head-accessory"];
92
+ export declare function isEarRelevantSlotType(slotType: string): boolean;
93
+ interface EarSamples {
94
+ /** 3 per sample. */
95
+ points: Float64Array;
96
+ axisX: number;
97
+ axisZ: number;
98
+ earMinAbsX: number;
99
+ }
100
+ export declare function findEarSamples(body: Document, sampleMm: number): EarSamples | null;
101
+ /** Measure only — never touches `garment`. `outcome` is what `clearEars` would do. */
102
+ export declare function measureEarClearance(garment: Document, body: Document, options?: EarClearanceOptions): EarClearanceReport;
103
+ /** Measure and, when the ears poke through, push the garment clear. Writes `garment`'s POSITION accessors ONLY when the outcome is 'improved'. */
104
+ export declare function clearEars(garment: Document, body: Document, options?: EarClearanceOptions): EarClearanceReport;
105
+ export {};