lootforge 0.3.0

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.
Files changed (243) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +764 -0
  3. package/bin/lootforge.js +28 -0
  4. package/dist/benchmarks/coarseToFineCost.d.ts +21 -0
  5. package/dist/benchmarks/coarseToFineCost.js +49 -0
  6. package/dist/benchmarks/coarseToFineCost.js.map +1 -0
  7. package/dist/checks/boundaryMetrics.d.ts +12 -0
  8. package/dist/checks/boundaryMetrics.js +102 -0
  9. package/dist/checks/boundaryMetrics.js.map +1 -0
  10. package/dist/checks/candidateScore.d.ts +11 -0
  11. package/dist/checks/candidateScore.js +462 -0
  12. package/dist/checks/candidateScore.js.map +1 -0
  13. package/dist/checks/commandParser.d.ts +5 -0
  14. package/dist/checks/commandParser.js +99 -0
  15. package/dist/checks/commandParser.js.map +1 -0
  16. package/dist/checks/consistencyOutliers.d.ts +42 -0
  17. package/dist/checks/consistencyOutliers.js +156 -0
  18. package/dist/checks/consistencyOutliers.js.map +1 -0
  19. package/dist/checks/imageAcceptance.d.ts +67 -0
  20. package/dist/checks/imageAcceptance.js +967 -0
  21. package/dist/checks/imageAcceptance.js.map +1 -0
  22. package/dist/checks/packInvariants.d.ts +56 -0
  23. package/dist/checks/packInvariants.js +1064 -0
  24. package/dist/checks/packInvariants.js.map +1 -0
  25. package/dist/checks/softAdapters.d.ts +25 -0
  26. package/dist/checks/softAdapters.js +275 -0
  27. package/dist/checks/softAdapters.js.map +1 -0
  28. package/dist/checks/vlmGate.d.ts +8 -0
  29. package/dist/checks/vlmGate.js +200 -0
  30. package/dist/checks/vlmGate.js.map +1 -0
  31. package/dist/cli/commands/atlas.d.ts +5 -0
  32. package/dist/cli/commands/atlas.js +18 -0
  33. package/dist/cli/commands/atlas.js.map +1 -0
  34. package/dist/cli/commands/eval.d.ts +6 -0
  35. package/dist/cli/commands/eval.js +23 -0
  36. package/dist/cli/commands/eval.js.map +1 -0
  37. package/dist/cli/commands/generate.d.ts +18 -0
  38. package/dist/cli/commands/generate.js +66 -0
  39. package/dist/cli/commands/generate.js.map +1 -0
  40. package/dist/cli/commands/init.d.ts +15 -0
  41. package/dist/cli/commands/init.js +146 -0
  42. package/dist/cli/commands/init.js.map +1 -0
  43. package/dist/cli/commands/package.d.ts +6 -0
  44. package/dist/cli/commands/package.js +27 -0
  45. package/dist/cli/commands/package.js.map +1 -0
  46. package/dist/cli/commands/plan.d.ts +16 -0
  47. package/dist/cli/commands/plan.js +49 -0
  48. package/dist/cli/commands/plan.js.map +1 -0
  49. package/dist/cli/commands/process.d.ts +14 -0
  50. package/dist/cli/commands/process.js +29 -0
  51. package/dist/cli/commands/process.js.map +1 -0
  52. package/dist/cli/commands/regenerate.d.ts +29 -0
  53. package/dist/cli/commands/regenerate.js +244 -0
  54. package/dist/cli/commands/regenerate.js.map +1 -0
  55. package/dist/cli/commands/review.d.ts +5 -0
  56. package/dist/cli/commands/review.js +18 -0
  57. package/dist/cli/commands/review.js.map +1 -0
  58. package/dist/cli/commands/select.d.ts +6 -0
  59. package/dist/cli/commands/select.js +21 -0
  60. package/dist/cli/commands/select.js.map +1 -0
  61. package/dist/cli/commands/serve.d.ts +16 -0
  62. package/dist/cli/commands/serve.js +100 -0
  63. package/dist/cli/commands/serve.js.map +1 -0
  64. package/dist/cli/commands/validate.d.ts +17 -0
  65. package/dist/cli/commands/validate.js +108 -0
  66. package/dist/cli/commands/validate.js.map +1 -0
  67. package/dist/cli/index.d.ts +1 -0
  68. package/dist/cli/index.js +157 -0
  69. package/dist/cli/index.js.map +1 -0
  70. package/dist/cli/parseArgs.d.ts +3 -0
  71. package/dist/cli/parseArgs.js +37 -0
  72. package/dist/cli/parseArgs.js.map +1 -0
  73. package/dist/contracts/stageArtifacts.d.ts +4031 -0
  74. package/dist/contracts/stageArtifacts.js +663 -0
  75. package/dist/contracts/stageArtifacts.js.map +1 -0
  76. package/dist/manifest/load.d.ts +3 -0
  77. package/dist/manifest/load.js +50 -0
  78. package/dist/manifest/load.js.map +1 -0
  79. package/dist/manifest/normalize-palette.d.ts +17 -0
  80. package/dist/manifest/normalize-palette.js +235 -0
  81. package/dist/manifest/normalize-palette.js.map +1 -0
  82. package/dist/manifest/normalize-policy.d.ts +48 -0
  83. package/dist/manifest/normalize-policy.js +239 -0
  84. package/dist/manifest/normalize-policy.js.map +1 -0
  85. package/dist/manifest/normalize-prompt.d.ts +14 -0
  86. package/dist/manifest/normalize-prompt.js +73 -0
  87. package/dist/manifest/normalize-prompt.js.map +1 -0
  88. package/dist/manifest/normalize-target.d.ts +49 -0
  89. package/dist/manifest/normalize-target.js +542 -0
  90. package/dist/manifest/normalize-target.js.map +1 -0
  91. package/dist/manifest/schema.d.ts +7570 -0
  92. package/dist/manifest/schema.js +373 -0
  93. package/dist/manifest/schema.js.map +1 -0
  94. package/dist/manifest/semantic-validation.d.ts +4 -0
  95. package/dist/manifest/semantic-validation.js +526 -0
  96. package/dist/manifest/semantic-validation.js.map +1 -0
  97. package/dist/manifest/types.d.ts +263 -0
  98. package/dist/manifest/types.js +2 -0
  99. package/dist/manifest/types.js.map +1 -0
  100. package/dist/manifest/validate.d.ts +12 -0
  101. package/dist/manifest/validate.js +221 -0
  102. package/dist/manifest/validate.js.map +1 -0
  103. package/dist/output/assetPackManifest.d.ts +19 -0
  104. package/dist/output/assetPackManifest.js +20 -0
  105. package/dist/output/assetPackManifest.js.map +1 -0
  106. package/dist/output/catalog.d.ts +60 -0
  107. package/dist/output/catalog.js +107 -0
  108. package/dist/output/catalog.js.map +1 -0
  109. package/dist/output/contactSheet.d.ts +13 -0
  110. package/dist/output/contactSheet.js +124 -0
  111. package/dist/output/contactSheet.js.map +1 -0
  112. package/dist/output/phaserManifest.d.ts +8 -0
  113. package/dist/output/phaserManifest.js +25 -0
  114. package/dist/output/phaserManifest.js.map +1 -0
  115. package/dist/output/pixiManifest.d.ts +8 -0
  116. package/dist/output/pixiManifest.js +37 -0
  117. package/dist/output/pixiManifest.js.map +1 -0
  118. package/dist/output/provenance.d.ts +121 -0
  119. package/dist/output/provenance.js +10 -0
  120. package/dist/output/provenance.js.map +1 -0
  121. package/dist/output/runtimeManifests.d.ts +21 -0
  122. package/dist/output/runtimeManifests.js +82 -0
  123. package/dist/output/runtimeManifests.js.map +1 -0
  124. package/dist/output/unityImportManifest.d.ts +10 -0
  125. package/dist/output/unityImportManifest.js +58 -0
  126. package/dist/output/unityImportManifest.js.map +1 -0
  127. package/dist/output/zip.d.ts +5 -0
  128. package/dist/output/zip.js +68 -0
  129. package/dist/output/zip.js.map +1 -0
  130. package/dist/pipeline/atlas.d.ts +33 -0
  131. package/dist/pipeline/atlas.js +286 -0
  132. package/dist/pipeline/atlas.js.map +1 -0
  133. package/dist/pipeline/eval.d.ts +104 -0
  134. package/dist/pipeline/eval.js +246 -0
  135. package/dist/pipeline/eval.js.map +1 -0
  136. package/dist/pipeline/generate.d.ts +44 -0
  137. package/dist/pipeline/generate.js +1088 -0
  138. package/dist/pipeline/generate.js.map +1 -0
  139. package/dist/pipeline/package.d.ts +18 -0
  140. package/dist/pipeline/package.js +218 -0
  141. package/dist/pipeline/package.js.map +1 -0
  142. package/dist/pipeline/process.d.ts +15 -0
  143. package/dist/pipeline/process.js +776 -0
  144. package/dist/pipeline/process.js.map +1 -0
  145. package/dist/pipeline/review.d.ts +10 -0
  146. package/dist/pipeline/review.js +341 -0
  147. package/dist/pipeline/review.js.map +1 -0
  148. package/dist/pipeline/seamHeal.d.ts +2 -0
  149. package/dist/pipeline/seamHeal.js +70 -0
  150. package/dist/pipeline/seamHeal.js.map +1 -0
  151. package/dist/pipeline/select.d.ts +39 -0
  152. package/dist/pipeline/select.js +79 -0
  153. package/dist/pipeline/select.js.map +1 -0
  154. package/dist/providers/job.d.ts +29 -0
  155. package/dist/providers/job.js +113 -0
  156. package/dist/providers/job.js.map +1 -0
  157. package/dist/providers/localDiffusion.d.ts +28 -0
  158. package/dist/providers/localDiffusion.js +235 -0
  159. package/dist/providers/localDiffusion.js.map +1 -0
  160. package/dist/providers/nano.d.ts +36 -0
  161. package/dist/providers/nano.js +402 -0
  162. package/dist/providers/nano.js.map +1 -0
  163. package/dist/providers/openai.d.ts +37 -0
  164. package/dist/providers/openai.js +378 -0
  165. package/dist/providers/openai.js.map +1 -0
  166. package/dist/providers/policy.d.ts +9 -0
  167. package/dist/providers/policy.js +192 -0
  168. package/dist/providers/policy.js.map +1 -0
  169. package/dist/providers/prompt.d.ts +3 -0
  170. package/dist/providers/prompt.js +63 -0
  171. package/dist/providers/prompt.js.map +1 -0
  172. package/dist/providers/registry.d.ts +24 -0
  173. package/dist/providers/registry.js +92 -0
  174. package/dist/providers/registry.js.map +1 -0
  175. package/dist/providers/runtime.d.ts +15 -0
  176. package/dist/providers/runtime.js +101 -0
  177. package/dist/providers/runtime.js.map +1 -0
  178. package/dist/providers/runtimeConfig.d.ts +20 -0
  179. package/dist/providers/runtimeConfig.js +146 -0
  180. package/dist/providers/runtimeConfig.js.map +1 -0
  181. package/dist/providers/types-core.d.ts +514 -0
  182. package/dist/providers/types-core.js +60 -0
  183. package/dist/providers/types-core.js.map +1 -0
  184. package/dist/providers/types.d.ts +4 -0
  185. package/dist/providers/types.js +5 -0
  186. package/dist/providers/types.js.map +1 -0
  187. package/dist/service/generationRequest.d.ts +58 -0
  188. package/dist/service/generationRequest.js +203 -0
  189. package/dist/service/generationRequest.js.map +1 -0
  190. package/dist/service/providerCapabilities.d.ts +40 -0
  191. package/dist/service/providerCapabilities.js +114 -0
  192. package/dist/service/providerCapabilities.js.map +1 -0
  193. package/dist/service/server.d.ts +31 -0
  194. package/dist/service/server.js +774 -0
  195. package/dist/service/server.js.map +1 -0
  196. package/dist/shared/errors.d.ts +13 -0
  197. package/dist/shared/errors.js +24 -0
  198. package/dist/shared/errors.js.map +1 -0
  199. package/dist/shared/fs.d.ts +6 -0
  200. package/dist/shared/fs.js +30 -0
  201. package/dist/shared/fs.js.map +1 -0
  202. package/dist/shared/image.d.ts +25 -0
  203. package/dist/shared/image.js +136 -0
  204. package/dist/shared/image.js.map +1 -0
  205. package/dist/shared/paths.d.ts +30 -0
  206. package/dist/shared/paths.js +103 -0
  207. package/dist/shared/paths.js.map +1 -0
  208. package/dist/shared/schemas.d.ts +209 -0
  209. package/dist/shared/schemas.js +93 -0
  210. package/dist/shared/schemas.js.map +1 -0
  211. package/dist/shared/typeGuards.d.ts +1 -0
  212. package/dist/shared/typeGuards.js +4 -0
  213. package/dist/shared/typeGuards.js.map +1 -0
  214. package/dist/shared/zod.d.ts +1 -0
  215. package/dist/shared/zod.js +14 -0
  216. package/dist/shared/zod.js.map +1 -0
  217. package/dist/showcase/format.d.ts +9 -0
  218. package/dist/showcase/format.js +61 -0
  219. package/dist/showcase/format.js.map +1 -0
  220. package/dist/showcase/panelRenderer.d.ts +59 -0
  221. package/dist/showcase/panelRenderer.js +294 -0
  222. package/dist/showcase/panelRenderer.js.map +1 -0
  223. package/dist/showcase/releaseConfig.d.ts +233 -0
  224. package/dist/showcase/releaseConfig.js +75 -0
  225. package/dist/showcase/releaseConfig.js.map +1 -0
  226. package/dist/showcase/releaseEvidence.d.ts +25 -0
  227. package/dist/showcase/releaseEvidence.js +540 -0
  228. package/dist/showcase/releaseEvidence.js.map +1 -0
  229. package/dist/showcase/releaseEvidenceSchema.d.ts +1611 -0
  230. package/dist/showcase/releaseEvidenceSchema.js +165 -0
  231. package/dist/showcase/releaseEvidenceSchema.js.map +1 -0
  232. package/dist/showcase/scenarioRenderer.d.ts +19 -0
  233. package/dist/showcase/scenarioRenderer.js +488 -0
  234. package/dist/showcase/scenarioRenderer.js.map +1 -0
  235. package/docs/ADAPTER_CONTRACT.md +141 -0
  236. package/docs/ENGINE_TARGETING.md +86 -0
  237. package/docs/MANIFEST_POLICY_COVERAGE.md +130 -0
  238. package/docs/RELEASE_WORKFLOW.md +117 -0
  239. package/docs/ROADMAP.md +411 -0
  240. package/docs/ROADMAP_ISSUES.md +244 -0
  241. package/docs/SERVICE_MODE.md +137 -0
  242. package/docs/manifest-schema.md +254 -0
  243. package/package.json +70 -0
@@ -0,0 +1,1064 @@
1
+ import { openImage } from "../shared/image.js";
2
+ import { normalizeTargetOutPath, resolvePathWithinDir } from "../shared/paths.js";
3
+ const BYTES_PER_MEGABYTE = 1024 * 1024;
4
+ const SUBJECT_COLOR_DISTANCE_THRESHOLD = 18;
5
+ const SUBJECT_ALPHA_DISTANCE_THRESHOLD = 12;
6
+ const FACING_DESCRIPTOR_GRID_SIZE = 12;
7
+ const FACING_MISMATCH_DELTA_MIN = 0.001;
8
+ const FACING_MISMATCH_RATIO_MIN = 1.01;
9
+ const FACING_ASSIGNMENT_IMPROVEMENT_MIN = 0.005;
10
+ const UPPER_BODY_REGION = { minYRatio: 0, maxYRatio: 0.48 };
11
+ const LOWER_BODY_REGION = { minYRatio: 0.48, maxYRatio: 0.84 };
12
+ const FOOT_REGION = { minYRatio: 0.78, maxYRatio: 1 };
13
+ export async function runPackInvariantChecks(params) {
14
+ const runtimeTargets = params.targets.filter((target) => target.catalogDisabled !== true);
15
+ const runtimeTargetIds = new Set(runtimeTargets.map((target) => target.id));
16
+ const targetsById = new Map(params.targets.map((target) => [target.id, target]));
17
+ const issues = [];
18
+ const targetIssues = [];
19
+ const continuityMetricsByAnimation = {};
20
+ const addIssue = (issue) => {
21
+ const normalizedTargetIds = Array.from(new Set(issue.targetIds));
22
+ if (normalizedTargetIds.length === 0) {
23
+ return;
24
+ }
25
+ const normalizedIssue = {
26
+ ...issue,
27
+ targetIds: normalizedTargetIds,
28
+ };
29
+ issues.push(normalizedIssue);
30
+ for (const targetId of normalizedTargetIds) {
31
+ if (!runtimeTargetIds.has(targetId)) {
32
+ continue;
33
+ }
34
+ targetIssues.push({
35
+ targetId,
36
+ level: normalizedIssue.level,
37
+ code: normalizedIssue.code,
38
+ message: normalizedIssue.message,
39
+ });
40
+ }
41
+ };
42
+ enforceRuntimeOutputUniqueness(runtimeTargets, addIssue);
43
+ const families = collectSpritesheetFamilies(params.targets);
44
+ for (const family of families.values()) {
45
+ await enforceSpritesheetFamilyInvariants({
46
+ family,
47
+ imagesDir: params.imagesDir,
48
+ addIssue,
49
+ continuityMetricsByAnimation,
50
+ runtimeTargetIds,
51
+ targetsById,
52
+ });
53
+ }
54
+ const textureBudgetMetricsByProfile = enforceTextureBudgetByProfile({
55
+ runtimeTargets,
56
+ items: params.items,
57
+ addIssue,
58
+ });
59
+ const metrics = {
60
+ ...(Object.keys(textureBudgetMetricsByProfile).length > 0
61
+ ? { textureBudgetMBByProfile: textureBudgetMetricsByProfile }
62
+ : {}),
63
+ ...(Object.keys(continuityMetricsByAnimation).length > 0
64
+ ? { spritesheetContinuityByAnimation: continuityMetricsByAnimation }
65
+ : {}),
66
+ };
67
+ const hasMetrics = Object.keys(metrics).length > 0;
68
+ const summary = issues.length > 0 || hasMetrics
69
+ ? {
70
+ errors: issues.filter((issue) => issue.level === "error").length,
71
+ warnings: issues.filter((issue) => issue.level === "warning").length,
72
+ issues,
73
+ ...(hasMetrics ? { metrics } : {}),
74
+ }
75
+ : undefined;
76
+ return {
77
+ summary,
78
+ targetIssues,
79
+ };
80
+ }
81
+ function enforceRuntimeOutputUniqueness(runtimeTargets, addIssue) {
82
+ const outPathToTargetIds = new Map();
83
+ for (const target of runtimeTargets) {
84
+ let normalizedOut;
85
+ try {
86
+ normalizedOut = normalizeTargetOutPath(target.out).toLowerCase();
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ const targetIds = outPathToTargetIds.get(normalizedOut) ?? new Set();
92
+ targetIds.add(target.id);
93
+ outPathToTargetIds.set(normalizedOut, targetIds);
94
+ }
95
+ for (const [normalizedOut, targetIds] of outPathToTargetIds) {
96
+ if (targetIds.size < 2) {
97
+ continue;
98
+ }
99
+ const duplicates = Array.from(targetIds).sort((left, right) => left.localeCompare(right));
100
+ addIssue({
101
+ level: "error",
102
+ code: "pack_duplicate_runtime_out",
103
+ message: `Runtime output collision for normalized path "${normalizedOut}" across targets: ${duplicates.join(", ")}.`,
104
+ targetIds: duplicates,
105
+ });
106
+ }
107
+ }
108
+ function collectSpritesheetFamilies(targets) {
109
+ const families = new Map();
110
+ for (const target of targets) {
111
+ const sheetTargetId = target.spritesheet?.sheetTargetId;
112
+ if (!sheetTargetId) {
113
+ continue;
114
+ }
115
+ const family = families.get(sheetTargetId) ?? {
116
+ id: sheetTargetId,
117
+ sheets: [],
118
+ frames: [],
119
+ };
120
+ if (target.spritesheet?.isSheet === true) {
121
+ family.sheets.push(target);
122
+ }
123
+ else {
124
+ family.frames.push(target);
125
+ }
126
+ families.set(sheetTargetId, family);
127
+ }
128
+ return families;
129
+ }
130
+ async function enforceSpritesheetFamilyInvariants(params) {
131
+ const { family } = params;
132
+ const sheetTargets = family.sheets;
133
+ const frameTargets = family.frames;
134
+ if (sheetTargets.length === 0 && frameTargets.length > 0) {
135
+ params.addIssue({
136
+ level: "error",
137
+ code: "spritesheet_missing_sheet_target",
138
+ message: `Spritesheet family "${family.id}" has frame targets but no sheet target.`,
139
+ targetIds: frameTargets.map((target) => target.id),
140
+ });
141
+ return;
142
+ }
143
+ if (sheetTargets.length > 1) {
144
+ params.addIssue({
145
+ level: "error",
146
+ code: "spritesheet_multiple_sheet_targets",
147
+ message: `Spritesheet family "${family.id}" has multiple sheet targets: ${sheetTargets
148
+ .map((target) => target.id)
149
+ .join(", ")}.`,
150
+ targetIds: sheetTargets.map((target) => target.id),
151
+ });
152
+ }
153
+ const sheetTarget = sheetTargets[0];
154
+ if (!sheetTarget) {
155
+ return;
156
+ }
157
+ if (sheetTarget.id !== family.id) {
158
+ params.addIssue({
159
+ level: "error",
160
+ code: "spritesheet_sheet_target_id_mismatch",
161
+ message: `Spritesheet sheet target "${sheetTarget.id}" does not match family id "${family.id}".`,
162
+ targetIds: [sheetTarget.id, ...frameTargets.map((target) => target.id)],
163
+ });
164
+ }
165
+ const sheetTargetId = sheetTarget.id;
166
+ const expectedAtlasGroup = normalizeAtlasGroup(sheetTarget.atlasGroup);
167
+ const mismatchedFrames = frameTargets.filter((frame) => normalizeAtlasGroup(frame.atlasGroup) !== expectedAtlasGroup);
168
+ if (mismatchedFrames.length > 0) {
169
+ params.addIssue({
170
+ level: "error",
171
+ code: "spritesheet_atlas_group_mismatch",
172
+ message: `Spritesheet family "${family.id}" has frame targets outside atlas group "${expectedAtlasGroup ?? "(none)"}".`,
173
+ targetIds: [sheetTargetId, ...mismatchedFrames.map((frame) => frame.id)],
174
+ });
175
+ }
176
+ const expectedAnimationCounts = new Map((sheetTarget.spritesheet?.animations ?? []).map((animation) => [
177
+ animation.name,
178
+ animation.count,
179
+ ]));
180
+ const framesByAnimation = new Map();
181
+ for (const frame of frameTargets) {
182
+ const animationName = frame.spritesheet?.animationName;
183
+ if (!animationName) {
184
+ params.addIssue({
185
+ level: "error",
186
+ code: "spritesheet_frame_missing_animation_name",
187
+ message: `Frame target "${frame.id}" in family "${family.id}" is missing spritesheet.animationName.`,
188
+ targetIds: [sheetTargetId, frame.id],
189
+ });
190
+ continue;
191
+ }
192
+ const list = framesByAnimation.get(animationName) ?? [];
193
+ list.push(frame);
194
+ framesByAnimation.set(animationName, list);
195
+ }
196
+ for (const [animationName, expectedCount] of expectedAnimationCounts) {
197
+ const actualCount = framesByAnimation.get(animationName)?.length ?? 0;
198
+ if (actualCount !== expectedCount) {
199
+ params.addIssue({
200
+ level: "error",
201
+ code: "spritesheet_frame_count_mismatch",
202
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" expected ${expectedCount} frame(s) but found ${actualCount}.`,
203
+ targetIds: [
204
+ sheetTargetId,
205
+ ...(framesByAnimation.get(animationName) ?? []).map((frame) => frame.id),
206
+ ],
207
+ });
208
+ }
209
+ }
210
+ for (const [animationName, frames] of framesByAnimation) {
211
+ if (!expectedAnimationCounts.has(animationName)) {
212
+ params.addIssue({
213
+ level: "error",
214
+ code: "spritesheet_unexpected_animation_frames",
215
+ message: `Spritesheet family "${family.id}" contains frames for unexpected animation "${animationName}".`,
216
+ targetIds: [sheetTargetId, ...frames.map((frame) => frame.id)],
217
+ });
218
+ }
219
+ }
220
+ const inspectedFrames = await inspectSpritesheetFrames({
221
+ frames: frameTargets,
222
+ imagesDir: params.imagesDir,
223
+ sheetTargetId,
224
+ addIssue: params.addIssue,
225
+ runtimeTargetIds: params.runtimeTargetIds,
226
+ });
227
+ if (inspectedFrames.size === 0) {
228
+ return;
229
+ }
230
+ for (const [animationName, frames] of framesByAnimation) {
231
+ const orderedFrames = [...frames].sort((left, right) => {
232
+ const leftIndex = left.spritesheet?.frameIndex ?? Number.MAX_SAFE_INTEGER;
233
+ const rightIndex = right.spritesheet?.frameIndex ?? Number.MAX_SAFE_INTEGER;
234
+ if (leftIndex !== rightIndex) {
235
+ return leftIndex - rightIndex;
236
+ }
237
+ return left.id.localeCompare(right.id);
238
+ });
239
+ if (orderedFrames.length < 2) {
240
+ continue;
241
+ }
242
+ let comparisons = 0;
243
+ let maxSilhouetteDrift = 0;
244
+ let maxAnchorDrift = 0;
245
+ let maxIdentityDrift = 0;
246
+ let maxPoseDrift = 0;
247
+ let maxUpperBodyMotionDrift = 0;
248
+ let maxLowerBodyMotionDrift = 0;
249
+ let maxFootMotionDrift = 0;
250
+ for (let index = 1; index < orderedFrames.length; index += 1) {
251
+ const previous = inspectedFrames.get(orderedFrames[index - 1].id);
252
+ const current = inspectedFrames.get(orderedFrames[index].id);
253
+ if (!previous || !current) {
254
+ continue;
255
+ }
256
+ if (previous.silhouette && current.silhouette) {
257
+ const silhouetteDrift = computeSilhouetteDrift(previous, current);
258
+ maxSilhouetteDrift = Math.max(maxSilhouetteDrift, silhouetteDrift);
259
+ }
260
+ const anchorDrift = computeAnchorDrift(previous, current);
261
+ maxAnchorDrift = Math.max(maxAnchorDrift, anchorDrift);
262
+ const identityDrift = computeIdentityDrift(previous, current);
263
+ maxIdentityDrift = Math.max(maxIdentityDrift, identityDrift);
264
+ const poseDrift = computePoseDrift(previous, current);
265
+ maxPoseDrift = Math.max(maxPoseDrift, poseDrift);
266
+ const upperBodyMotionDrift = computeRegionMotionDrift(previous.upperBodyDescriptor, current.upperBodyDescriptor);
267
+ maxUpperBodyMotionDrift = Math.max(maxUpperBodyMotionDrift, upperBodyMotionDrift);
268
+ const lowerBodyMotionDrift = computeRegionMotionDrift(previous.lowerBodyDescriptor, current.lowerBodyDescriptor);
269
+ maxLowerBodyMotionDrift = Math.max(maxLowerBodyMotionDrift, lowerBodyMotionDrift);
270
+ const footMotionDrift = computeRegionMotionDrift(previous.footDescriptor, current.footDescriptor);
271
+ maxFootMotionDrift = Math.max(maxFootMotionDrift, footMotionDrift);
272
+ comparisons += 1;
273
+ }
274
+ if (comparisons === 0) {
275
+ continue;
276
+ }
277
+ const continuityKey = `${sheetTargetId}:${animationName}`;
278
+ params.continuityMetricsByAnimation[continuityKey] = {
279
+ comparisons,
280
+ maxSilhouetteDrift: Number(maxSilhouetteDrift.toFixed(6)),
281
+ maxAnchorDrift: Number(maxAnchorDrift.toFixed(6)),
282
+ maxIdentityDrift: Number(maxIdentityDrift.toFixed(6)),
283
+ maxPoseDrift: Number(maxPoseDrift.toFixed(6)),
284
+ maxUpperBodyMotionDrift: Number(maxUpperBodyMotionDrift.toFixed(6)),
285
+ maxLowerBodyMotionDrift: Number(maxLowerBodyMotionDrift.toFixed(6)),
286
+ maxFootMotionDrift: Number(maxFootMotionDrift.toFixed(6)),
287
+ };
288
+ const silhouetteThreshold = resolveThresholdValue([
289
+ sheetTarget.spritesheetSilhouetteDriftMax,
290
+ ...orderedFrames.map((frame) => frame.spritesheetSilhouetteDriftMax),
291
+ ]);
292
+ if (typeof silhouetteThreshold === "number" && maxSilhouetteDrift > silhouetteThreshold) {
293
+ params.addIssue({
294
+ level: "error",
295
+ code: "spritesheet_silhouette_drift_exceeded",
296
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" exceeded silhouette drift threshold (${maxSilhouetteDrift.toFixed(4)} > ${silhouetteThreshold.toFixed(4)}).`,
297
+ targetIds: [sheetTargetId, ...orderedFrames.map((frame) => frame.id)],
298
+ metrics: {
299
+ measured: Number(maxSilhouetteDrift.toFixed(6)),
300
+ threshold: Number(silhouetteThreshold.toFixed(6)),
301
+ },
302
+ });
303
+ }
304
+ const anchorThreshold = resolveThresholdValue([
305
+ sheetTarget.spritesheetAnchorDriftMax,
306
+ ...orderedFrames.map((frame) => frame.spritesheetAnchorDriftMax),
307
+ ]);
308
+ if (typeof anchorThreshold === "number" && maxAnchorDrift > anchorThreshold) {
309
+ params.addIssue({
310
+ level: "error",
311
+ code: "spritesheet_anchor_drift_exceeded",
312
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" exceeded anchor drift threshold (${maxAnchorDrift.toFixed(4)} > ${anchorThreshold.toFixed(4)}).`,
313
+ targetIds: [sheetTargetId, ...orderedFrames.map((frame) => frame.id)],
314
+ metrics: {
315
+ measured: Number(maxAnchorDrift.toFixed(6)),
316
+ threshold: Number(anchorThreshold.toFixed(6)),
317
+ },
318
+ });
319
+ }
320
+ const identityThreshold = resolveThresholdValue([
321
+ sheetTarget.spritesheetIdentityDriftMax,
322
+ ...orderedFrames.map((frame) => frame.spritesheetIdentityDriftMax),
323
+ ]);
324
+ if (typeof identityThreshold === "number" && maxIdentityDrift > identityThreshold) {
325
+ params.addIssue({
326
+ level: "error",
327
+ code: "spritesheet_identity_drift_exceeded",
328
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" exceeded identity drift threshold (${maxIdentityDrift.toFixed(4)} > ${identityThreshold.toFixed(4)}).`,
329
+ targetIds: [sheetTargetId, ...orderedFrames.map((frame) => frame.id)],
330
+ metrics: {
331
+ measured: Number(maxIdentityDrift.toFixed(6)),
332
+ threshold: Number(identityThreshold.toFixed(6)),
333
+ },
334
+ });
335
+ }
336
+ const poseThreshold = resolveThresholdValue([
337
+ sheetTarget.spritesheetPoseDriftMax,
338
+ ...orderedFrames.map((frame) => frame.spritesheetPoseDriftMax),
339
+ ]);
340
+ if (typeof poseThreshold === "number" && maxPoseDrift > poseThreshold) {
341
+ params.addIssue({
342
+ level: "error",
343
+ code: "spritesheet_pose_drift_exceeded",
344
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" exceeded pose drift threshold (${maxPoseDrift.toFixed(4)} > ${poseThreshold.toFixed(4)}).`,
345
+ targetIds: [sheetTargetId, ...orderedFrames.map((frame) => frame.id)],
346
+ metrics: {
347
+ measured: Number(maxPoseDrift.toFixed(6)),
348
+ threshold: Number(poseThreshold.toFixed(6)),
349
+ },
350
+ });
351
+ }
352
+ const walkMotionViolation = resolveWalkCycleMotionViolation({
353
+ animationName,
354
+ sheetTarget,
355
+ orderedFrames,
356
+ maxUpperBodyMotionDrift,
357
+ maxLowerBodyMotionDrift,
358
+ maxFootMotionDrift,
359
+ });
360
+ if (walkMotionViolation) {
361
+ params.addIssue({
362
+ level: "error",
363
+ code: "spritesheet_walk_cycle_motion_too_low",
364
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" does not read as a walk cycle: ${walkMotionViolation.reason}.`,
365
+ targetIds: [sheetTargetId, ...orderedFrames.map((frame) => frame.id)],
366
+ metrics: walkMotionViolation.metrics,
367
+ });
368
+ }
369
+ const expectedFacing = resolveExpectedFacingDirection(sheetTarget, orderedFrames, animationName);
370
+ if (!expectedFacing) {
371
+ continue;
372
+ }
373
+ const animationInspections = orderedFrames
374
+ .map((frame) => inspectedFrames.get(frame.id))
375
+ .filter((inspection) => inspection !== undefined);
376
+ const facingConsistency = resolveFacingConsistencyViolation(animationInspections);
377
+ if (facingConsistency) {
378
+ params.addIssue({
379
+ level: "error",
380
+ code: "spritesheet_facing_direction_inconsistent",
381
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" contains mirrored-facing frames despite declared facing "${expectedFacing}".`,
382
+ targetIds: [sheetTargetId, ...facingConsistency.mismatchedFrameIds],
383
+ metrics: {
384
+ improvement: Number(facingConsistency.improvement.toFixed(6)),
385
+ mismatchedFrames: facingConsistency.mismatchedFrameIds.length,
386
+ },
387
+ });
388
+ }
389
+ const referenceTargets = resolveFacingReferenceTargets({
390
+ frames: orderedFrames,
391
+ sheetTarget,
392
+ targetsById: params.targetsById,
393
+ expectedFacing,
394
+ });
395
+ if (referenceTargets.length === 0) {
396
+ continue;
397
+ }
398
+ const referenceInspections = (await Promise.all(referenceTargets.map((target) => inspectTargetImage(target, params.imagesDir).catch(() => null)))).filter((inspection) => inspection !== null);
399
+ const facingMismatch = resolveFacingReferenceMismatch({
400
+ frames: animationInspections,
401
+ references: referenceInspections,
402
+ });
403
+ if (facingMismatch) {
404
+ params.addIssue({
405
+ level: "error",
406
+ code: "spritesheet_facing_direction_mismatch",
407
+ message: `Spritesheet animation "${animationName}" in family "${family.id}" drifted away from the declared "${expectedFacing}" facing relative to its reference sprite.`,
408
+ targetIds: [sheetTargetId, ...facingMismatch.mismatchedFrameIds],
409
+ metrics: {
410
+ maxMirrorAdvantage: Number(facingMismatch.maxMirrorAdvantage.toFixed(6)),
411
+ mismatchedFrames: facingMismatch.mismatchedFrameIds.length,
412
+ },
413
+ });
414
+ }
415
+ }
416
+ }
417
+ async function inspectSpritesheetFrames(params) {
418
+ const inspected = new Map();
419
+ for (const frame of params.frames) {
420
+ try {
421
+ normalizeTargetOutPath(frame.out);
422
+ }
423
+ catch (error) {
424
+ params.addIssue({
425
+ level: "warning",
426
+ code: "spritesheet_frame_path_invalid",
427
+ message: `Unable to normalize frame output path for "${frame.id}": ${error instanceof Error ? error.message : String(error)}`,
428
+ targetIds: [params.sheetTargetId, frame.id],
429
+ });
430
+ continue;
431
+ }
432
+ try {
433
+ const inspection = await inspectTargetImage(frame, params.imagesDir);
434
+ inspected.set(frame.id, inspection);
435
+ }
436
+ catch (error) {
437
+ params.addIssue({
438
+ level: "warning",
439
+ code: "spritesheet_frame_image_unavailable",
440
+ message: `Unable to inspect frame image for "${frame.id}": ${error instanceof Error ? error.message : String(error)}`,
441
+ targetIds: [params.sheetTargetId, frame.id].filter((targetId) => params.runtimeTargetIds.has(targetId)),
442
+ });
443
+ }
444
+ }
445
+ return inspected;
446
+ }
447
+ async function inspectTargetImage(target, imagesDir) {
448
+ const normalizedOut = normalizeTargetOutPath(target.out);
449
+ const imagePath = resolvePathWithinDir(imagesDir, normalizedOut, `target output for "${target.id}"`);
450
+ const raw = await openImage(imagePath, "qa")
451
+ .ensureAlpha()
452
+ .raw()
453
+ .toBuffer({ resolveWithObject: true });
454
+ const subjectMask = extractSubjectMask(raw.data, raw.info.channels, raw.info.width, raw.info.height);
455
+ const silhouette = extractMaskSilhouette(subjectMask, raw.info.width, raw.info.height);
456
+ return {
457
+ frame: target,
458
+ width: raw.info.width,
459
+ height: raw.info.height,
460
+ silhouette,
461
+ colorHistogram: computeVisibleColorHistogram(raw.data, raw.info.channels, subjectMask),
462
+ facingDescriptor: computeFacingDescriptor(raw.data, raw.info.channels, raw.info.width, raw.info.height, subjectMask, silhouette),
463
+ upperBodyDescriptor: computeMotionRegionDescriptor(raw.data, raw.info.channels, raw.info.width, raw.info.height, subjectMask, silhouette, UPPER_BODY_REGION),
464
+ lowerBodyDescriptor: computeMotionRegionDescriptor(raw.data, raw.info.channels, raw.info.width, raw.info.height, subjectMask, silhouette, LOWER_BODY_REGION),
465
+ footDescriptor: computeMotionRegionDescriptor(raw.data, raw.info.channels, raw.info.width, raw.info.height, subjectMask, silhouette, FOOT_REGION),
466
+ };
467
+ }
468
+ function extractSubjectMask(raw, channels, width, height) {
469
+ const mask = new Uint8Array(width * height);
470
+ if (channels < 4 || width <= 0 || height <= 0) {
471
+ return mask;
472
+ }
473
+ const background = estimateBackgroundColor(raw, channels, width, height);
474
+ let subjectPixels = 0;
475
+ for (let y = 0; y < height; y += 1) {
476
+ for (let x = 0; x < width; x += 1) {
477
+ const index = (y * width + x) * channels;
478
+ const alpha = raw[index + 3];
479
+ if (alpha <= 0) {
480
+ continue;
481
+ }
482
+ const colorDistance = Math.sqrt((raw[index] - background.r) ** 2 +
483
+ (raw[index + 1] - background.g) ** 2 +
484
+ (raw[index + 2] - background.b) ** 2);
485
+ const alphaDistance = Math.abs(alpha - background.a);
486
+ if (colorDistance > SUBJECT_COLOR_DISTANCE_THRESHOLD ||
487
+ alphaDistance > SUBJECT_ALPHA_DISTANCE_THRESHOLD) {
488
+ mask[y * width + x] = 1;
489
+ subjectPixels += 1;
490
+ }
491
+ }
492
+ }
493
+ if (subjectPixels > 0) {
494
+ return mask;
495
+ }
496
+ for (let y = 0; y < height; y += 1) {
497
+ for (let x = 0; x < width; x += 1) {
498
+ const index = (y * width + x) * channels;
499
+ if (raw[index + 3] > 0) {
500
+ mask[y * width + x] = 1;
501
+ }
502
+ }
503
+ }
504
+ return mask;
505
+ }
506
+ function estimateBackgroundColor(raw, channels, width, height) {
507
+ const samplePoints = [
508
+ [0, 0],
509
+ [width - 1, 0],
510
+ [0, height - 1],
511
+ [width - 1, height - 1],
512
+ [Math.floor(width / 2), 0],
513
+ [Math.floor(width / 2), height - 1],
514
+ [0, Math.floor(height / 2)],
515
+ [width - 1, Math.floor(height / 2)],
516
+ ];
517
+ let r = 0;
518
+ let g = 0;
519
+ let b = 0;
520
+ let a = 0;
521
+ for (const [x, y] of samplePoints) {
522
+ const index = (y * width + x) * channels;
523
+ r += raw[index];
524
+ g += raw[index + 1];
525
+ b += raw[index + 2];
526
+ a += raw[index + 3];
527
+ }
528
+ return {
529
+ r: r / samplePoints.length,
530
+ g: g / samplePoints.length,
531
+ b: b / samplePoints.length,
532
+ a: a / samplePoints.length,
533
+ };
534
+ }
535
+ function extractMaskSilhouette(mask, width, height) {
536
+ if (width <= 0 || height <= 0) {
537
+ return null;
538
+ }
539
+ let minX = Number.POSITIVE_INFINITY;
540
+ let minY = Number.POSITIVE_INFINITY;
541
+ let maxX = Number.NEGATIVE_INFINITY;
542
+ let maxY = Number.NEGATIVE_INFINITY;
543
+ let area = 0;
544
+ let sumX = 0;
545
+ let sumY = 0;
546
+ let sumXX = 0;
547
+ let sumYY = 0;
548
+ let sumXY = 0;
549
+ for (let y = 0; y < height; y += 1) {
550
+ for (let x = 0; x < width; x += 1) {
551
+ const index = y * width + x;
552
+ if (mask[index] === 0) {
553
+ continue;
554
+ }
555
+ area += 1;
556
+ sumX += x;
557
+ sumY += y;
558
+ sumXX += x * x;
559
+ sumYY += y * y;
560
+ sumXY += x * y;
561
+ if (x < minX)
562
+ minX = x;
563
+ if (y < minY)
564
+ minY = y;
565
+ if (x > maxX)
566
+ maxX = x;
567
+ if (y > maxY)
568
+ maxY = y;
569
+ }
570
+ }
571
+ if (area === 0) {
572
+ return null;
573
+ }
574
+ const meanX = sumX / area;
575
+ const meanY = sumY / area;
576
+ const covarianceXX = sumXX / area - meanX * meanX;
577
+ const covarianceYY = sumYY / area - meanY * meanY;
578
+ const covarianceXY = sumXY / area - meanX * meanY;
579
+ const orientationRadians = 0.5 * Math.atan2(2 * covarianceXY, covarianceXX - covarianceYY);
580
+ return {
581
+ minX,
582
+ minY,
583
+ maxX,
584
+ maxY,
585
+ area,
586
+ centerX: (minX + maxX) / 2,
587
+ centerY: (minY + maxY) / 2,
588
+ orientationRadians,
589
+ };
590
+ }
591
+ function computeSilhouetteDrift(left, right) {
592
+ if (!left.silhouette || !right.silhouette) {
593
+ return 0;
594
+ }
595
+ const diagonal = Math.max(Math.hypot(left.width, left.height), Math.hypot(right.width, right.height), 1);
596
+ const centerDelta = Math.hypot(left.silhouette.centerX - right.silhouette.centerX, left.silhouette.centerY - right.silhouette.centerY) / diagonal;
597
+ const areaDelta = Math.abs(left.silhouette.area - right.silhouette.area) /
598
+ Math.max(left.silhouette.area, right.silhouette.area, 1);
599
+ return (centerDelta + areaDelta) / 2;
600
+ }
601
+ function computeAnchorDrift(left, right) {
602
+ if (!left.silhouette || !right.silhouette) {
603
+ return 0;
604
+ }
605
+ const leftAnchor = resolveAnchorPoint(left);
606
+ const rightAnchor = resolveAnchorPoint(right);
607
+ const diagonal = Math.max(Math.hypot(left.width, left.height), Math.hypot(right.width, right.height), 1);
608
+ const leftVectorX = leftAnchor.x - left.silhouette.centerX;
609
+ const leftVectorY = leftAnchor.y - left.silhouette.centerY;
610
+ const rightVectorX = rightAnchor.x - right.silhouette.centerX;
611
+ const rightVectorY = rightAnchor.y - right.silhouette.centerY;
612
+ return Math.hypot(leftVectorX - rightVectorX, leftVectorY - rightVectorY) / diagonal;
613
+ }
614
+ function computeIdentityDrift(left, right) {
615
+ const distance = histogramDistance(left.colorHistogram, right.colorHistogram);
616
+ return Math.max(0, Math.min(1, distance / 2));
617
+ }
618
+ function computePoseDrift(left, right) {
619
+ if (!left.silhouette || !right.silhouette) {
620
+ return 0;
621
+ }
622
+ const delta = angularDistance(left.silhouette.orientationRadians, right.silhouette.orientationRadians);
623
+ return Math.max(0, Math.min(1, delta / Math.PI));
624
+ }
625
+ function computeRegionMotionDrift(left, right) {
626
+ if (!left || !right) {
627
+ return 0;
628
+ }
629
+ return computeFacingDescriptorDistance(left, right);
630
+ }
631
+ function resolveWalkCycleMotionViolation(params) {
632
+ if (params.animationName !== "walk") {
633
+ return null;
634
+ }
635
+ const lowerBodyMotionMin = resolveThresholdValue([
636
+ params.sheetTarget.spritesheetWalkLowerBodyMotionMin,
637
+ ...params.orderedFrames.map((frame) => frame.spritesheetWalkLowerBodyMotionMin),
638
+ ]);
639
+ const footMotionMin = resolveThresholdValue([
640
+ params.sheetTarget.spritesheetWalkFootMotionMin,
641
+ ...params.orderedFrames.map((frame) => frame.spritesheetWalkFootMotionMin),
642
+ ]);
643
+ const footToUpperMotionRatioMin = resolveThresholdValue([
644
+ params.sheetTarget.spritesheetWalkFootToUpperMotionRatioMin,
645
+ ...params.orderedFrames.map((frame) => frame.spritesheetWalkFootToUpperMotionRatioMin),
646
+ ]);
647
+ if (typeof lowerBodyMotionMin !== "number" &&
648
+ typeof footMotionMin !== "number" &&
649
+ typeof footToUpperMotionRatioMin !== "number") {
650
+ return null;
651
+ }
652
+ const footToUpperMotionRatio = params.maxFootMotionDrift / Math.max(params.maxUpperBodyMotionDrift, 1e-6);
653
+ const reasons = [];
654
+ const metrics = {
655
+ measuredUpperBodyMotion: Number(params.maxUpperBodyMotionDrift.toFixed(6)),
656
+ measuredLowerBodyMotion: Number(params.maxLowerBodyMotionDrift.toFixed(6)),
657
+ measuredFootMotion: Number(params.maxFootMotionDrift.toFixed(6)),
658
+ measuredFootToUpperRatio: Number(footToUpperMotionRatio.toFixed(6)),
659
+ };
660
+ if (typeof lowerBodyMotionMin === "number" &&
661
+ params.maxLowerBodyMotionDrift < lowerBodyMotionMin) {
662
+ reasons.push(`lower-body motion stayed too small (${params.maxLowerBodyMotionDrift.toFixed(4)} < ${lowerBodyMotionMin.toFixed(4)})`);
663
+ metrics.requiredLowerBodyMotion = Number(lowerBodyMotionMin.toFixed(6));
664
+ }
665
+ if (typeof footMotionMin === "number" && params.maxFootMotionDrift < footMotionMin) {
666
+ reasons.push(`foot motion stayed too small (${params.maxFootMotionDrift.toFixed(4)} < ${footMotionMin.toFixed(4)})`);
667
+ metrics.requiredFootMotion = Number(footMotionMin.toFixed(6));
668
+ }
669
+ if (typeof footToUpperMotionRatioMin === "number" &&
670
+ footToUpperMotionRatio < footToUpperMotionRatioMin) {
671
+ reasons.push(`foot motion was not meaningfully stronger than upper-body motion (${footToUpperMotionRatio.toFixed(4)} < ${footToUpperMotionRatioMin.toFixed(4)})`);
672
+ metrics.requiredFootToUpperRatio = Number(footToUpperMotionRatioMin.toFixed(6));
673
+ }
674
+ if (reasons.length === 0) {
675
+ return null;
676
+ }
677
+ return {
678
+ reason: reasons.join("; "),
679
+ metrics,
680
+ };
681
+ }
682
+ function resolveExpectedFacingDirection(sheetTarget, orderedFrames, animationName) {
683
+ const animationFacing = sheetTarget.spritesheet?.animations?.find((animation) => animation.name === animationName)?.facing;
684
+ if (animationFacing) {
685
+ return animationFacing;
686
+ }
687
+ for (const frame of orderedFrames) {
688
+ if (frame.spritesheet?.facing) {
689
+ return frame.spritesheet.facing;
690
+ }
691
+ }
692
+ return sheetTarget.facing;
693
+ }
694
+ function resolveFacingReferenceTargets(params) {
695
+ const orderedIds = [
696
+ ...(params.sheetTarget.styleReferenceFrom ?? []),
697
+ ...(params.sheetTarget.dependsOn ?? []),
698
+ ...params.frames.flatMap((frame) => [
699
+ ...(frame.styleReferenceFrom ?? []),
700
+ ...(frame.dependsOn ?? []),
701
+ ]),
702
+ ];
703
+ const references = [];
704
+ const seen = new Set();
705
+ for (const targetId of orderedIds) {
706
+ if (seen.has(targetId)) {
707
+ continue;
708
+ }
709
+ seen.add(targetId);
710
+ const target = params.targetsById.get(targetId);
711
+ if (target?.facing === params.expectedFacing && isExplicitFacingReferenceTarget(target)) {
712
+ references.push(target);
713
+ }
714
+ }
715
+ return references;
716
+ }
717
+ function resolveFacingConsistencyViolation(frames) {
718
+ const comparableFrames = frames.filter(hasFacingDescriptor);
719
+ if (comparableFrames.length < 2) {
720
+ return null;
721
+ }
722
+ const pairwise = buildPairwiseFacingDistances(comparableFrames);
723
+ const assignment = resolveBestFacingAssignment(pairwise);
724
+ if (!assignment || assignment.improvement < FACING_ASSIGNMENT_IMPROVEMENT_MIN) {
725
+ return null;
726
+ }
727
+ return {
728
+ mismatchedFrameIds: comparableFrames
729
+ .filter((_, index) => assignment.bits[index] === 1)
730
+ .map((frame) => frame.frame.id),
731
+ improvement: assignment.improvement,
732
+ };
733
+ }
734
+ function buildPairwiseFacingDistances(frames) {
735
+ return frames.map((left, leftIndex) => frames.map((right, rightIndex) => {
736
+ if (leftIndex === rightIndex) {
737
+ return { direct: 0, mirrored: 0 };
738
+ }
739
+ const leftDescriptor = left.facingDescriptor;
740
+ const rightDescriptor = right.facingDescriptor;
741
+ if (!leftDescriptor || !rightDescriptor) {
742
+ return { direct: Number.POSITIVE_INFINITY, mirrored: Number.POSITIVE_INFINITY };
743
+ }
744
+ return {
745
+ direct: computeFacingDescriptorDistance(leftDescriptor, rightDescriptor),
746
+ mirrored: computeFacingDescriptorDistance(leftDescriptor, flipFacingDescriptor(rightDescriptor)),
747
+ };
748
+ }));
749
+ }
750
+ function resolveBestFacingAssignment(pairwise) {
751
+ const frameCount = pairwise.length;
752
+ if (frameCount < 2) {
753
+ return null;
754
+ }
755
+ const baselineBits = new Array(frameCount).fill(0);
756
+ const baselineCost = computeFacingAssignmentCost(baselineBits, pairwise);
757
+ let bestBits = baselineBits;
758
+ let bestCost = baselineCost;
759
+ if (frameCount <= 12) {
760
+ const variants = 1 << (frameCount - 1);
761
+ for (let mask = 1; mask < variants; mask += 1) {
762
+ const bits = new Array(frameCount).fill(0);
763
+ for (let index = 1; index < frameCount; index += 1) {
764
+ bits[index] = (mask >> (index - 1)) & 1;
765
+ }
766
+ const cost = computeFacingAssignmentCost(bits, pairwise);
767
+ if (cost < bestCost) {
768
+ bestBits = bits;
769
+ bestCost = cost;
770
+ }
771
+ }
772
+ }
773
+ else {
774
+ const bits = new Array(frameCount).fill(0);
775
+ for (let index = 1; index < frameCount; index += 1) {
776
+ const distances = pairwise[0][index];
777
+ if (distances.mirrored < distances.direct) {
778
+ bits[index] = 1;
779
+ }
780
+ }
781
+ const cost = computeFacingAssignmentCost(bits, pairwise);
782
+ if (cost < bestCost) {
783
+ bestBits = bits;
784
+ bestCost = cost;
785
+ }
786
+ }
787
+ if (bestBits.every((bit) => bit === 0)) {
788
+ return null;
789
+ }
790
+ return {
791
+ bits: bestBits,
792
+ improvement: baselineCost - bestCost,
793
+ };
794
+ }
795
+ function computeFacingAssignmentCost(bits, pairwise) {
796
+ let total = 0;
797
+ for (let leftIndex = 0; leftIndex < bits.length; leftIndex += 1) {
798
+ for (let rightIndex = leftIndex + 1; rightIndex < bits.length; rightIndex += 1) {
799
+ const distances = pairwise[leftIndex][rightIndex];
800
+ total += bits[leftIndex] === bits[rightIndex] ? distances.direct : distances.mirrored;
801
+ }
802
+ }
803
+ return total;
804
+ }
805
+ function resolveFacingReferenceMismatch(params) {
806
+ const comparableFrames = params.frames.filter(hasFacingDescriptor);
807
+ const comparableReferences = params.references.filter(hasFacingDescriptor);
808
+ if (comparableFrames.length === 0 || comparableReferences.length === 0) {
809
+ return null;
810
+ }
811
+ const mismatchedFrameIds = [];
812
+ let maxMirrorAdvantage = 0;
813
+ for (const frame of comparableFrames) {
814
+ const descriptor = frame.facingDescriptor;
815
+ let bestDirect = Number.POSITIVE_INFINITY;
816
+ let bestMirrored = Number.POSITIVE_INFINITY;
817
+ for (const reference of comparableReferences) {
818
+ const referenceDescriptor = reference.facingDescriptor;
819
+ bestDirect = Math.min(bestDirect, computeFacingDescriptorDistance(referenceDescriptor, descriptor));
820
+ bestMirrored = Math.min(bestMirrored, computeFacingDescriptorDistance(referenceDescriptor, flipFacingDescriptor(descriptor)));
821
+ }
822
+ const mirrorAdvantage = bestDirect - bestMirrored;
823
+ maxMirrorAdvantage = Math.max(maxMirrorAdvantage, mirrorAdvantage);
824
+ if (bestDirect > bestMirrored + FACING_MISMATCH_DELTA_MIN &&
825
+ bestDirect / Math.max(bestMirrored, 1e-6) > FACING_MISMATCH_RATIO_MIN) {
826
+ mismatchedFrameIds.push(frame.frame.id);
827
+ }
828
+ }
829
+ if (mismatchedFrameIds.length === 0) {
830
+ return null;
831
+ }
832
+ return {
833
+ mismatchedFrameIds,
834
+ maxMirrorAdvantage,
835
+ };
836
+ }
837
+ function hasFacingDescriptor(frame) {
838
+ return frame.facingDescriptor !== null;
839
+ }
840
+ function isExplicitFacingReferenceTarget(target) {
841
+ const promptText = [
842
+ target.promptSpec.primary,
843
+ target.promptSpec.composition,
844
+ target.promptSpec.constraints,
845
+ ]
846
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
847
+ .join(" ");
848
+ if (promptText.length === 0) {
849
+ return false;
850
+ }
851
+ return /\b(left-facing|right-facing|facing left|facing right|side[- ]profile|side view|profile view)\b/i.test(promptText);
852
+ }
853
+ function computeFacingDescriptor(raw, channels, width, height, subjectMask, silhouette) {
854
+ // TODO(agent): this uses shape/luminance heuristics; replace with a semantic pose/facing
855
+ // classifier if showcase captures still slip through or produce false positives.
856
+ return computeGridDescriptor(raw, channels, width, height, subjectMask, silhouette, {
857
+ minYRatio: 0,
858
+ maxYRatio: 1,
859
+ });
860
+ }
861
+ function computeMotionRegionDescriptor(raw, channels, width, height, subjectMask, silhouette, region) {
862
+ return computeGridDescriptor(raw, channels, width, height, subjectMask, silhouette, region);
863
+ }
864
+ function computeGridDescriptor(raw, channels, width, height, subjectMask, silhouette, region) {
865
+ if (!silhouette || width <= 0 || height <= 0) {
866
+ return null;
867
+ }
868
+ const bboxWidth = Math.max(1, silhouette.maxX - silhouette.minX + 1);
869
+ const bboxHeight = Math.max(1, silhouette.maxY - silhouette.minY + 1);
870
+ const regionMinY = Math.min(silhouette.maxY, silhouette.minY + Math.floor((bboxHeight - 1) * clamp01(region.minYRatio)));
871
+ const regionMaxY = Math.max(regionMinY, Math.min(silhouette.maxY, silhouette.minY + Math.ceil(bboxHeight * clamp01(region.maxYRatio)) - 1));
872
+ const regionHeight = Math.max(1, regionMaxY - regionMinY + 1);
873
+ const alphaGrid = new Array(FACING_DESCRIPTOR_GRID_SIZE ** 2).fill(0);
874
+ const luminanceGrid = new Array(FACING_DESCRIPTOR_GRID_SIZE ** 2).fill(0);
875
+ const cellCounts = new Array(FACING_DESCRIPTOR_GRID_SIZE ** 2).fill(0);
876
+ let subjectPixels = 0;
877
+ for (let y = regionMinY; y <= regionMaxY; y += 1) {
878
+ for (let x = silhouette.minX; x <= silhouette.maxX; x += 1) {
879
+ if (subjectMask[y * width + x] === 0) {
880
+ continue;
881
+ }
882
+ const gridX = Math.min(FACING_DESCRIPTOR_GRID_SIZE - 1, Math.floor(((x - silhouette.minX) * FACING_DESCRIPTOR_GRID_SIZE) / bboxWidth));
883
+ const gridY = Math.min(FACING_DESCRIPTOR_GRID_SIZE - 1, Math.floor(((y - regionMinY) * FACING_DESCRIPTOR_GRID_SIZE) / regionHeight));
884
+ const gridIndex = gridY * FACING_DESCRIPTOR_GRID_SIZE + gridX;
885
+ const rawIndex = (y * width + x) * channels;
886
+ alphaGrid[gridIndex] += 1;
887
+ luminanceGrid[gridIndex] +=
888
+ 0.2126 * raw[rawIndex] + 0.7152 * raw[rawIndex + 1] + 0.0722 * raw[rawIndex + 2];
889
+ cellCounts[gridIndex] += 1;
890
+ subjectPixels += 1;
891
+ }
892
+ }
893
+ if (subjectPixels === 0) {
894
+ return null;
895
+ }
896
+ return {
897
+ gridSize: FACING_DESCRIPTOR_GRID_SIZE,
898
+ alphaGrid: alphaGrid.map((value) => value / subjectPixels),
899
+ luminanceGrid: luminanceGrid.map((value, index) => cellCounts[index] > 0 ? value / cellCounts[index] / 255 : 0),
900
+ };
901
+ }
902
+ function flipFacingDescriptor(descriptor) {
903
+ const alphaGrid = new Array(descriptor.alphaGrid.length).fill(0);
904
+ const luminanceGrid = new Array(descriptor.luminanceGrid.length).fill(0);
905
+ for (let y = 0; y < descriptor.gridSize; y += 1) {
906
+ for (let x = 0; x < descriptor.gridSize; x += 1) {
907
+ const sourceIndex = y * descriptor.gridSize + (descriptor.gridSize - 1 - x);
908
+ const targetIndex = y * descriptor.gridSize + x;
909
+ alphaGrid[targetIndex] = descriptor.alphaGrid[sourceIndex] ?? 0;
910
+ luminanceGrid[targetIndex] = descriptor.luminanceGrid[sourceIndex] ?? 0;
911
+ }
912
+ }
913
+ return {
914
+ gridSize: descriptor.gridSize,
915
+ alphaGrid,
916
+ luminanceGrid,
917
+ };
918
+ }
919
+ function computeFacingDescriptorDistance(left, right) {
920
+ const length = Math.max(left.alphaGrid.length, right.alphaGrid.length);
921
+ let distance = 0;
922
+ for (let index = 0; index < length; index += 1) {
923
+ distance +=
924
+ Math.abs((left.alphaGrid[index] ?? 0) - (right.alphaGrid[index] ?? 0)) * 0.7 +
925
+ Math.abs((left.luminanceGrid[index] ?? 0) - (right.luminanceGrid[index] ?? 0)) * 0.3;
926
+ }
927
+ return distance / Math.max(length, 1);
928
+ }
929
+ function angularDistance(a, b) {
930
+ let delta = Math.abs(a - b);
931
+ while (delta > Math.PI) {
932
+ delta -= Math.PI;
933
+ }
934
+ return Math.abs(delta);
935
+ }
936
+ function computeVisibleColorHistogram(raw, channels, subjectMask, bins = 8) {
937
+ const histogram = new Array(bins * bins * bins).fill(0);
938
+ let visibleCount = 0;
939
+ const channelToBin = (value) => Math.max(0, Math.min(bins - 1, Math.floor((value / 256) * bins)));
940
+ for (let index = 0; index < raw.length; index += channels) {
941
+ if (subjectMask[index / channels] === 0) {
942
+ continue;
943
+ }
944
+ const rBin = channelToBin(raw[index]);
945
+ const gBin = channelToBin(raw[index + 1]);
946
+ const bBin = channelToBin(raw[index + 2]);
947
+ const histogramIndex = rBin * bins * bins + gBin * bins + bBin;
948
+ histogram[histogramIndex] += 1;
949
+ visibleCount += 1;
950
+ }
951
+ if (visibleCount === 0) {
952
+ return histogram;
953
+ }
954
+ return histogram.map((value) => value / visibleCount);
955
+ }
956
+ function histogramDistance(left, right) {
957
+ const length = Math.max(left.length, right.length);
958
+ let distance = 0;
959
+ for (let index = 0; index < length; index += 1) {
960
+ distance += Math.abs((left[index] ?? 0) - (right[index] ?? 0));
961
+ }
962
+ return distance;
963
+ }
964
+ function resolveAnchorPoint(frame) {
965
+ const pivot = frame.frame.spritesheet?.pivot;
966
+ const normalizedPivot = {
967
+ x: clamp01(pivot?.x ?? 0.5),
968
+ y: clamp01(pivot?.y ?? 0.5),
969
+ };
970
+ return {
971
+ x: normalizedPivot.x * frame.width,
972
+ y: normalizedPivot.y * frame.height,
973
+ };
974
+ }
975
+ function clamp01(value) {
976
+ if (!Number.isFinite(value)) {
977
+ return 0;
978
+ }
979
+ if (value < 0) {
980
+ return 0;
981
+ }
982
+ if (value > 1) {
983
+ return 1;
984
+ }
985
+ return value;
986
+ }
987
+ function resolveThresholdValue(values) {
988
+ for (const value of values) {
989
+ if (typeof value === "number" && Number.isFinite(value)) {
990
+ return value;
991
+ }
992
+ }
993
+ return undefined;
994
+ }
995
+ function enforceTextureBudgetByProfile(params) {
996
+ const metricsByProfile = {};
997
+ const itemByTargetId = new Map(params.items.map((item) => [item.targetId, item]));
998
+ const profileStats = new Map();
999
+ for (const target of params.runtimeTargets) {
1000
+ const profileId = target.evaluationProfileId ?? "__default__";
1001
+ const entry = profileStats.get(profileId) ?? {
1002
+ targetIds: new Set(),
1003
+ estimatedBytes: 0,
1004
+ budgetMB: undefined,
1005
+ hasMismatchedBudget: false,
1006
+ };
1007
+ entry.targetIds.add(target.id);
1008
+ const item = itemByTargetId.get(target.id);
1009
+ const width = item?.width;
1010
+ const height = item?.height;
1011
+ if (typeof width === "number" && typeof height === "number") {
1012
+ entry.estimatedBytes += width * height * 4;
1013
+ }
1014
+ const budgetMB = target.packTextureBudgetMB;
1015
+ if (typeof budgetMB === "number") {
1016
+ if (typeof entry.budgetMB === "number" && Math.abs(entry.budgetMB - budgetMB) > 1e-6) {
1017
+ entry.hasMismatchedBudget = true;
1018
+ }
1019
+ entry.budgetMB = Math.min(entry.budgetMB ?? budgetMB, budgetMB);
1020
+ }
1021
+ profileStats.set(profileId, entry);
1022
+ }
1023
+ for (const [profileId, entry] of profileStats) {
1024
+ const estimatedMB = entry.estimatedBytes / BYTES_PER_MEGABYTE;
1025
+ metricsByProfile[profileId] = {
1026
+ estimatedMB: Number(estimatedMB.toFixed(6)),
1027
+ ...(typeof entry.budgetMB === "number"
1028
+ ? { budgetMB: Number(entry.budgetMB.toFixed(6)) }
1029
+ : {}),
1030
+ targetCount: entry.targetIds.size,
1031
+ };
1032
+ if (entry.hasMismatchedBudget) {
1033
+ params.addIssue({
1034
+ level: "warning",
1035
+ code: "pack_texture_budget_profile_mismatch",
1036
+ message: `Evaluation profile "${profileId}" resolved conflicting packTextureBudgetMB values across targets.`,
1037
+ targetIds: Array.from(entry.targetIds),
1038
+ evaluationProfileId: profileId,
1039
+ });
1040
+ }
1041
+ if (typeof entry.budgetMB === "number" && estimatedMB > entry.budgetMB) {
1042
+ params.addIssue({
1043
+ level: "error",
1044
+ code: "pack_texture_budget_exceeded",
1045
+ message: `Evaluation profile "${profileId}" estimated texture memory ${estimatedMB.toFixed(2)}MB exceeds configured budget ${entry.budgetMB.toFixed(2)}MB.`,
1046
+ targetIds: Array.from(entry.targetIds),
1047
+ evaluationProfileId: profileId,
1048
+ metrics: {
1049
+ estimatedMB: Number(estimatedMB.toFixed(6)),
1050
+ budgetMB: Number(entry.budgetMB.toFixed(6)),
1051
+ },
1052
+ });
1053
+ }
1054
+ }
1055
+ return metricsByProfile;
1056
+ }
1057
+ function normalizeAtlasGroup(value) {
1058
+ if (!value) {
1059
+ return null;
1060
+ }
1061
+ const trimmed = value.trim();
1062
+ return trimmed.length > 0 ? trimmed : null;
1063
+ }
1064
+ //# sourceMappingURL=packInvariants.js.map