@helix3/helix-cli 0.1.15-helix3.227 → 0.1.15-helix3.229

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.
@@ -31,6 +31,41 @@ export declare const WEARABLE_LIMITS: {
31
31
  readonly maxFileBytes: number;
32
32
  readonly maxMaterials: 4;
33
33
  };
34
+ /** The backend's `VEHICLE_MAX_*` defaults. A vehicle is not furniture and is not judged as such. */
35
+ export declare const VEHICLE_LIMITS: {
36
+ readonly maxTriangles: 90000;
37
+ readonly maxTextureEdge: 2048;
38
+ readonly maxFileBytes: number;
39
+ readonly maxMaterials: 48;
40
+ };
41
+ /** The backend's `WEAPON_MAX_*` defaults. Held at arm's length in first person; not furniture either. */
42
+ export declare const WEAPON_LIMITS: {
43
+ readonly maxTriangles: 30000;
44
+ readonly maxTextureEdge: 2048;
45
+ readonly maxFileBytes: number;
46
+ readonly maxMaterials: 12;
47
+ };
48
+ /** The backend's `HOME_ITEM_MAX_*` defaults. */
49
+ export declare const HOME_ITEM_LIMITS: {
50
+ readonly maxTriangles: 15000;
51
+ readonly maxTextureEdge: 2048;
52
+ readonly maxFileBytes: number;
53
+ readonly maxMaterials: 6;
54
+ };
55
+ /** Kinds this preflight can judge. A kind absent here is left entirely to the server. */
56
+ export type PreflightKind = 'avatar' | 'wearable' | 'add_on' | 'home_item' | 'vehicle' | 'weapon';
57
+ /**
58
+ * THE DRAW-CALL WALL, PER KIND — the backend's `*_MAX_DRAW_CALLS` defaults.
59
+ *
60
+ * A draw call is one glTF mesh primitive, counted once per node that places it.
61
+ * It is NOT the material count, which is only a lower bound: the published
62
+ * reference vehicle bound 43 materials across 188 primitives and so cleared a
63
+ * 48-material ceiling by 11 while submitting 188 batches.
64
+ *
65
+ * Design targets, which this does not enforce but the report prints, are half
66
+ * of each: character 6, wearable 2, add-on 4, item 8, vehicle 12, weapon 4.
67
+ */
68
+ export declare const DRAW_CALL_LIMITS: Record<PreflightKind, number>;
34
69
  export type PreflightSeverity = 'error' | 'warning';
35
70
  export interface PreflightFinding {
36
71
  severity: PreflightSeverity;
@@ -49,12 +84,6 @@ export interface AvatarPreflightResult {
49
84
  firstPersonHide: number | null;
50
85
  };
51
86
  }
52
- /**
53
- * Check a character GLB against the publish gate without touching the network.
54
- *
55
- * `kind` picks the ceiling set: an avatar and a wearable are the same file format with different budgets,
56
- * because several wearables are worn on top of one avatar.
57
- */
58
- export declare function preflightAvatar(buffer: Buffer, kind?: 'avatar' | 'wearable'): AvatarPreflightResult;
87
+ export declare function preflightAvatar(buffer: Buffer, kind?: PreflightKind): AvatarPreflightResult;
59
88
  /** One line per finding, plus the capability line, for the CLI's own output. */
60
89
  export declare function formatPreflight(result: AvatarPreflightResult): string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WEARABLE_LIMITS = exports.AVATAR_LIMITS = void 0;
3
+ exports.DRAW_CALL_LIMITS = exports.HOME_ITEM_LIMITS = exports.WEAPON_LIMITS = exports.VEHICLE_LIMITS = exports.WEARABLE_LIMITS = exports.AVATAR_LIMITS = void 0;
4
4
  exports.preflightAvatar = preflightAvatar;
5
5
  exports.formatPreflight = formatPreflight;
6
6
  const glb_1 = require("../glb");
@@ -36,6 +36,56 @@ exports.WEARABLE_LIMITS = {
36
36
  maxFileBytes: 6 * 1024 * 1024,
37
37
  maxMaterials: 4,
38
38
  };
39
+ /** The backend's `VEHICLE_MAX_*` defaults. A vehicle is not furniture and is not judged as such. */
40
+ exports.VEHICLE_LIMITS = {
41
+ maxTriangles: 90_000,
42
+ maxTextureEdge: 2048,
43
+ maxFileBytes: 16 * 1024 * 1024,
44
+ maxMaterials: 48,
45
+ };
46
+ /** The backend's `WEAPON_MAX_*` defaults. Held at arm's length in first person; not furniture either. */
47
+ exports.WEAPON_LIMITS = {
48
+ maxTriangles: 30_000,
49
+ maxTextureEdge: 2048,
50
+ maxFileBytes: 8 * 1024 * 1024,
51
+ maxMaterials: 12,
52
+ };
53
+ /** The backend's `HOME_ITEM_MAX_*` defaults. */
54
+ exports.HOME_ITEM_LIMITS = {
55
+ maxTriangles: 15_000,
56
+ maxTextureEdge: 2048,
57
+ maxFileBytes: 8 * 1024 * 1024,
58
+ maxMaterials: 6,
59
+ };
60
+ const LIMITS_BY_KIND = {
61
+ avatar: exports.AVATAR_LIMITS,
62
+ wearable: exports.WEARABLE_LIMITS,
63
+ // An add-on is judged against the wearable ceilings server-side: several are
64
+ // mounted at once onto a host already paying its own budget.
65
+ add_on: exports.WEARABLE_LIMITS,
66
+ home_item: exports.HOME_ITEM_LIMITS,
67
+ vehicle: exports.VEHICLE_LIMITS,
68
+ weapon: exports.WEAPON_LIMITS,
69
+ };
70
+ /**
71
+ * THE DRAW-CALL WALL, PER KIND — the backend's `*_MAX_DRAW_CALLS` defaults.
72
+ *
73
+ * A draw call is one glTF mesh primitive, counted once per node that places it.
74
+ * It is NOT the material count, which is only a lower bound: the published
75
+ * reference vehicle bound 43 materials across 188 primitives and so cleared a
76
+ * 48-material ceiling by 11 while submitting 188 batches.
77
+ *
78
+ * Design targets, which this does not enforce but the report prints, are half
79
+ * of each: character 6, wearable 2, add-on 4, item 8, vehicle 12, weapon 4.
80
+ */
81
+ exports.DRAW_CALL_LIMITS = {
82
+ avatar: 12,
83
+ wearable: 4,
84
+ add_on: 8,
85
+ home_item: 16,
86
+ vehicle: 24,
87
+ weapon: 8,
88
+ };
39
89
  /**
40
90
  * Core joints the runtime needs to embed a body at all.
41
91
  *
@@ -71,8 +121,59 @@ function readSceneExtras(buffer) {
71
121
  * `kind` picks the ceiling set: an avatar and a wearable are the same file format with different budgets,
72
122
  * because several wearables are worn on top of one avatar.
73
123
  */
124
+ /**
125
+ * Which parts must stay separate primitives, per kind.
126
+ *
127
+ * "Merge your meshes" is correct advice right up until it destroys the thing
128
+ * that made the item interactive. The runtime resolves articulated parts BY
129
+ * NODE NAME — `getObjectByName` for a vehicle's articulators and lamps, socket
130
+ * source names for its wheels — so joining them does not just merge geometry,
131
+ * it deletes the handle the runtime steers by. Naming the specific parts per
132
+ * kind is the difference between advice a creator can follow safely and advice
133
+ * that breaks their car the first time someone opens a door.
134
+ */
135
+ const ARTICULATED_PARTS = {
136
+ avatar: 'the head, hands and anything a face or IK target drives',
137
+ wearable: 'anything a bone or blendshape drives',
138
+ add_on: 'anything mounted on a socket or driven by the host',
139
+ home_item: 'lids, drawers, doors and anything an interaction opens',
140
+ vehicle: 'wheels, doors, hood, boot, steering wheel and lamps',
141
+ weapon: 'the magazine, bolt, trigger and optic',
142
+ };
143
+ /**
144
+ * The local rejection text — the same facts the server will state, minus the round trip.
145
+ *
146
+ * It has to carry more than "too many draw calls" because the creator cannot see draw calls in any tool they
147
+ * own. So: what the number is, what a draw call IS, which materials are responsible by name, how much of the
148
+ * overage is free to fix, and which of the two fixes each part needs. The split matters — telling someone to
149
+ * atlas textures when their problem is unmerged geometry sends them down a much harder road than they need.
150
+ */
151
+ function describeDrawCallOverage(breakdown, limit, kind) {
152
+ const lines = [
153
+ `${breakdown.drawCalls.toLocaleString()} draw calls exceeds the ${limit} ${kind.replace('_', ' ')} cap `
154
+ + '(one draw call = one glTF mesh primitive, counted once per node that places it — not one per material, '
155
+ + 'and not one per object in your DCC)',
156
+ ];
157
+ if (breakdown.offenders.length > 0) {
158
+ lines.push(' heaviest materials: '
159
+ + breakdown.offenders.map((o) => `${o.material} x${o.drawCalls}`).join(', '));
160
+ }
161
+ if (breakdown.mergeable > 0) {
162
+ lines.push(` ${breakdown.mergeable.toLocaleString()} of these are FREE to remove: the geometry binds only `
163
+ + `${breakdown.boundMaterials} distinct material(s), so parts sharing one can be joined into a single `
164
+ + 'primitive with no visual change (Blender: select them, Ctrl+J, then Merge by Distance). Do NOT join '
165
+ + `parts the runtime moves separately — ${ARTICULATED_PARTS[kind]} — they are resolved by node name.`);
166
+ }
167
+ if (breakdown.boundMaterials > limit) {
168
+ lines.push(` merging alone cannot reach the cap: ${breakdown.boundMaterials} distinct materials is a floor of `
169
+ + `${breakdown.boundMaterials} draws. Atlas the set down to ${limit} or fewer first.`);
170
+ }
171
+ lines.push(' if this item is already published, the server allows a grace window for updates — pass --allow-oversize '
172
+ + 'to send it and let the server decide.');
173
+ return lines.join('\n');
174
+ }
74
175
  function preflightAvatar(buffer, kind = 'avatar') {
75
- const limits = kind === 'avatar' ? exports.AVATAR_LIMITS : exports.WEARABLE_LIMITS;
176
+ const limits = LIMITS_BY_KIND[kind] ?? exports.HOME_ITEM_LIMITS;
76
177
  const summary = (0, glb_1.summarizeGlb)(buffer);
77
178
  const extras = readSceneExtras(buffer);
78
179
  const findings = [];
@@ -88,6 +189,11 @@ function preflightAvatar(buffer, kind = 'avatar') {
88
189
  if (summary.maxTextureEdge > limits.maxTextureEdge) {
89
190
  fail('texture-edge', `largest texture edge ${summary.maxTextureEdge}px exceeds the ${limits.maxTextureEdge}px cap`);
90
191
  }
192
+ const drawCalls = (0, glb_1.measureDrawCalls)(buffer);
193
+ const drawCallLimit = exports.DRAW_CALL_LIMITS[kind] ?? exports.DRAW_CALL_LIMITS.home_item;
194
+ if (!drawCalls.unreadable && drawCalls.drawCalls > drawCallLimit) {
195
+ fail('draw-calls', describeDrawCallOverage(drawCalls, drawCallLimit, kind));
196
+ }
91
197
  if (summary.sizeBytes > limits.maxFileBytes) {
92
198
  const mb = (n) => `${(n / 1024 / 1024).toFixed(1)} MB`;
93
199
  fail('file-size', `${mb(summary.sizeBytes)} exceeds the ${mb(limits.maxFileBytes)} ${kind} cap`);
@@ -1 +1 @@
1
- {"version":3,"file":"avatarPreflight.js","sourceRoot":"","sources":["../../src/character/avatarPreflight.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"avatarPreflight.js","sourceRoot":"","sources":["../../src/character/avatarPreflight.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * DRAW CALLS — what the renderer will submit for this GLB, measured locally.
3
+ *
4
+ * WHY A CREATOR NEEDS THIS BEFORE UPLOADING. Draw calls are the one publish ceiling that no DCC tool displays.
5
+ * Blender shows objects, materials and triangles; a glTF viewer shows nodes and meshes. None of them shows the
6
+ * number the platform actually judges, so a creator can look at a "clean" file in every tool they own and still
7
+ * be refused. Learning that after a 9 MB upload is the round trip this preflight exists to remove.
8
+ *
9
+ * WHY THIS IS A SECOND IMPLEMENTATION, DELIBERATELY. The authoritative copy is `asset-verification.ts` in
10
+ * helix-backend-api, mirrored into universal-item-validator-worker and held there by the `glb-parser-sync` CI
11
+ * check. It cannot import this module, and this module cannot be the one it calls: both of those hosts consume
12
+ * this package from npm, so a measurement added here reaches them only after a release of
13
+ * `@hypersoniclabs/helix-cli` and a lockfile bump in two repositories — which would mean the server could not
14
+ * enforce the budget in the same change that defines it. The server gate is the one that counts; this one
15
+ * exists to be EARLY, and it is pinned to the same numbers by `draw-calls.spec.ts`.
16
+ *
17
+ * If the two ever disagree, the server is right and this is the bug. The body below is a verbatim port; keep
18
+ * it that way.
19
+ */
20
+ /** One material's share of the submissions, for naming worst offenders. */
21
+ export interface DrawCallOffender {
22
+ material: string;
23
+ drawCalls: number;
24
+ triangles: number;
25
+ }
26
+ export interface DrawCallBreakdown {
27
+ /** GPU submissions: mesh primitives, once per node that instantiates the mesh. */
28
+ drawCalls: number;
29
+ /**
30
+ * Distinct materials bound by drawn geometry. This is the FLOOR `drawCalls` can reach by merging geometry
31
+ * alone — below it you must atlas.
32
+ */
33
+ boundMaterials: number;
34
+ /**
35
+ * `drawCalls - boundMaterials`: submissions removable by merging geometry that already shares a material, at
36
+ * zero visual cost. The single most useful number to hand a creator, because it is the part of the overage
37
+ * that costs them nothing to fix.
38
+ */
39
+ mergeable: number;
40
+ /** Materials bound by the most primitives, worst first, capped at 5. */
41
+ offenders: DrawCallOffender[];
42
+ /** True when the JSON chunk could not be read; callers must not gate on a guess. */
43
+ unreadable: boolean;
44
+ }
45
+ /**
46
+ * Count what the renderer will submit for this GLB.
47
+ *
48
+ * Instancing rules, stated because they are the whole difference between this
49
+ * number and a naive primitive count:
50
+ * - A mesh referenced by N nodes is drawn N times. Four wheel nodes sharing one
51
+ * wheel mesh are four draws, and a creator who "deduplicated" them has saved
52
+ * bytes, not batches.
53
+ * - A mesh referenced by NO node is not in the scene graph and is not drawn. It
54
+ * is still shipped bytes, which the file-size ceiling already judges.
55
+ * - `EXT_mesh_gpu_instancing` collapses its node's copies into one submission,
56
+ * which is exactly what per-node counting already yields.
57
+ * - Points and lines still submit; they are counted as draws and carry no
58
+ * triangles.
59
+ */
60
+ export declare function measureDrawCalls(buffer: Buffer): DrawCallBreakdown;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ /**
3
+ * DRAW CALLS — what the renderer will submit for this GLB, measured locally.
4
+ *
5
+ * WHY A CREATOR NEEDS THIS BEFORE UPLOADING. Draw calls are the one publish ceiling that no DCC tool displays.
6
+ * Blender shows objects, materials and triangles; a glTF viewer shows nodes and meshes. None of them shows the
7
+ * number the platform actually judges, so a creator can look at a "clean" file in every tool they own and still
8
+ * be refused. Learning that after a 9 MB upload is the round trip this preflight exists to remove.
9
+ *
10
+ * WHY THIS IS A SECOND IMPLEMENTATION, DELIBERATELY. The authoritative copy is `asset-verification.ts` in
11
+ * helix-backend-api, mirrored into universal-item-validator-worker and held there by the `glb-parser-sync` CI
12
+ * check. It cannot import this module, and this module cannot be the one it calls: both of those hosts consume
13
+ * this package from npm, so a measurement added here reaches them only after a release of
14
+ * `@hypersoniclabs/helix-cli` and a lockfile bump in two repositories — which would mean the server could not
15
+ * enforce the budget in the same change that defines it. The server gate is the one that counts; this one
16
+ * exists to be EARLY, and it is pinned to the same numbers by `draw-calls.spec.ts`.
17
+ *
18
+ * If the two ever disagree, the server is right and this is the bug. The body below is a verbatim port; keep
19
+ * it that way.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.measureDrawCalls = measureDrawCalls;
23
+ const GLB_MAGIC = 0x46546c67;
24
+ const GLB_CHUNK_JSON = 0x4e4f534a;
25
+ /** The glTF JSON chunk, or null when the buffer is not a readable GLB. */
26
+ function readGlbJson(buffer) {
27
+ try {
28
+ if (buffer.length < 20 || buffer.readUInt32LE(0) !== GLB_MAGIC)
29
+ return null;
30
+ const declared = buffer.readUInt32LE(8);
31
+ const end = Math.min(Number.isFinite(declared) && declared > 0 ? declared : buffer.length, buffer.length);
32
+ let offset = 12;
33
+ while (offset + 8 <= end) {
34
+ const length = buffer.readUInt32LE(offset);
35
+ const type = buffer.readUInt32LE(offset + 4);
36
+ const start = offset + 8;
37
+ if (length < 0 || start + length > buffer.length)
38
+ return null;
39
+ if (type === GLB_CHUNK_JSON) {
40
+ const parsed = JSON.parse(buffer.subarray(start, start + length).toString('utf8'));
41
+ return parsed !== null && typeof parsed === 'object'
42
+ ? parsed
43
+ : null;
44
+ }
45
+ offset = start + length + ((4 - (length % 4)) % 4);
46
+ }
47
+ return null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Triangles a primitive contributes, per instance. Non-triangle modes contribute none. */
54
+ function primitiveTriangles(primitive, accessors) {
55
+ const mode = typeof primitive.mode === 'number' ? primitive.mode : 4;
56
+ let vertices = 0;
57
+ if (typeof primitive.indices === 'number') {
58
+ vertices = accessors[primitive.indices]?.count ?? 0;
59
+ }
60
+ else if (typeof primitive.attributes?.POSITION === 'number') {
61
+ vertices = accessors[primitive.attributes.POSITION]?.count ?? 0;
62
+ }
63
+ if (mode === 4)
64
+ return Math.floor(vertices / 3);
65
+ if (mode === 5 || mode === 6)
66
+ return Math.max(0, vertices - 2);
67
+ return 0;
68
+ }
69
+ /** A primitive's material, named for the creator rather than indexed. */
70
+ function primitiveMaterialName(primitive, materials) {
71
+ const index = primitive.material;
72
+ if (typeof index !== 'number')
73
+ return '(no material)';
74
+ const name = materials[index]?.name;
75
+ return typeof name === 'string' && name.length > 0
76
+ ? name
77
+ : `material[${index}]`;
78
+ }
79
+ /**
80
+ * Count what the renderer will submit for this GLB.
81
+ *
82
+ * Instancing rules, stated because they are the whole difference between this
83
+ * number and a naive primitive count:
84
+ * - A mesh referenced by N nodes is drawn N times. Four wheel nodes sharing one
85
+ * wheel mesh are four draws, and a creator who "deduplicated" them has saved
86
+ * bytes, not batches.
87
+ * - A mesh referenced by NO node is not in the scene graph and is not drawn. It
88
+ * is still shipped bytes, which the file-size ceiling already judges.
89
+ * - `EXT_mesh_gpu_instancing` collapses its node's copies into one submission,
90
+ * which is exactly what per-node counting already yields.
91
+ * - Points and lines still submit; they are counted as draws and carry no
92
+ * triangles.
93
+ */
94
+ function measureDrawCalls(buffer) {
95
+ const empty = {
96
+ drawCalls: 0,
97
+ boundMaterials: 0,
98
+ mergeable: 0,
99
+ offenders: [],
100
+ unreadable: true,
101
+ };
102
+ const json = readGlbJson(buffer);
103
+ if (!json)
104
+ return empty;
105
+ const meshes = Array.isArray(json.meshes) ? json.meshes : [];
106
+ const nodes = Array.isArray(json.nodes) ? json.nodes : [];
107
+ const materials = Array.isArray(json.materials)
108
+ ? json.materials
109
+ : [];
110
+ const accessors = Array.isArray(json.accessors)
111
+ ? json.accessors
112
+ : [];
113
+ if (meshes.length === 0)
114
+ return { ...empty, unreadable: false };
115
+ const instantiations = new Array(meshes.length).fill(0);
116
+ let referenced = 0;
117
+ for (const node of nodes) {
118
+ const index = node?.mesh;
119
+ if (typeof index === 'number' && index >= 0 && index < meshes.length) {
120
+ instantiations[index] += 1;
121
+ referenced += 1;
122
+ }
123
+ }
124
+ // A GLB whose nodes reference nothing is either authored without a scene
125
+ // graph or written by a tool we do not model. Judging it as zero draws would
126
+ // wave through the very thing this gate exists to catch, so fall back to the
127
+ // authored primitive count — the count a renderer reaches if anything ever
128
+ // puts these meshes in a scene.
129
+ const fallback = referenced === 0;
130
+ const byMaterial = new Map();
131
+ let drawCalls = 0;
132
+ for (let m = 0; m < meshes.length; m++) {
133
+ const copies = fallback ? 1 : (instantiations[m] ?? 0);
134
+ if (copies === 0)
135
+ continue;
136
+ const primitives = Array.isArray(meshes[m]?.primitives)
137
+ ? meshes[m].primitives
138
+ : [];
139
+ for (const primitive of primitives) {
140
+ drawCalls += copies;
141
+ const name = primitiveMaterialName(primitive, materials);
142
+ const bucket = byMaterial.get(name) ?? { drawCalls: 0, triangles: 0 };
143
+ bucket.drawCalls += copies;
144
+ bucket.triangles += primitiveTriangles(primitive, accessors) * copies;
145
+ byMaterial.set(name, bucket);
146
+ }
147
+ }
148
+ const offenders = [...byMaterial.entries()]
149
+ .map(([material, counts]) => ({ material, ...counts }))
150
+ .sort((a, b) => b.drawCalls - a.drawCalls || b.triangles - a.triangles)
151
+ .slice(0, 5);
152
+ return {
153
+ drawCalls,
154
+ boundMaterials: byMaterial.size,
155
+ mergeable: Math.max(0, drawCalls - byMaterial.size),
156
+ offenders,
157
+ unreadable: false,
158
+ };
159
+ }
160
+ //# sourceMappingURL=draw-calls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw-calls.js","sourceRoot":"","sources":["../../src/glb/draw-calls.ts"],"names":[],"mappings":"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"}
@@ -8,3 +8,4 @@
8
8
  * their host config/logger coupling is inverted into injected arguments.
9
9
  */
10
10
  export { GlbParseError, summarizeGlb, type GlbBoundingBox, type GlbSummary, type GlbTextureInfo, } from './glb-summary';
11
+ export { measureDrawCalls, type DrawCallBreakdown, type DrawCallOffender, } from './draw-calls';
package/dist/glb/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.summarizeGlb = exports.GlbParseError = void 0;
3
+ exports.measureDrawCalls = exports.summarizeGlb = exports.GlbParseError = void 0;
4
4
  /**
5
5
  * `@hypersoniclabs/helix-cli/glb` — shared GLB pipeline modules.
6
6
  *
@@ -13,4 +13,6 @@ exports.summarizeGlb = exports.GlbParseError = void 0;
13
13
  var glb_summary_1 = require("./glb-summary");
14
14
  Object.defineProperty(exports, "GlbParseError", { enumerable: true, get: function () { return glb_summary_1.GlbParseError; } });
15
15
  Object.defineProperty(exports, "summarizeGlb", { enumerable: true, get: function () { return glb_summary_1.summarizeGlb; } });
16
+ var draw_calls_1 = require("./draw-calls");
17
+ Object.defineProperty(exports, "measureDrawCalls", { enumerable: true, get: function () { return draw_calls_1.measureDrawCalls; } });
16
18
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/glb/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,6CAMuB;AALrB,4GAAA,aAAa,OAAA;AACb,2GAAA,YAAY,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/glb/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,6CAMuB;AALrB,4GAAA,aAAa,OAAA;AACb,2GAAA,YAAY,OAAA;AAMd,2CAIsB;AAHpB,8GAAA,gBAAgB,OAAA"}
package/dist/lib.js CHANGED
@@ -116,6 +116,23 @@ Object.defineProperty(exports, "PLACEMENT_REQUEST_FILENAME", { enumerable: true,
116
116
  const bundle_1 = require("./bundle");
117
117
  const package_1 = require("./package");
118
118
  const avatarPreflight_1 = require("./character/avatarPreflight");
119
+ /**
120
+ * The item kinds this process can judge before the upload.
121
+ *
122
+ * A kind absent here is not unchecked — the server checks every kind. It is
123
+ * only unchecked EARLY, which costs a round trip. Adding a kind here means
124
+ * mirroring its server ceilings in `avatarPreflight.ts`, and a mirror that
125
+ * guesses LOOSER numbers than the server is worse than no mirror at all: it
126
+ * prints confident green for a file that will be refused.
127
+ */
128
+ const PREFLIGHT_KINDS = new Set([
129
+ 'avatar',
130
+ 'wearable',
131
+ 'add_on',
132
+ 'home_item',
133
+ 'vehicle',
134
+ 'weapon',
135
+ ]);
119
136
  const ability_1 = require("./ability");
120
137
  const item_1 = require("./item");
121
138
  const achievement_1 = require("./achievement");
@@ -863,10 +880,16 @@ function planItemPublish(opts) {
863
880
  if (sizeWarning)
864
881
  warnings.push(sizeWarning);
865
882
  // ── The publish gate, checked here instead of after the upload ──────────────────────────────────────────
866
- // Every avatar/wearable rejection the backend issues is statically decidable from the file. Uploading first
883
+ // Every rejection the backend issues for these kinds is statically decidable from the file. Uploading first
867
884
  // and learning second costs the creator a round trip to be told a number they could have been told offline —
868
885
  // and `--dry-run` exists precisely so the answer arrives before the network does.
869
- if (opts.kind === 'avatar' || opts.kind === 'wearable') {
886
+ //
887
+ // This used to cover avatars and wearables only, which is why a vehicle learned about its ceilings the
888
+ // expensive way: the one kind whose files are big enough for the round trip to actually hurt was the one
889
+ // kind not checked. The draw-call budget makes that worse if left alone — draw calls are invisible in every
890
+ // authoring tool a creator owns, so without a local check there is no way to see the number before the
891
+ // server states it.
892
+ if (PREFLIGHT_KINDS.has(opts.kind)) {
870
893
  // A preflight that cannot READ the file must not block the publish. `assertLooksLikeGlb` above already
871
894
  // rejects what is not a GLB; past that point an unparseable variant — an extension this summariser does
872
895
  // not know, a container it cannot walk — is the SERVER's call to make, and refusing locally would turn a