pixelkiln 0.88.0 → 0.89.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.
- package/README.md +1 -1
- package/dist/cli.js +2017 -782
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +18 -7
- package/dist/index.cjs +1740 -588
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2239 -359
- package/dist/index.d.ts +2239 -359
- package/dist/index.js +1629 -503
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +77 -5
- package/docs/ENDPOINTS.md +132 -9
- package/docs/GENERATORS.md +88 -6
- package/docs/MANIFEST.md +27 -6
- package/docs/PIXELLAB.md +93 -7
- package/docs/REVISIONS.md +70 -5
- package/docs/TILES.md +30 -6
- package/package.json +1 -1
- package/schema/manifest.schema.json +147 -1
- package/schema/recipe.schema.json +39 -0
- package/skills/pixelkiln/SKILL.md +1 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +138 -12
- package/skills/pixelkiln/references/pixellab.md +117 -26
- package/skills/pixelkiln/references/revisions.md +14 -2
package/dist/index.cjs
CHANGED
|
@@ -86,6 +86,12 @@ __export(index_exports, {
|
|
|
86
86
|
MapScenePlacementSchema: () => MapScenePlacementSchema,
|
|
87
87
|
MapSceneSchema: () => MapSceneSchema,
|
|
88
88
|
MediaType: () => MediaType,
|
|
89
|
+
NORMAL_MAP_CONVENTIONS: () => NORMAL_MAP_CONVENTIONS,
|
|
90
|
+
NORMAL_MAP_COST_GENERATIONS: () => NORMAL_MAP_COST_GENERATIONS,
|
|
91
|
+
NORMAL_MAP_MAX_FRAMES: () => NORMAL_MAP_MAX_FRAMES,
|
|
92
|
+
NORMAL_MAP_MAX_SIDE: () => NORMAL_MAP_MAX_SIDE,
|
|
93
|
+
NORMAL_MAP_MIN_SIDE: () => NORMAL_MAP_MIN_SIDE,
|
|
94
|
+
NormalMapConventionSchema: () => NormalMapConventionSchema,
|
|
89
95
|
OverwriteRefusedError: () => OverwriteRefusedError,
|
|
90
96
|
PIXELATE_COST_GENERATIONS: () => PIXELATE_COST_GENERATIONS,
|
|
91
97
|
PIXELATE_MAX_SIDE: () => PIXELATE_MAX_SIDE,
|
|
@@ -113,12 +119,17 @@ __export(index_exports, {
|
|
|
113
119
|
ScenarioProvider: () => ScenarioProvider,
|
|
114
120
|
StyleInputSchema: () => StyleInputSchema,
|
|
115
121
|
StyleSchema: () => StyleSchema,
|
|
122
|
+
TerrainConnectSchema: () => TerrainConnectSchema,
|
|
123
|
+
TerrainLinkSchema: () => TerrainLinkSchema,
|
|
124
|
+
TerrainSideSchema: () => TerrainSideSchema,
|
|
116
125
|
UNZOOM_MAX_AREA: () => UNZOOM_MAX_AREA,
|
|
117
126
|
UNZOOM_MIN_SIDE: () => UNZOOM_MIN_SIDE,
|
|
118
127
|
UiElementSchema: () => UiElementSchema,
|
|
119
128
|
UiPieceSchema: () => UiPieceSchema,
|
|
120
129
|
UnsupportedCapabilityError: () => UnsupportedCapabilityError,
|
|
121
130
|
UsageError: () => UsageError,
|
|
131
|
+
VFX_ARTISTS: () => VFX_ARTISTS,
|
|
132
|
+
VFX_ARTIST_MAX_SPRITE: () => VFX_ARTIST_MAX_SPRITE,
|
|
122
133
|
VFX_TOOL: () => VFX_TOOL,
|
|
123
134
|
VfxPartSchema: () => VfxPartSchema,
|
|
124
135
|
WorkspaceProjectSchema: () => WorkspaceProjectSchema,
|
|
@@ -151,6 +162,7 @@ __export(index_exports, {
|
|
|
151
162
|
createProvider: () => createProvider,
|
|
152
163
|
currentEntryOutputPath: () => currentEntryOutputPath,
|
|
153
164
|
currentOutputPath: () => currentOutputPath,
|
|
165
|
+
defaultNormalMapOut: () => defaultNormalMapOut,
|
|
154
166
|
defaultOpenCommand: () => defaultOpenCommand,
|
|
155
167
|
defaultPixelateOut: () => defaultPixelateOut,
|
|
156
168
|
defaultUnzoomOut: () => defaultUnzoomOut,
|
|
@@ -212,6 +224,7 @@ __export(index_exports, {
|
|
|
212
224
|
loadWorkspace: () => loadWorkspace,
|
|
213
225
|
lockKey: () => lockKey,
|
|
214
226
|
makeVfx: () => makeVfx,
|
|
227
|
+
makeVfxWithArtist: () => makeVfxWithArtist,
|
|
215
228
|
matchOrphanStyle: () => matchOrphanStyle,
|
|
216
229
|
mcpClientFromEnv: () => mcpClientFromEnv,
|
|
217
230
|
measureBalanceChange: () => measureBalanceChange,
|
|
@@ -223,6 +236,9 @@ __export(index_exports, {
|
|
|
223
236
|
mergePalettes: () => mergePalettes,
|
|
224
237
|
mountSprites: () => mountSprites,
|
|
225
238
|
mountStyle: () => mountStyle,
|
|
239
|
+
normalMapFile: () => normalMapFile,
|
|
240
|
+
normalMapResultPng: () => normalMapResultPng,
|
|
241
|
+
normalMapSizeProblem: () => normalMapSizeProblem,
|
|
226
242
|
normalizeLockOutputPaths: () => normalizeLockOutputPaths,
|
|
227
243
|
normalizeRefinementPalette: () => normalizeRefinementPalette,
|
|
228
244
|
normalizeTerrainTiles: () => normalizeTerrainTiles,
|
|
@@ -240,6 +256,7 @@ __export(index_exports, {
|
|
|
240
256
|
parseMcpBody: () => parseMcpBody,
|
|
241
257
|
parseProFlashQuote: () => parseProFlashQuote,
|
|
242
258
|
parseTerrainDescriptions: () => parseTerrainDescriptions,
|
|
259
|
+
parseVfxArtistResult: () => parseVfxArtistResult,
|
|
243
260
|
parseVfxExport: () => parseVfxExport,
|
|
244
261
|
parseVfxRecipe: () => parseVfxRecipe,
|
|
245
262
|
parseVfxStep: () => parseVfxStep,
|
|
@@ -263,6 +280,7 @@ __export(index_exports, {
|
|
|
263
280
|
readHandEditCompanion: () => readHandEditCompanion,
|
|
264
281
|
readQualityBaseline: () => readQualityBaseline,
|
|
265
282
|
readSavedVfxRecipe: () => readSavedVfxRecipe,
|
|
283
|
+
readVfxConversation: () => readVfxConversation,
|
|
266
284
|
readVfxRecipe: () => readVfxRecipe,
|
|
267
285
|
recipeDigest: () => recipeDigest,
|
|
268
286
|
referencedHashes: () => referencedHashes,
|
|
@@ -325,6 +343,7 @@ __export(index_exports, {
|
|
|
325
343
|
submit: () => submit,
|
|
326
344
|
summarize: () => summarize,
|
|
327
345
|
tagAdopted: () => tagAdopted,
|
|
346
|
+
terrainCost: () => terrainCost,
|
|
328
347
|
terrainTileCount: () => terrainTileCount,
|
|
329
348
|
tileFeatureOutputCount: () => tileFeatureOutputCount,
|
|
330
349
|
tileVariationCount: () => tileVariationCount,
|
|
@@ -338,6 +357,13 @@ __export(index_exports, {
|
|
|
338
357
|
validateWorkspace: () => validateWorkspace,
|
|
339
358
|
verifyArtifactBundle: () => verifyArtifactBundle,
|
|
340
359
|
verifyRecipe: () => verifyRecipe,
|
|
360
|
+
vfxArtistCeiling: () => vfxArtistCeiling,
|
|
361
|
+
vfxArtistFiles: () => vfxArtistFiles,
|
|
362
|
+
vfxArtistFrameFacts: () => vfxArtistFrameFacts,
|
|
363
|
+
vfxArtistQuote: () => vfxArtistQuote,
|
|
364
|
+
vfxArtistSprite: () => vfxArtistSprite,
|
|
365
|
+
vfxAtlasFrames: () => vfxAtlasFrames,
|
|
366
|
+
vfxJobFailure: () => vfxJobFailure,
|
|
341
367
|
vfxOutputPaths: () => vfxOutputPaths,
|
|
342
368
|
vfxRecipeSha256: () => vfxRecipeSha256,
|
|
343
369
|
withArtifactManifest: () => withArtifactManifest,
|
|
@@ -368,8 +394,10 @@ var RevisionModeSchema = import_zod.z.enum([
|
|
|
368
394
|
"animate-pixminimax",
|
|
369
395
|
"animate-skeleton",
|
|
370
396
|
"interpolate",
|
|
371
|
-
"edit-animation"
|
|
397
|
+
"edit-animation",
|
|
398
|
+
"normal-map"
|
|
372
399
|
]);
|
|
400
|
+
var NormalMapConventionSchema = import_zod.z.enum(["opengl", "directx"]);
|
|
373
401
|
function tileVariationCount(descriptions) {
|
|
374
402
|
return Math.max(1, descriptions) * 4;
|
|
375
403
|
}
|
|
@@ -412,9 +440,16 @@ function tilesCost(tileSize, variations) {
|
|
|
412
440
|
function isometricTileCost() {
|
|
413
441
|
return 1;
|
|
414
442
|
}
|
|
415
|
-
function terrainTileCount(transitionSize) {
|
|
443
|
+
function terrainTileCount(transitionSize, mode2, orientation) {
|
|
444
|
+
if (orientation === "sidescroller") return 16;
|
|
445
|
+
if (mode2 === "pro") return transitionSize != null && transitionSize > 0.4 ? 25 : 16;
|
|
416
446
|
return transitionSize === 1 ? 25 : 16;
|
|
417
447
|
}
|
|
448
|
+
function terrainCost(mode2, tileSize, orientation) {
|
|
449
|
+
if (orientation === "sidescroller") return 3;
|
|
450
|
+
if (mode2 === "pro") return tileSize <= 32 ? 15 : 25;
|
|
451
|
+
return 4;
|
|
452
|
+
}
|
|
418
453
|
function parseTerrainDescriptions(prompt) {
|
|
419
454
|
const parts = prompt.split(/\d+\s*\)\s*\./).map((part) => part.trim()).filter(Boolean);
|
|
420
455
|
if (parts.length < 2) return null;
|
|
@@ -550,6 +585,17 @@ var CharacterStateSchema = import_zod.z.object({
|
|
|
550
585
|
height: import_zod.z.number().int().min(16).max(256)
|
|
551
586
|
}).strict().optional()
|
|
552
587
|
}).strict();
|
|
588
|
+
var TerrainSideSchema = import_zod.z.enum(["lower", "upper"]);
|
|
589
|
+
var TerrainLinkSchema = import_zod.z.object({
|
|
590
|
+
/** A terrain asset in the same style. */
|
|
591
|
+
of: import_zod.z.string().min(1),
|
|
592
|
+
/** Which of that set's terrains: its `lower` or its `upper`. */
|
|
593
|
+
terrain: TerrainSideSchema
|
|
594
|
+
}).strict();
|
|
595
|
+
var TerrainConnectSchema = import_zod.z.object({
|
|
596
|
+
lower: TerrainLinkSchema.optional(),
|
|
597
|
+
upper: TerrainLinkSchema.optional()
|
|
598
|
+
}).strict().refine((connect) => connect.lower || connect.upper, { message: "connect needs lower, upper, or both" });
|
|
553
599
|
var CharacterPortraitSizeSchema = import_zod.z.union([
|
|
554
600
|
import_zod.z.literal(16),
|
|
555
601
|
import_zod.z.literal(32),
|
|
@@ -580,13 +626,38 @@ var AssetVfxSchema = import_zod.z.object({
|
|
|
580
626
|
parts: import_zod.z.array(VfxPartSchema).optional(),
|
|
581
627
|
sprite: import_zod.z.string().min(1).optional(),
|
|
582
628
|
fps: import_zod.z.number().int().min(1).max(60).optional(),
|
|
583
|
-
name: import_zod.z.string().min(1).optional()
|
|
629
|
+
name: import_zod.z.string().min(1).optional(),
|
|
630
|
+
description: import_zod.z.string().trim().min(1).max(8e3).optional(),
|
|
631
|
+
artist: import_zod.z.enum(["lite", "pro"]).optional()
|
|
584
632
|
}).strict().superRefine((vfx, context) => {
|
|
633
|
+
if (vfx.description !== void 0) {
|
|
634
|
+
for (const key of ["preset", "recipe", "parts", "fps"]) {
|
|
635
|
+
if (vfx[key] !== void 0) {
|
|
636
|
+
context.addIssue({
|
|
637
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
638
|
+
message: `a vfx description goes to PixelLab's VFX artist, which takes no ${key}; use a description or the workbench's preset/recipe/parts/fps, not both`,
|
|
639
|
+
path: [key]
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (vfx.sprite && /^(character|object|gallery):/.test(vfx.sprite)) {
|
|
644
|
+
context.addIssue({
|
|
645
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
646
|
+
message: "the VFX artist takes a sprite's pixels, not a PixelLab reference; point sprite at a PNG file (another asset's output works)",
|
|
647
|
+
path: ["sprite"]
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (vfx.name && vfx.name.length > 80) {
|
|
651
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "the VFX artist takes a name of at most 80 characters", path: ["name"] });
|
|
652
|
+
}
|
|
653
|
+
} else if (vfx.artist) {
|
|
654
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "artist picks who draws a vfx description; add a description", path: ["artist"] });
|
|
655
|
+
}
|
|
585
656
|
if (vfx.preset && vfx.recipe) {
|
|
586
657
|
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
|
|
587
658
|
}
|
|
588
|
-
if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
589
|
-
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
|
|
659
|
+
if (vfx.description === void 0 && !vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
660
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part, or a description", path: ["preset"] });
|
|
590
661
|
}
|
|
591
662
|
if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
|
|
592
663
|
context.addIssue({
|
|
@@ -771,6 +842,8 @@ var RevisionSchema = import_zod.z.object({
|
|
|
771
842
|
skeletonTemplate: import_zod.z.string().min(1).optional(),
|
|
772
843
|
/** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
|
|
773
844
|
removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
|
|
845
|
+
/** `normal-map` only: `opengl` (green up; PixelLab's default) or `directx` (green down, for Unreal). */
|
|
846
|
+
convention: NormalMapConventionSchema.optional(),
|
|
774
847
|
/**
|
|
775
848
|
* `animate-skeleton`: what the character *looks like*, a noun
|
|
776
849
|
* phrase (colours, clothing, held items) — distinct from the asset's
|
|
@@ -917,13 +990,20 @@ var RevisionSchema = import_zod.z.object({
|
|
|
917
990
|
path: ["removalTask"]
|
|
918
991
|
});
|
|
919
992
|
}
|
|
920
|
-
if (revision.strength !== void 0 && revision.mode === "remove-background") {
|
|
993
|
+
if (revision.strength !== void 0 && (revision.mode === "remove-background" || revision.mode === "normal-map")) {
|
|
921
994
|
context.addIssue({
|
|
922
995
|
code: import_zod.z.ZodIssueCode.custom,
|
|
923
|
-
message:
|
|
996
|
+
message: `${revision.mode} revisions do not take a strength`,
|
|
924
997
|
path: ["strength"]
|
|
925
998
|
});
|
|
926
999
|
}
|
|
1000
|
+
if (revision.convention !== void 0 && revision.mode !== "normal-map") {
|
|
1001
|
+
context.addIssue({
|
|
1002
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
1003
|
+
message: "convention applies to normal-map revisions only",
|
|
1004
|
+
path: ["convention"]
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
927
1007
|
if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
|
|
928
1008
|
context.addIssue({
|
|
929
1009
|
code: import_zod.z.ZodIssueCode.custom,
|
|
@@ -942,7 +1022,7 @@ var RevisionSchema = import_zod.z.object({
|
|
|
942
1022
|
}
|
|
943
1023
|
});
|
|
944
1024
|
var FRAME_SET_REVISION_MODES = ["animate", "animate-pixminimax", "animate-skeleton", "interpolate", "edit-animation"];
|
|
945
|
-
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation"];
|
|
1025
|
+
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation", "normal-map"];
|
|
946
1026
|
var StyleObjectSchema = import_zod.z.object({
|
|
947
1027
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
948
1028
|
provider: import_zod.z.string().min(1).optional(),
|
|
@@ -1055,9 +1135,10 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
1055
1135
|
terrainTileSize: import_zod.z.union([import_zod.z.literal(16), import_zod.z.literal(32), import_zod.z.literal(64)]).optional(),
|
|
1056
1136
|
/**
|
|
1057
1137
|
* `terrain` generator only. `standard` is the classic Wang tileset
|
|
1058
|
-
* pipeline; `pro` is a newer
|
|
1059
|
-
*
|
|
1060
|
-
* in place of
|
|
1138
|
+
* pipeline; `pro` is a newer pipeline with its own shape controls
|
|
1139
|
+
* (`terrainRaggedness`, `terrainRoundness`, `terrainBoundaryAmplitude`,
|
|
1140
|
+
* `terrainStepSlope`, `terrainConnectDiagonals`) in place of
|
|
1141
|
+
* `terrainShapeStyle`. The API default is `standard`.
|
|
1061
1142
|
*/
|
|
1062
1143
|
terrainMode: import_zod.z.enum(["standard", "pro"]).optional(),
|
|
1063
1144
|
/**
|
|
@@ -1069,31 +1150,86 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
1069
1150
|
/**
|
|
1070
1151
|
* `terrain` generator only, `terrainMode: "standard"`. Procedural
|
|
1071
1152
|
* boundary geometry: `square` or `round`, 16px or 32px tiles only.
|
|
1072
|
-
* Rejected together with `terrainMode: "pro"`,
|
|
1073
|
-
*
|
|
1153
|
+
* Rejected together with `terrainMode: "pro"`, which has its own shape
|
|
1154
|
+
* controls.
|
|
1074
1155
|
*/
|
|
1075
1156
|
terrainShapeStyle: import_zod.z.enum(["square", "round"]).optional(),
|
|
1076
|
-
/**
|
|
1077
|
-
*
|
|
1157
|
+
/**
|
|
1158
|
+
* Deprecated: no effect. PixelLab's Pro pipeline no longer reads
|
|
1159
|
+
* `spread_x`; the API keeps accepting it so existing calls work, and so
|
|
1160
|
+
* does pixelkiln. Use `terrainBoundaryAmplitude` or `terrainStepSlope`.
|
|
1161
|
+
*/
|
|
1078
1162
|
terrainSpreadX: import_zod.z.number().min(0).max(1).optional(),
|
|
1079
|
-
/**
|
|
1080
|
-
*
|
|
1163
|
+
/**
|
|
1164
|
+
* Deprecated: no effect. PixelLab's Pro pipeline no longer reads
|
|
1165
|
+
* `slope_size`; the API keeps accepting it so existing calls work, and
|
|
1166
|
+
* so does pixelkiln. `terrainStepSlope` is the slope control now.
|
|
1167
|
+
*/
|
|
1081
1168
|
terrainSlopeSize: import_zod.z.number().min(0).max(1).optional(),
|
|
1082
1169
|
/** `terrain` generator only, `terrainMode: "pro"`. Terrain boundary
|
|
1083
|
-
* noise (0 = smooth, 1 = rough)
|
|
1170
|
+
* noise (0 = smooth, 1 = rough), half strength on tall sets
|
|
1171
|
+
* (`terrainTransitionSize` above 0.4). API default 0. */
|
|
1084
1172
|
terrainRaggedness: import_zod.z.number().min(0).max(1).optional(),
|
|
1173
|
+
/**
|
|
1174
|
+
* `terrain` generator only, `terrainMode: "pro"` or `terrainShapeStyle`.
|
|
1175
|
+
* Corner shape of the border between the terrains: 0 square, 1 round,
|
|
1176
|
+
* 2 corners cut at 45° (values between blend). Above 1 needs pro. The API
|
|
1177
|
+
* default is 0.85 on pro and 0.6 with a shape style. With
|
|
1178
|
+
* `terrainConnectDiagonals`, 2 is the fix for 45° paths and coastlines
|
|
1179
|
+
* that otherwise break into one island per step.
|
|
1180
|
+
*/
|
|
1181
|
+
terrainRoundness: import_zod.z.number().min(0).max(2).optional(),
|
|
1182
|
+
/**
|
|
1183
|
+
* `terrain` generator only, `terrainMode: "pro"` (any transition size)
|
|
1184
|
+
* or `terrainShapeStyle` (transition size up to 0.5). How far the border
|
|
1185
|
+
* meanders instead of running straight: 0 straight (the default) to 0.3.
|
|
1186
|
+
* Tiles still connect seamlessly.
|
|
1187
|
+
*/
|
|
1188
|
+
terrainBoundaryAmplitude: import_zod.z.number().min(0).max(0.3).optional(),
|
|
1189
|
+
/**
|
|
1190
|
+
* `terrain` generator only, with `terrainBoundaryAmplitude`. Picks the
|
|
1191
|
+
* meander's shape: another number gives a different border at the same
|
|
1192
|
+
* amplitude, the same number the same border. API default 0.
|
|
1193
|
+
*/
|
|
1194
|
+
terrainBoundarySeed: import_zod.z.number().int().min(0).optional(),
|
|
1195
|
+
/**
|
|
1196
|
+
* `terrain` generator only, `terrainMode: "pro"` with
|
|
1197
|
+
* `terrainTransitionSize: 0.25`. How the bank between the terrains
|
|
1198
|
+
* falls: 0 a straight cliff (the default) to 2 a gentle slope spread
|
|
1199
|
+
* over more ground.
|
|
1200
|
+
*/
|
|
1201
|
+
terrainStepSlope: import_zod.z.number().min(0).max(2).optional(),
|
|
1202
|
+
/**
|
|
1203
|
+
* `terrain` generator only, `terrainMode: "pro"`. On the two diagonal
|
|
1204
|
+
* tiles (upper terrain in opposite corners), true joins the upper
|
|
1205
|
+
* corners through the middle into one area; false (the default) runs the
|
|
1206
|
+
* lower terrain between them.
|
|
1207
|
+
*/
|
|
1208
|
+
terrainConnectDiagonals: import_zod.z.boolean().optional(),
|
|
1085
1209
|
/**
|
|
1086
1210
|
* `terrain` generator only. Visual height of the step between lower and
|
|
1087
1211
|
* upper terrain. Without `terrainShapeStyle`, only 0, 0.25, 0.5, or 1 are
|
|
1088
|
-
* accepted
|
|
1089
|
-
* third "transition" value
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1212
|
+
* accepted. Standard switches to the 25-tile cliff layout at 1, where
|
|
1213
|
+
* corners take a third "transition" value; Pro reads it as wall height
|
|
1214
|
+
* and switches to the 25-tile tall-wall layout above 0.4, so 0.5 is
|
|
1215
|
+
* already 25 tiles there. With `terrainShapeStyle`, any value from 0 to 1
|
|
1216
|
+
* works, but above 0.5 switches to an extended 32-tile layout this
|
|
1217
|
+
* adapter does not model. API default 0.
|
|
1092
1218
|
*/
|
|
1093
1219
|
terrainTransitionSize: import_zod.z.number().min(0).max(1).optional(),
|
|
1094
1220
|
/** `terrain` generator only. Camera angle; the API default is `high
|
|
1095
1221
|
* top-down`. */
|
|
1096
1222
|
terrainView: import_zod.z.enum(["low top-down", "high top-down"]).optional(),
|
|
1223
|
+
/**
|
|
1224
|
+
* `terrain` generator only. `top-down` (the default) is a two-terrain
|
|
1225
|
+
* map set from `/create-tileset`; `sidescroller` is a platform set from
|
|
1226
|
+
* `/create-tileset-sidescroller`, seen from the side: the prompt's 1).
|
|
1227
|
+
* is the platform body and 2). the top layer on it (grass, snow, moss),
|
|
1228
|
+
* and the set's other terrain is empty air. 16 or 32px tiles,
|
|
1229
|
+
* `terrainTransitionSize` 0, 0.25, 0.5, or 1 (the top layer's height),
|
|
1230
|
+
* and none of the top-down mode, view, or shape controls.
|
|
1231
|
+
*/
|
|
1232
|
+
terrainOrientation: import_zod.z.enum(["top-down", "sidescroller"]).optional(),
|
|
1097
1233
|
/**
|
|
1098
1234
|
* `isometricTile` generator only. `/create-isometric-tile`'s own tile
|
|
1099
1235
|
* grid size, distinct from `size` (the generation canvas, 16-64px — the
|
|
@@ -1173,7 +1309,9 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
1173
1309
|
/** Fixed seed for reproducibility where the endpoint supports it. */
|
|
1174
1310
|
seed: import_zod.z.number().int().optional(),
|
|
1175
1311
|
/**
|
|
1176
|
-
* Forced palette, as `#rrggbb` values. `pixflux`
|
|
1312
|
+
* Forced palette, as `#rrggbb` values. Sent as a swatch to `pixflux`
|
|
1313
|
+
* and the generators that take one, and to `imagePro` as its
|
|
1314
|
+
* `target_palette.colors` (2 to 256 colours).
|
|
1177
1315
|
*
|
|
1178
1316
|
* Unlike prose, this is a hard constraint. The API is handed a swatch
|
|
1179
1317
|
* image and the output is limited to those colours. Verified: a four-colour
|
|
@@ -1183,6 +1321,10 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
1183
1321
|
* chocolate bar.
|
|
1184
1322
|
*/
|
|
1185
1323
|
palette: import_zod.z.array(HexColorSchema).default([]),
|
|
1324
|
+
/** `imagePro` only: the palette's name ("Game Boy", "PICO-8"), which PixelLab uses to help the look. Needs `palette`. */
|
|
1325
|
+
paletteName: import_zod.z.string().min(1).max(100).optional(),
|
|
1326
|
+
/** `imagePro` only: at most this many colours in each candidate, 2 to 64 (drawn from `palette` when one is set). */
|
|
1327
|
+
maxColors: import_zod.z.number().int().min(2).max(64).optional(),
|
|
1186
1328
|
/**
|
|
1187
1329
|
* Snap every visible pixel of the downloaded art to the nearest `palette`
|
|
1188
1330
|
* colour, without dithering. The palette is sent to providers that take
|
|
@@ -1236,6 +1378,22 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
1236
1378
|
path: ["quality"]
|
|
1237
1379
|
});
|
|
1238
1380
|
}
|
|
1381
|
+
for (const field of ["paletteName", "maxColors"]) {
|
|
1382
|
+
if (style[field] !== void 0 && style.generator !== "imagePro") {
|
|
1383
|
+
ctx.addIssue({
|
|
1384
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
1385
|
+
message: `${field} applies to the imagePro generator only`,
|
|
1386
|
+
path: [field]
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (style.paletteName !== void 0 && style.palette.length === 0) {
|
|
1391
|
+
ctx.addIssue({
|
|
1392
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
1393
|
+
message: "paletteName names the palette's colours; set palette too",
|
|
1394
|
+
path: ["paletteName"]
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1239
1397
|
if (style.enforcePalette && style.palette.length < 2) {
|
|
1240
1398
|
ctx.addIssue({
|
|
1241
1399
|
code: import_zod.z.ZodIssueCode.custom,
|
|
@@ -1361,6 +1519,8 @@ var AssetSchema = import_zod.z.object({
|
|
|
1361
1519
|
scene: MapSceneSchema.optional(),
|
|
1362
1520
|
/** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
|
|
1363
1521
|
vfx: AssetVfxSchema.optional(),
|
|
1522
|
+
/** `terrain` styles only: share a terrain with another set. See `TerrainConnectSchema`. */
|
|
1523
|
+
connect: TerrainConnectSchema.optional(),
|
|
1364
1524
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1365
1525
|
proportions: CharacterProportionsSchema.optional(),
|
|
1366
1526
|
/**
|
|
@@ -1437,7 +1597,7 @@ var AssetSchema = import_zod.z.object({
|
|
|
1437
1597
|
path: ["revision"]
|
|
1438
1598
|
});
|
|
1439
1599
|
}
|
|
1440
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene", asset.vfx && "vfx"].filter(Boolean);
|
|
1600
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene", asset.vfx && "vfx", asset.connect && "connect"].filter(Boolean);
|
|
1441
1601
|
if (shapes.length > 1) {
|
|
1442
1602
|
context.addIssue({
|
|
1443
1603
|
code: import_zod.z.ZodIssueCode.custom,
|
|
@@ -1529,6 +1689,10 @@ var LockSourceSchema = import_zod.z.object({
|
|
|
1529
1689
|
role: import_zod.z.string().optional(),
|
|
1530
1690
|
mediaType: MediaTypeSchema.optional()
|
|
1531
1691
|
});
|
|
1692
|
+
var LockTerrainConnectSchema = import_zod.z.record(
|
|
1693
|
+
TerrainSideSchema,
|
|
1694
|
+
import_zod.z.object({ sourceAssetId: import_zod.z.string().min(1), sourceObjectId: import_zod.z.string().min(1) }).strict()
|
|
1695
|
+
);
|
|
1532
1696
|
var LockHistoryEntrySchema = import_zod.z.object({
|
|
1533
1697
|
specHash: import_zod.z.string(),
|
|
1534
1698
|
generator: GeneratorSchema,
|
|
@@ -1551,6 +1715,8 @@ var LockHistoryEntrySchema = import_zod.z.object({
|
|
|
1551
1715
|
billed: import_zod.z.object({ amount: import_zod.z.number(), unit: import_zod.z.string() }).nullable().default(null),
|
|
1552
1716
|
provider: import_zod.z.string().default("pixellab"),
|
|
1553
1717
|
postprocess: PostprocessSchema.optional(),
|
|
1718
|
+
/** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
|
|
1719
|
+
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1554
1720
|
/** When the replacement was submitted. */
|
|
1555
1721
|
retiredAt: import_zod.z.string()
|
|
1556
1722
|
});
|
|
@@ -1589,6 +1755,7 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
1589
1755
|
skeletonTemplate: import_zod.z.string().min(1).optional(),
|
|
1590
1756
|
description: import_zod.z.string().min(1).optional(),
|
|
1591
1757
|
removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
|
|
1758
|
+
convention: NormalMapConventionSchema.optional(),
|
|
1592
1759
|
driftThreshold: import_zod.z.number().min(0).optional(),
|
|
1593
1760
|
subjectDescription: import_zod.z.string().min(1).max(300).optional(),
|
|
1594
1761
|
initialPose: import_zod.z.string().min(1).max(300).optional(),
|
|
@@ -1616,6 +1783,14 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
1616
1783
|
sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/),
|
|
1617
1784
|
referenceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
|
|
1618
1785
|
}).strict().nullable().default(null),
|
|
1786
|
+
/**
|
|
1787
|
+
* For a connected terrain set: per side of this set, the set it shares
|
|
1788
|
+
* that terrain with and that set's provider id when this was submitted.
|
|
1789
|
+
* Tracked the way `outfit` is, not through `specHash`: the other set's
|
|
1790
|
+
* id lives in the lockfile, which manifest resolution does not read.
|
|
1791
|
+
* Absent (not null) otherwise, so other entries do not grow the key.
|
|
1792
|
+
*/
|
|
1793
|
+
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1619
1794
|
/**
|
|
1620
1795
|
* Output hashes owned by the previous generation while its replacement is
|
|
1621
1796
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1940,7 +2115,16 @@ var TilesetGetSchema = import_zod3.z.object({
|
|
|
1940
2115
|
terrain_types: import_zod3.z.array(import_zod3.z.string()),
|
|
1941
2116
|
tiles: import_zod3.z.array(TilesetTileSchema).min(1)
|
|
1942
2117
|
}).passthrough(),
|
|
1943
|
-
usage: UsageSchema
|
|
2118
|
+
usage: UsageSchema,
|
|
2119
|
+
/**
|
|
2120
|
+
* `terrain_ids` names each terrain's base tile id: the id another set
|
|
2121
|
+
* sends as `lower_base_tile_id`/`upper_base_tile_id` to share that
|
|
2122
|
+
* terrain. A sidescroller set has only `lower` (its upper is empty air).
|
|
2123
|
+
*/
|
|
2124
|
+
metadata: import_zod3.z.object({
|
|
2125
|
+
terrain_ids: import_zod3.z.record(import_zod3.z.string()).nullable().optional(),
|
|
2126
|
+
view: import_zod3.z.string().nullable().optional()
|
|
2127
|
+
}).passthrough().nullable().optional()
|
|
1944
2128
|
}).passthrough();
|
|
1945
2129
|
var IsometricTileSubmitSchema = import_zod3.z.object({
|
|
1946
2130
|
tile_id: import_zod3.z.string().min(1),
|
|
@@ -2029,6 +2213,12 @@ var ProFlashImageSubmitSchema = import_zod3.z.object({
|
|
|
2029
2213
|
estimated_generations: import_zod3.z.number().nullable().optional(),
|
|
2030
2214
|
usage: import_zod3.z.unknown().optional()
|
|
2031
2215
|
}).passthrough();
|
|
2216
|
+
var VfxSubmitSchema = import_zod3.z.object({
|
|
2217
|
+
background_job_id: import_zod3.z.string().min(1),
|
|
2218
|
+
status: import_zod3.z.string().default("processing"),
|
|
2219
|
+
quoted_generations: import_zod3.z.number().nullable().optional(),
|
|
2220
|
+
usage: import_zod3.z.unknown().optional()
|
|
2221
|
+
}).passthrough();
|
|
2032
2222
|
var UnzoomResponseSchema = import_zod3.z.object({
|
|
2033
2223
|
image: import_zod3.z.object({ base64: import_zod3.z.string().min(1) }).passthrough(),
|
|
2034
2224
|
original_size: import_zod3.z.object({ width: import_zod3.z.number().int(), height: import_zod3.z.number().int() }).passthrough(),
|
|
@@ -2369,12 +2559,19 @@ var PixelLabClient = class {
|
|
|
2369
2559
|
upper_description: args.upperDescription
|
|
2370
2560
|
};
|
|
2371
2561
|
if (args.transitionDescription) body.transition_description = args.transitionDescription;
|
|
2562
|
+
if (args.lowerBaseTileId) body.lower_base_tile_id = args.lowerBaseTileId;
|
|
2563
|
+
if (args.upperBaseTileId) body.upper_base_tile_id = args.upperBaseTileId;
|
|
2372
2564
|
if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
|
|
2373
2565
|
if (args.mode) body.mode = args.mode;
|
|
2374
2566
|
if (args.shapeStyle) body.shape_style = args.shapeStyle;
|
|
2375
2567
|
if (args.spreadX != null) body.spread_x = args.spreadX;
|
|
2376
2568
|
if (args.slopeSize != null) body.slope_size = args.slopeSize;
|
|
2377
2569
|
if (args.raggedness != null) body.raggedness = args.raggedness;
|
|
2570
|
+
if (args.roundness != null) body.roundness = args.roundness;
|
|
2571
|
+
if (args.boundaryAmplitude != null) body.boundary_amplitude = args.boundaryAmplitude;
|
|
2572
|
+
if (args.boundarySeed != null) body.boundary_seed = args.boundarySeed;
|
|
2573
|
+
if (args.stepSlope != null) body.step_slope = args.stepSlope;
|
|
2574
|
+
if (args.connectDiagonals != null) body.connect_diagonals = args.connectDiagonals;
|
|
2378
2575
|
if (args.transitionSize != null) body.transition_size = args.transitionSize;
|
|
2379
2576
|
if (args.view) body.view = args.view;
|
|
2380
2577
|
if (args.outline) body.outline = args.outline;
|
|
@@ -2387,14 +2584,48 @@ var PixelLabClient = class {
|
|
|
2387
2584
|
"create tileset"
|
|
2388
2585
|
);
|
|
2389
2586
|
}
|
|
2390
|
-
/**
|
|
2587
|
+
/**
|
|
2588
|
+
* `/create-tileset-sidescroller`: a platform set seen from the side. One
|
|
2589
|
+
* terrain (`lower_description`, the platform body) and the decorative
|
|
2590
|
+
* layer on top of it (`transition_description`); the other side of every
|
|
2591
|
+
* edge is empty air, so there is no upper description, mode, view, or
|
|
2592
|
+
* shape control. 16 or 32px tiles only. The result is read through
|
|
2593
|
+
* `getTileset` like a top-down set.
|
|
2594
|
+
*/
|
|
2595
|
+
async createTilesetSidescroller(args) {
|
|
2596
|
+
const body = { lower_description: args.lowerDescription };
|
|
2597
|
+
if (args.transitionDescription) body.transition_description = args.transitionDescription;
|
|
2598
|
+
if (args.lowerBaseTileId) body.lower_base_tile_id = args.lowerBaseTileId;
|
|
2599
|
+
if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
|
|
2600
|
+
if (args.transitionSize != null) body.transition_size = args.transitionSize;
|
|
2601
|
+
if (args.outline) body.outline = args.outline;
|
|
2602
|
+
if (args.shading) body.shading = args.shading;
|
|
2603
|
+
if (args.detail) body.detail = args.detail;
|
|
2604
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2605
|
+
return validateResponse(
|
|
2606
|
+
TilesetSubmitSchema,
|
|
2607
|
+
await this.request("/create-tileset-sidescroller", { method: "POST", body: JSON.stringify(body) }),
|
|
2608
|
+
"create sidescroller tileset"
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Throws PixelLabError(423) while the set is still drawing; see Tileset.
|
|
2613
|
+
* Serves top-down and sidescroller sets alike.
|
|
2614
|
+
*/
|
|
2391
2615
|
async getTileset(tilesetId) {
|
|
2392
2616
|
const res = await validateResponse(
|
|
2393
2617
|
TilesetGetSchema,
|
|
2394
2618
|
await this.request(`/tilesets/${tilesetId}`),
|
|
2395
2619
|
"get tileset"
|
|
2396
2620
|
);
|
|
2397
|
-
return {
|
|
2621
|
+
return {
|
|
2622
|
+
tileset: res.tileset,
|
|
2623
|
+
usage: res.usage ?? null,
|
|
2624
|
+
metadata: {
|
|
2625
|
+
terrainIds: res.metadata?.terrain_ids ?? {},
|
|
2626
|
+
view: res.metadata?.view ?? null
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2398
2629
|
}
|
|
2399
2630
|
/** A single standalone isometric tile — no connectable set, no candidates. */
|
|
2400
2631
|
async createIsometricTile(args) {
|
|
@@ -2931,6 +3162,13 @@ var PixelLabClient = class {
|
|
|
2931
3162
|
*
|
|
2932
3163
|
* Reference images and a style image (`reference_images`, `style_image` +
|
|
2933
3164
|
* `style_options`) exist on this endpoint but are not modeled here yet.
|
|
3165
|
+
*
|
|
3166
|
+
* `targetPalette` is the beta `target_palette` (PixelLab 0.4.130): with
|
|
3167
|
+
* `colors`, the image is drawn for them and every result pixel is one of
|
|
3168
|
+
* them; `name` ("PICO-8") helps the look; `maxColors` caps the colours in
|
|
3169
|
+
* each candidate. Its `smart` flag is not modeled: it is documented as
|
|
3170
|
+
* "single images only", and this endpoint returns a grid of candidates
|
|
3171
|
+
* below 171px.
|
|
2934
3172
|
*/
|
|
2935
3173
|
async createImagePro(args) {
|
|
2936
3174
|
const body = {
|
|
@@ -2939,6 +3177,14 @@ var PixelLabClient = class {
|
|
|
2939
3177
|
};
|
|
2940
3178
|
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
2941
3179
|
if (args.seed != null) body.seed = args.seed;
|
|
3180
|
+
const palette = args.targetPalette;
|
|
3181
|
+
if (palette && (palette.colors?.length || palette.maxColors != null)) {
|
|
3182
|
+
body.target_palette = {
|
|
3183
|
+
...palette.colors?.length ? { colors: palette.colors } : {},
|
|
3184
|
+
...palette.name ? { name: palette.name } : {},
|
|
3185
|
+
...palette.maxColors != null ? { max_colors: palette.maxColors } : {}
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
2942
3188
|
return validateResponse(
|
|
2943
3189
|
RevisionJobSubmitSchema,
|
|
2944
3190
|
await this.request("/generate-image-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
@@ -3129,6 +3375,27 @@ var PixelLabClient = class {
|
|
|
3129
3375
|
);
|
|
3130
3376
|
return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
|
|
3131
3377
|
}
|
|
3378
|
+
/**
|
|
3379
|
+
* `/normal-map` (PixelLab 0.4.130): the normal map of a sprite, so a game
|
|
3380
|
+
* engine can light it. RGB is the way each pixel faces, alpha is 255 where
|
|
3381
|
+
* the sprite shows and 0 elsewhere, at the input's size. An animation goes
|
|
3382
|
+
* as one horizontal strip of `frames` equal frames (up to 32, each 8 to 512
|
|
3383
|
+
* px per side) and comes back as a strip, planned once on the first frame
|
|
3384
|
+
* so the light stays steady. `opengl` (the default) is green up, for
|
|
3385
|
+
* Godot, Unity, and Phaser; `directx` is green down, for Unreal. A
|
|
3386
|
+
* background job whose map is `last_response.image`; 0.5 generation,
|
|
3387
|
+
* charged only when the map is delivered.
|
|
3388
|
+
*/
|
|
3389
|
+
async normalMap(args) {
|
|
3390
|
+
const body = { image: args.image };
|
|
3391
|
+
if (args.frames != null && args.frames !== 1) body.frames = args.frames;
|
|
3392
|
+
if (args.convention) body.convention = args.convention;
|
|
3393
|
+
return validateResponse(
|
|
3394
|
+
RevisionJobSubmitSchema,
|
|
3395
|
+
await this.request("/normal-map", { method: "POST", body: JSON.stringify(body) }),
|
|
3396
|
+
"normal-map"
|
|
3397
|
+
);
|
|
3398
|
+
}
|
|
3132
3399
|
/**
|
|
3133
3400
|
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
3134
3401
|
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
@@ -3219,6 +3486,28 @@ var PixelLabClient = class {
|
|
|
3219
3486
|
"generate-font-pro"
|
|
3220
3487
|
);
|
|
3221
3488
|
}
|
|
3489
|
+
/**
|
|
3490
|
+
* `POST /vfx`: PixelLab's effects artist builds an animated effect from a
|
|
3491
|
+
* description, around an optional sprite, and saves it to My effects. With
|
|
3492
|
+
* `effectId` it changes that saved effect in place; adding the effect's
|
|
3493
|
+
* `conversation` prices the change as a change. A background job whose
|
|
3494
|
+
* 202 carries `quoted_generations`; PixelLab bills on delivery, up to
|
|
3495
|
+
* twice the quote. A 503 (renderer busy) is retried by the shared policy.
|
|
3496
|
+
*/
|
|
3497
|
+
async createVfx(args) {
|
|
3498
|
+
const body = { description: args.description };
|
|
3499
|
+
if (args.artist) body.artist = args.artist;
|
|
3500
|
+
if (args.sprite) body.sprite = { type: "base64", base64: args.sprite.base64, format: args.sprite.format };
|
|
3501
|
+
if (args.effectId) body.effect_id = args.effectId;
|
|
3502
|
+
if (args.conversation) body.conversation = args.conversation;
|
|
3503
|
+
if (args.name) body.name = args.name;
|
|
3504
|
+
if (args.gamePack) body.game_pack = true;
|
|
3505
|
+
return validateResponse(
|
|
3506
|
+
VfxSubmitSchema,
|
|
3507
|
+
await this.request("/vfx", { method: "POST", body: JSON.stringify(body) }),
|
|
3508
|
+
"vfx"
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3222
3511
|
/** `GET /generate-font-pro/{job_id}`: status, then no-auth atlas and `.ttf` URLs once completed. */
|
|
3223
3512
|
async getFontJob(jobId) {
|
|
3224
3513
|
const raw = await this.request(`/generate-font-pro/${encodeURIComponent(jobId)}`);
|
|
@@ -3818,6 +4107,41 @@ function parseHex(hex2) {
|
|
|
3818
4107
|
const n = parseInt(m[1], 16);
|
|
3819
4108
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
3820
4109
|
}
|
|
4110
|
+
function joinStripPng(frames) {
|
|
4111
|
+
const decoded = frames.map((frame) => decodePng(frame));
|
|
4112
|
+
const first = decoded[0];
|
|
4113
|
+
if (!first) throw new Error("a strip needs at least one frame");
|
|
4114
|
+
const { width, height } = first;
|
|
4115
|
+
for (const [index, frame] of decoded.entries()) {
|
|
4116
|
+
if (frame.width !== width || frame.height !== height) {
|
|
4117
|
+
throw new Error(`strip frame ${index} is ${frame.width}x${frame.height}; frame 0 is ${width}x${height}`);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
const stride = width * 4;
|
|
4121
|
+
const out = Buffer.alloc(stride * decoded.length * height);
|
|
4122
|
+
for (let y = 0; y < height; y++) {
|
|
4123
|
+
for (const [index, frame] of decoded.entries()) {
|
|
4124
|
+
frame.pixels.copy(out, (y * decoded.length + index) * stride, y * stride, (y + 1) * stride);
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
return encodeRgbaPng(width * decoded.length, height, out);
|
|
4128
|
+
}
|
|
4129
|
+
function splitStripPng(strip, frames) {
|
|
4130
|
+
const image = decodePng(strip);
|
|
4131
|
+
if (frames < 1 || image.width % frames !== 0) {
|
|
4132
|
+
throw new Error(`a ${image.width}x${image.height} strip does not split into ${frames} equal frames`);
|
|
4133
|
+
}
|
|
4134
|
+
const width = image.width / frames;
|
|
4135
|
+
const stride = width * 4;
|
|
4136
|
+
return Array.from({ length: frames }, (_, index) => {
|
|
4137
|
+
const out = Buffer.alloc(stride * image.height);
|
|
4138
|
+
for (let y = 0; y < image.height; y++) {
|
|
4139
|
+
const from = (y * image.width + index * width) * 4;
|
|
4140
|
+
image.pixels.copy(out, y * stride, from, from + stride);
|
|
4141
|
+
}
|
|
4142
|
+
return encodeRgbaPng(width, image.height, out);
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
3821
4145
|
|
|
3822
4146
|
// src/media.ts
|
|
3823
4147
|
var MediaType = {
|
|
@@ -3933,10 +4257,10 @@ function imageMetadata(buf) {
|
|
|
3933
4257
|
}
|
|
3934
4258
|
|
|
3935
4259
|
// src/providers/pixellab.ts
|
|
3936
|
-
var
|
|
4260
|
+
var import_node_fs4 = require("fs");
|
|
3937
4261
|
var import_node_crypto2 = require("crypto");
|
|
3938
4262
|
var import_node_os = __toESM(require("os"), 1);
|
|
3939
|
-
var
|
|
4263
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
3940
4264
|
|
|
3941
4265
|
// src/hash.ts
|
|
3942
4266
|
var import_node_crypto = require("crypto");
|
|
@@ -3972,6 +4296,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3972
4296
|
detail: effect ? void 0 : spec.detail ?? null,
|
|
3973
4297
|
seed: effect ? void 0 : spec.seed ?? null,
|
|
3974
4298
|
palette: effect ? void 0 : spec.palette,
|
|
4299
|
+
// `imagePro` only, and absent unless the style sets them.
|
|
4300
|
+
paletteName: spec.paletteName,
|
|
4301
|
+
maxColors: spec.maxColors,
|
|
3975
4302
|
// `noBackground` only reaches the wire for pixflux and uiAsset; the
|
|
3976
4303
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
3977
4304
|
// intentionally absent; width/height are derived from it, so it is
|
|
@@ -3986,6 +4313,25 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3986
4313
|
uiPieces: spec.uiPieces,
|
|
3987
4314
|
uiElements: spec.uiElements,
|
|
3988
4315
|
uiColorPalette: spec.uiColorPalette,
|
|
4316
|
+
// The older terrain knobs (terrainMode, terrainTransitionSize, and the
|
|
4317
|
+
// rest) were never part of this hash, and adding them now would make
|
|
4318
|
+
// every existing terrain entry stale. The Pro shape controls added
|
|
4319
|
+
// after them are identity, each only when set.
|
|
4320
|
+
terrainRoundness: spec.terrainRoundness,
|
|
4321
|
+
terrainBoundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
4322
|
+
terrainBoundarySeed: spec.terrainBoundarySeed,
|
|
4323
|
+
terrainStepSlope: spec.terrainStepSlope,
|
|
4324
|
+
terrainConnectDiagonals: spec.terrainConnectDiagonals,
|
|
4325
|
+
// "top-down" is the default and draws the same set as leaving it out.
|
|
4326
|
+
terrainOrientation: spec.terrainOrientation === "sidescroller" ? spec.terrainOrientation : void 0,
|
|
4327
|
+
// Which set and which of its terrains, not that terrain's id: the id
|
|
4328
|
+
// is in the lockfile, and the plan compares it there (like a mirror).
|
|
4329
|
+
terrainConnect: spec.terrainConnect ? Object.fromEntries(
|
|
4330
|
+
["lower", "upper"].flatMap((side) => {
|
|
4331
|
+
const link = spec.terrainConnect[side];
|
|
4332
|
+
return link ? [[side, { of: link.assetId, terrain: link.terrain }]] : [];
|
|
4333
|
+
})
|
|
4334
|
+
) : void 0,
|
|
3989
4335
|
// Scene and mask enter as their bytes' hashes, never their paths.
|
|
3990
4336
|
scene: spec.scene ? {
|
|
3991
4337
|
image: spec.scene.sha256,
|
|
@@ -4047,13 +4393,17 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
4047
4393
|
reference: spec.objectPro.reference?.sha256
|
|
4048
4394
|
} : void 0,
|
|
4049
4395
|
// A recipe and a local sprite enter as their contents' hashes, never
|
|
4050
|
-
// their paths; the My effects name is not identity.
|
|
4396
|
+
// their paths; the My effects name is not identity. `description` and
|
|
4397
|
+
// `artist` exist only for the paid VFX artist and stay absent for a
|
|
4398
|
+
// workbench effect, so its hash is the one it always had.
|
|
4051
4399
|
vfx: spec.vfx ? {
|
|
4052
4400
|
preset: spec.vfx.preset,
|
|
4053
4401
|
recipe: spec.vfx.recipe?.sha256,
|
|
4054
4402
|
parts: spec.vfx.parts,
|
|
4055
4403
|
fps: spec.vfx.fps,
|
|
4056
|
-
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
|
|
4404
|
+
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0,
|
|
4405
|
+
description: spec.vfx.description,
|
|
4406
|
+
artist: spec.vfx.description ? spec.vfx.artist : void 0
|
|
4057
4407
|
} : void 0,
|
|
4058
4408
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
4059
4409
|
// compares those directly, so only the choice of source is identity.
|
|
@@ -4083,6 +4433,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
4083
4433
|
skeletonTemplate: spec.revision.skeletonTemplate,
|
|
4084
4434
|
description: spec.revision.description,
|
|
4085
4435
|
removalTask: spec.revision.removalTask,
|
|
4436
|
+
convention: spec.revision.convention,
|
|
4086
4437
|
driftThreshold: spec.revision.driftThreshold,
|
|
4087
4438
|
subjectDescription: spec.revision.subjectDescription,
|
|
4088
4439
|
initialPose: spec.revision.initialPose,
|
|
@@ -4229,7 +4580,8 @@ function vfxOutputPaths(out) {
|
|
|
4229
4580
|
gif: `${base}.gif`,
|
|
4230
4581
|
atlas: `${base}.json`,
|
|
4231
4582
|
provenance: `${base}.vfx.json`,
|
|
4232
|
-
gamePack: `${base}-game.zip
|
|
4583
|
+
gamePack: `${base}-game.zip`,
|
|
4584
|
+
normals: `${base}-normals.png`
|
|
4233
4585
|
};
|
|
4234
4586
|
}
|
|
4235
4587
|
function isVfxReference(sprite) {
|
|
@@ -4319,10 +4671,17 @@ function vfxRecipeSha256(recipe) {
|
|
|
4319
4671
|
const { name: _name, notes: _notes, ...rest } = recipe;
|
|
4320
4672
|
return sha256(JSON.stringify(rest));
|
|
4321
4673
|
}
|
|
4674
|
+
function vfxAtlasFrames(atlas) {
|
|
4675
|
+
const listed = atlas?.frames;
|
|
4676
|
+
const entries = Array.isArray(listed) ? listed : listed && typeof listed === "object" ? Object.values(listed) : null;
|
|
4677
|
+
if (!entries?.length) return null;
|
|
4678
|
+
const rects = entries.map((entry) => entry?.frame);
|
|
4679
|
+
return rects.every((rect) => rect && [rect.x, rect.y, rect.w, rect.h].every((n) => typeof n === "number")) ? rects : null;
|
|
4680
|
+
}
|
|
4322
4681
|
function sliceVfxSheet(sheet, facts, atlas) {
|
|
4323
4682
|
const image = decodePng(sheet);
|
|
4324
|
-
const listed = atlas
|
|
4325
|
-
const rects =
|
|
4683
|
+
const listed = vfxAtlasFrames(atlas);
|
|
4684
|
+
const rects = listed && listed.length === facts.frames ? listed : Array.from({ length: facts.frames }, (_, index) => {
|
|
4326
4685
|
const columns = Math.max(1, Math.floor(image.width / facts.width));
|
|
4327
4686
|
return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
|
|
4328
4687
|
});
|
|
@@ -4382,45 +4741,516 @@ async function makeVfx(client2, opts) {
|
|
|
4382
4741
|
bytes = Buffer.from(`${JSON.stringify(atlas, null, 2)}
|
|
4383
4742
|
`);
|
|
4384
4743
|
}
|
|
4385
|
-
await (0, import_promises.writeFile)(target, bytes);
|
|
4386
|
-
if (target === paths.gamePack) written.gamePack = target;
|
|
4744
|
+
await (0, import_promises.writeFile)(target, bytes);
|
|
4745
|
+
if (target === paths.gamePack) written.gamePack = target;
|
|
4746
|
+
}
|
|
4747
|
+
for (const file of [paths.sheet, paths.gif, paths.atlas]) {
|
|
4748
|
+
if (!(0, import_node_fs2.existsSync)(file)) throw new Error(`PixelLab's export listed no file for ${import_node_path.default.basename(file)}: ${exported.files.join(", ")}`);
|
|
4749
|
+
}
|
|
4750
|
+
const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : import_node_path.default.relative(import_node_path.default.dirname(paths.provenance), import_node_path.default.resolve(opts.sprite)).split(import_node_path.default.sep).join("/");
|
|
4751
|
+
const provenance = {
|
|
4752
|
+
version: 1,
|
|
4753
|
+
provider: "pixellab",
|
|
4754
|
+
tool: VFX_TOOL,
|
|
4755
|
+
name,
|
|
4756
|
+
effectId: exported.effectId,
|
|
4757
|
+
editUrl: exported.editUrl,
|
|
4758
|
+
source: {
|
|
4759
|
+
...opts.preset ? { preset: opts.preset } : {},
|
|
4760
|
+
...opts.effect ? { effect: opts.effect } : {},
|
|
4761
|
+
...opts.recipe ? { recipeFile: true } : {},
|
|
4762
|
+
...opts.parts?.length ? { parts: opts.parts } : {},
|
|
4763
|
+
...spriteRecord ? { sprite: spriteRecord } : {}
|
|
4764
|
+
},
|
|
4765
|
+
width: exported.width,
|
|
4766
|
+
height: exported.height,
|
|
4767
|
+
frames: exported.frames,
|
|
4768
|
+
fps: exported.fps,
|
|
4769
|
+
loop: exported.loop,
|
|
4770
|
+
anchor: exported.anchor,
|
|
4771
|
+
files: {
|
|
4772
|
+
sheet: import_node_path.default.basename(paths.sheet),
|
|
4773
|
+
gif: import_node_path.default.basename(paths.gif),
|
|
4774
|
+
atlas: import_node_path.default.basename(paths.atlas),
|
|
4775
|
+
...written.gamePack ? { gamePack: import_node_path.default.basename(written.gamePack) } : {}
|
|
4776
|
+
},
|
|
4777
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4778
|
+
recipe
|
|
4779
|
+
};
|
|
4780
|
+
await (0, import_promises.writeFile)(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
4781
|
+
`);
|
|
4782
|
+
return { ...exported, paths: written, recipe };
|
|
4783
|
+
}
|
|
4784
|
+
var VFX_ARTISTS = ["lite", "pro"];
|
|
4785
|
+
var VFX_ARTIST_MAX_SPRITE = 512;
|
|
4786
|
+
function vfxArtistQuote(artist, sprite, change = false) {
|
|
4787
|
+
const table = {
|
|
4788
|
+
lite: { new: [1, 1, 3, 5], change: [1, 1, 2, 2] },
|
|
4789
|
+
pro: { new: [6, 8, 14, 28], change: [4, 4, 5, 8] }
|
|
4790
|
+
};
|
|
4791
|
+
const tier = sprite === "none" ? 0 : sprite === "unknown" ? 3 : sprite <= 128 ? 1 : sprite <= 256 ? 2 : 3;
|
|
4792
|
+
return table[artist][change ? "change" : "new"][tier];
|
|
4793
|
+
}
|
|
4794
|
+
function vfxArtistCeiling(quote) {
|
|
4795
|
+
return quote * 2;
|
|
4796
|
+
}
|
|
4797
|
+
function vfxArtistSprite(bytes, label) {
|
|
4798
|
+
const metadata = imageMetadata(bytes);
|
|
4799
|
+
if (metadata?.format !== "png") throw new Error(`${label}: PixelLab's VFX artist takes a PNG sprite`);
|
|
4800
|
+
const side = Math.max(metadata.width, metadata.height);
|
|
4801
|
+
if (side > VFX_ARTIST_MAX_SPRITE) {
|
|
4802
|
+
throw new Error(
|
|
4803
|
+
`${label} is ${metadata.width}x${metadata.height}; PixelLab's VFX artist takes up to ${VFX_ARTIST_MAX_SPRITE}x${VFX_ARTIST_MAX_SPRITE} (crop or pad it; never rescale pixel art)`
|
|
4804
|
+
);
|
|
4805
|
+
}
|
|
4806
|
+
return { image: { base64: bytes.toString("base64"), format: "png" }, side };
|
|
4807
|
+
}
|
|
4808
|
+
function vfxArtistFiles(raw) {
|
|
4809
|
+
const urlOf = (value) => {
|
|
4810
|
+
if (typeof value === "string") return /^https?:\/\//i.test(value) ? value : null;
|
|
4811
|
+
if (!value || typeof value !== "object") return null;
|
|
4812
|
+
for (const key of ["url", "download_url", "href"]) {
|
|
4813
|
+
const url = value[key];
|
|
4814
|
+
if (typeof url === "string" && /^https?:\/\//i.test(url)) return url;
|
|
4815
|
+
}
|
|
4816
|
+
return null;
|
|
4817
|
+
};
|
|
4818
|
+
const nameOf = (value, url, key) => {
|
|
4819
|
+
if (value && typeof value === "object") {
|
|
4820
|
+
for (const field of ["name", "filename", "file"]) {
|
|
4821
|
+
const name = value[field];
|
|
4822
|
+
if (typeof name === "string" && name) return name;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
if (key && /\.[a-z0-9]+$/i.test(key)) return key;
|
|
4826
|
+
return decodeURIComponent(new URL(url).pathname.split("/").pop() ?? "");
|
|
4827
|
+
};
|
|
4828
|
+
const entries = Array.isArray(raw) ? raw.map((value) => [void 0, value]) : raw && typeof raw === "object" ? Object.entries(raw) : [];
|
|
4829
|
+
const files = [];
|
|
4830
|
+
for (const [key, value] of entries) {
|
|
4831
|
+
const url = urlOf(value);
|
|
4832
|
+
if (url) files.push({ name: nameOf(value, url, key), url });
|
|
4833
|
+
}
|
|
4834
|
+
return files;
|
|
4835
|
+
}
|
|
4836
|
+
function parseVfxArtistResult(done, jobId) {
|
|
4837
|
+
const files = vfxArtistFiles(done.files);
|
|
4838
|
+
const pick = (test, skip) => files.find((file) => test.test(file.name) && !skip?.test(file.name)) ?? null;
|
|
4839
|
+
const normals = /-normals?\.png$/i;
|
|
4840
|
+
const sheet = pick(/\.png$/i, normals);
|
|
4841
|
+
const effectId = typeof done.effect_id === "string" && done.effect_id ? done.effect_id : null;
|
|
4842
|
+
if (!effectId || !sheet) {
|
|
4843
|
+
throw new Error(
|
|
4844
|
+
`Invalid PixelLab response for vfx job ${jobId}: completed without ${effectId ? "a spritesheet among its files" : "an effect_id"} (last_response keys: ${Object.keys(done).join(", ") || "none"}; files: ${JSON.stringify(done.files ?? null).slice(0, 400)}); update parseVfxArtistResult in src/pixellab-vfx.ts with the real shape`
|
|
4845
|
+
);
|
|
4846
|
+
}
|
|
4847
|
+
const number = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4848
|
+
const text = (value) => typeof value === "string" && value ? value : null;
|
|
4849
|
+
const canvas = done.canvas ?? {};
|
|
4850
|
+
const anchor = done.anchor;
|
|
4851
|
+
return {
|
|
4852
|
+
effectId,
|
|
4853
|
+
name: text(done.name),
|
|
4854
|
+
editUrl: text(done.edit_url),
|
|
4855
|
+
message: text(done.message),
|
|
4856
|
+
width: number(canvas.w),
|
|
4857
|
+
height: number(canvas.h),
|
|
4858
|
+
frames: number(done.frames),
|
|
4859
|
+
fps: number(done.fps),
|
|
4860
|
+
loop: typeof done.loop === "boolean" ? done.loop : null,
|
|
4861
|
+
anchor: anchor && number(anchor.x) !== null && number(anchor.y) !== null ? { kind: text(anchor.kind), x: anchor.x, y: anchor.y } : null,
|
|
4862
|
+
parts: done.parts ?? null,
|
|
4863
|
+
conversation: text(done.conversation),
|
|
4864
|
+
files,
|
|
4865
|
+
sheet,
|
|
4866
|
+
atlas: pick(/\.json$/i),
|
|
4867
|
+
gif: pick(/\.gif$/i),
|
|
4868
|
+
gamePack: pick(/\.zip$/i),
|
|
4869
|
+
normals: pick(normals)
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
function vfxArtistFrameFacts(result, atlas) {
|
|
4873
|
+
const rects = vfxAtlasFrames(atlas);
|
|
4874
|
+
const frames = result.frames ?? rects?.length ?? null;
|
|
4875
|
+
const width = result.width ?? rects?.[0]?.w ?? null;
|
|
4876
|
+
const height = result.height ?? rects?.[0]?.h ?? null;
|
|
4877
|
+
if (!frames || !width || !height) {
|
|
4878
|
+
throw new Error(`PixelLab vfx effect ${result.effectId} came back with no frame size or count, in the result or its atlas`);
|
|
4879
|
+
}
|
|
4880
|
+
return { width, height, frames };
|
|
4881
|
+
}
|
|
4882
|
+
async function makeVfxWithArtist(client2, opts) {
|
|
4883
|
+
const paths = vfxOutputPaths(opts.out);
|
|
4884
|
+
if (!opts.force) {
|
|
4885
|
+
for (const file of [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []]) {
|
|
4886
|
+
if ((0, import_node_fs2.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
const progress = opts.onProgress ?? (() => {
|
|
4890
|
+
});
|
|
4891
|
+
const sprite = opts.sprite ? vfxArtistSprite(await (0, import_promises.readFile)(opts.sprite), opts.sprite).image : void 0;
|
|
4892
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
4893
|
+
const interval = opts.intervalMs ?? 6e3;
|
|
4894
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
4895
|
+
const submitted = await client2.createVfx({
|
|
4896
|
+
description: opts.description,
|
|
4897
|
+
artist: opts.artist,
|
|
4898
|
+
sprite,
|
|
4899
|
+
effectId: opts.effect,
|
|
4900
|
+
conversation: opts.effect ? opts.conversation : void 0,
|
|
4901
|
+
name: opts.name,
|
|
4902
|
+
gamePack: opts.gamePack
|
|
4903
|
+
});
|
|
4904
|
+
const jobId = submitted.background_job_id;
|
|
4905
|
+
progress(`submitted job ${jobId}, quoted ${submitted.quoted_generations ?? "?"} generations`);
|
|
4906
|
+
const started = Date.now();
|
|
4907
|
+
let job;
|
|
4908
|
+
for (; ; ) {
|
|
4909
|
+
job = await client2.getBackgroundJob(jobId);
|
|
4910
|
+
if (job.status === "completed") break;
|
|
4911
|
+
if (job.status === "failed") throw new Error(`PixelLab vfx job ${jobId} failed upstream${vfxJobFailure(job.last_response)}`);
|
|
4912
|
+
if (Date.now() - started > timeout) {
|
|
4913
|
+
throw new Error(
|
|
4914
|
+
`PixelLab vfx job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream (and bills when delivered), but this command has stopped waiting`
|
|
4915
|
+
);
|
|
4916
|
+
}
|
|
4917
|
+
progress(job.status);
|
|
4918
|
+
await sleep3(interval);
|
|
4919
|
+
}
|
|
4920
|
+
const result = parseVfxArtistResult(job.last_response ?? {}, jobId);
|
|
4921
|
+
await (0, import_promises.mkdir)(import_node_path.default.dirname(paths.sheet), { recursive: true });
|
|
4922
|
+
const written = { sheet: paths.sheet, provenance: paths.provenance };
|
|
4923
|
+
await (0, import_promises.writeFile)(paths.sheet, await client2.download(result.sheet.url));
|
|
4924
|
+
let atlas;
|
|
4925
|
+
if (result.atlas) {
|
|
4926
|
+
atlas = JSON.parse((await client2.download(result.atlas.url)).toString("utf8"));
|
|
4927
|
+
const meta = atlas.meta;
|
|
4928
|
+
if (meta) meta.image = import_node_path.default.basename(paths.sheet);
|
|
4929
|
+
await (0, import_promises.writeFile)(paths.atlas, `${JSON.stringify(atlas, null, 2)}
|
|
4930
|
+
`);
|
|
4931
|
+
written.atlas = paths.atlas;
|
|
4932
|
+
}
|
|
4933
|
+
for (const [file, target, key] of [
|
|
4934
|
+
[result.gif, paths.gif, "gif"],
|
|
4935
|
+
[result.gamePack, paths.gamePack, "gamePack"],
|
|
4936
|
+
[result.normals, paths.normals, "normals"]
|
|
4937
|
+
]) {
|
|
4938
|
+
if (!file) continue;
|
|
4939
|
+
await (0, import_promises.writeFile)(target, await client2.download(file.url));
|
|
4940
|
+
written[key] = target;
|
|
4941
|
+
}
|
|
4942
|
+
const facts = vfxArtistFrameFacts(result, atlas);
|
|
4943
|
+
const usage = job.usage ?? null;
|
|
4944
|
+
const provenance = {
|
|
4945
|
+
version: 1,
|
|
4946
|
+
provider: "pixellab",
|
|
4947
|
+
tool: "vfx",
|
|
4948
|
+
artist: opts.artist ?? "lite",
|
|
4949
|
+
description: opts.description,
|
|
4950
|
+
name: result.name,
|
|
4951
|
+
effectId: result.effectId,
|
|
4952
|
+
editUrl: result.editUrl,
|
|
4953
|
+
message: result.message,
|
|
4954
|
+
source: {
|
|
4955
|
+
...opts.sprite ? { sprite: import_node_path.default.relative(import_node_path.default.dirname(paths.provenance), import_node_path.default.resolve(opts.sprite)).split(import_node_path.default.sep).join("/") } : {},
|
|
4956
|
+
...opts.effect ? { changed: opts.effect } : {}
|
|
4957
|
+
},
|
|
4958
|
+
...facts,
|
|
4959
|
+
fps: result.fps,
|
|
4960
|
+
loop: result.loop,
|
|
4961
|
+
anchor: result.anchor,
|
|
4962
|
+
parts: result.parts,
|
|
4963
|
+
files: Object.fromEntries(
|
|
4964
|
+
["sheet", "gif", "atlas", "gamePack", "normals"].filter((key) => written[key]).map((key) => [key, import_node_path.default.basename(written[key])])
|
|
4965
|
+
),
|
|
4966
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
4967
|
+
usage,
|
|
4968
|
+
conversation: result.conversation,
|
|
4969
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4970
|
+
};
|
|
4971
|
+
await (0, import_promises.writeFile)(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
4972
|
+
`);
|
|
4973
|
+
return {
|
|
4974
|
+
jobId,
|
|
4975
|
+
effectId: result.effectId,
|
|
4976
|
+
editUrl: result.editUrl,
|
|
4977
|
+
name: result.name,
|
|
4978
|
+
message: result.message,
|
|
4979
|
+
...facts,
|
|
4980
|
+
fps: result.fps,
|
|
4981
|
+
loop: result.loop,
|
|
4982
|
+
anchor: result.anchor,
|
|
4983
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
4984
|
+
usage,
|
|
4985
|
+
paths: written
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
function vfxJobFailure(done) {
|
|
4989
|
+
for (const key of ["error", "detail", "message"]) {
|
|
4990
|
+
const value = done?.[key];
|
|
4991
|
+
if (typeof value === "string" && value) return `: ${value}`;
|
|
4992
|
+
}
|
|
4993
|
+
return "";
|
|
4994
|
+
}
|
|
4995
|
+
async function readVfxConversation(file, effectId) {
|
|
4996
|
+
if (!(0, import_node_fs2.existsSync)(file)) return void 0;
|
|
4997
|
+
try {
|
|
4998
|
+
const record = JSON.parse(await (0, import_promises.readFile)(file, "utf8"));
|
|
4999
|
+
return record.effectId === effectId && typeof record.conversation === "string" ? record.conversation : void 0;
|
|
5000
|
+
} catch {
|
|
5001
|
+
return void 0;
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
// src/source-url.ts
|
|
5006
|
+
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
5007
|
+
"access_token",
|
|
5008
|
+
"apikey",
|
|
5009
|
+
"api_key",
|
|
5010
|
+
"auth",
|
|
5011
|
+
"authorization",
|
|
5012
|
+
"awsaccesskeyid",
|
|
5013
|
+
"googleaccessid",
|
|
5014
|
+
"key",
|
|
5015
|
+
"sig"
|
|
5016
|
+
]);
|
|
5017
|
+
function isSensitiveSourceUrl(value) {
|
|
5018
|
+
let parsed;
|
|
5019
|
+
try {
|
|
5020
|
+
parsed = new URL(value);
|
|
5021
|
+
} catch {
|
|
5022
|
+
return false;
|
|
5023
|
+
}
|
|
5024
|
+
if (parsed.username || parsed.password) return true;
|
|
5025
|
+
for (const key of parsed.searchParams.keys()) {
|
|
5026
|
+
const normalized = key.toLowerCase();
|
|
5027
|
+
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
5028
|
+
}
|
|
5029
|
+
return false;
|
|
5030
|
+
}
|
|
5031
|
+
function shouldPersistSourceUrl(value) {
|
|
5032
|
+
let parsed;
|
|
5033
|
+
try {
|
|
5034
|
+
parsed = new URL(value);
|
|
5035
|
+
} catch {
|
|
5036
|
+
return true;
|
|
5037
|
+
}
|
|
5038
|
+
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
5039
|
+
return !isSensitiveSourceUrl(value);
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
// src/pixellab-utils.ts
|
|
5043
|
+
var import_node_fs3 = require("fs");
|
|
5044
|
+
var import_promises2 = require("fs/promises");
|
|
5045
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
5046
|
+
var UNZOOM_MIN_SIDE = 256;
|
|
5047
|
+
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
5048
|
+
function defaultUnzoomOut(input) {
|
|
5049
|
+
const ext = import_node_path2.default.extname(input);
|
|
5050
|
+
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
5051
|
+
}
|
|
5052
|
+
async function unzoomFile(client2, input, opts = {}) {
|
|
5053
|
+
const out = import_node_path2.default.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
5054
|
+
if (import_node_path2.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
5055
|
+
if ((0, import_node_fs3.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
5056
|
+
const bytes = await (0, import_promises2.readFile)(input);
|
|
5057
|
+
const metadata = imageMetadata(bytes);
|
|
5058
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
5059
|
+
const { width, height, format } = metadata;
|
|
5060
|
+
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
5061
|
+
throw new Error(
|
|
5062
|
+
`${input} is ${width}x${height}; PixelLab's unzoom needs at least ${UNZOOM_MIN_SIDE}x${UNZOOM_MIN_SIDE} to find the pixel grid, so this is probably already at its native size`
|
|
5063
|
+
);
|
|
5064
|
+
}
|
|
5065
|
+
if (width * height > UNZOOM_MAX_AREA) {
|
|
5066
|
+
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
5067
|
+
}
|
|
5068
|
+
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
5069
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(out), { recursive: true });
|
|
5070
|
+
await (0, import_promises2.writeFile)(out, res.png);
|
|
5071
|
+
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
5072
|
+
}
|
|
5073
|
+
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
5074
|
+
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
5075
|
+
var FONT_COST_GENERATIONS = 15;
|
|
5076
|
+
function fontOutputPaths(out) {
|
|
5077
|
+
const base = import_node_path2.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
5078
|
+
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
5079
|
+
}
|
|
5080
|
+
async function generateFont(client2, opts) {
|
|
5081
|
+
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
5082
|
+
if (!opts.force) {
|
|
5083
|
+
for (const file of [ttf, atlas]) {
|
|
5084
|
+
if ((0, import_node_fs3.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
5088
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
5089
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
5090
|
+
const submitted = await client2.generateFontPro({
|
|
5091
|
+
description: opts.description,
|
|
5092
|
+
weight: opts.weight,
|
|
5093
|
+
glyphPx: opts.glyphPx,
|
|
5094
|
+
seed: opts.seed,
|
|
5095
|
+
fontName: opts.fontName
|
|
5096
|
+
});
|
|
5097
|
+
const jobId = submitted.background_job_id;
|
|
5098
|
+
const started = Date.now();
|
|
5099
|
+
for (; ; ) {
|
|
5100
|
+
const job = await client2.getFontJob(jobId);
|
|
5101
|
+
opts.onProgress?.(job.status);
|
|
5102
|
+
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
5103
|
+
if (job.status === "completed") {
|
|
5104
|
+
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
5105
|
+
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
5106
|
+
}
|
|
5107
|
+
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
5108
|
+
client2.download(job.download_ttf_url),
|
|
5109
|
+
client2.download(job.download_atlas_url)
|
|
5110
|
+
]);
|
|
5111
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(ttf), { recursive: true });
|
|
5112
|
+
await (0, import_promises2.writeFile)(ttf, ttfBytes);
|
|
5113
|
+
await (0, import_promises2.writeFile)(atlas, atlasBytes);
|
|
5114
|
+
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
5115
|
+
}
|
|
5116
|
+
if (Date.now() - started > timeout) {
|
|
5117
|
+
throw new Error(
|
|
5118
|
+
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
await sleep3(interval);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
var PIXELATE_COST_GENERATIONS = 6;
|
|
5125
|
+
var PIXELATE_MAX_SIDE = 2048;
|
|
5126
|
+
function defaultPixelateOut(input) {
|
|
5127
|
+
const ext = import_node_path2.default.extname(input);
|
|
5128
|
+
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
5129
|
+
}
|
|
5130
|
+
async function pixelateFile(client2, input, opts = {}) {
|
|
5131
|
+
const out = import_node_path2.default.resolve(opts.out ?? defaultPixelateOut(input));
|
|
5132
|
+
if (import_node_path2.default.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
5133
|
+
if ((0, import_node_fs3.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
5134
|
+
const bytes = await (0, import_promises2.readFile)(input);
|
|
5135
|
+
const metadata = imageMetadata(bytes);
|
|
5136
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
5137
|
+
const { width, height, format } = metadata;
|
|
5138
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
5139
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
5140
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
5141
|
+
const submitted = await client2.imageToPixelartProFlash({
|
|
5142
|
+
image: { base64: bytes.toString("base64"), format },
|
|
5143
|
+
description: opts.description,
|
|
5144
|
+
seed: opts.seed
|
|
5145
|
+
});
|
|
5146
|
+
const jobId = submitted.background_job_id;
|
|
5147
|
+
const started = Date.now();
|
|
5148
|
+
for (; ; ) {
|
|
5149
|
+
const job = await client2.getBackgroundJob(jobId);
|
|
5150
|
+
opts.onProgress?.(job.status);
|
|
5151
|
+
if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
|
|
5152
|
+
if (job.status === "completed") {
|
|
5153
|
+
const images = job.last_response?.images;
|
|
5154
|
+
const first = Array.isArray(images) ? images[0] : void 0;
|
|
5155
|
+
if (!first || typeof first.base64 !== "string" || !first.base64) {
|
|
5156
|
+
throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
|
|
5157
|
+
}
|
|
5158
|
+
const png = Buffer.from(first.base64, "base64");
|
|
5159
|
+
const size = imageMetadata(png);
|
|
5160
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(out), { recursive: true });
|
|
5161
|
+
await (0, import_promises2.writeFile)(out, png);
|
|
5162
|
+
return {
|
|
5163
|
+
jobId,
|
|
5164
|
+
out,
|
|
5165
|
+
original: { width, height },
|
|
5166
|
+
pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
|
|
5167
|
+
imageId: submitted.image_id,
|
|
5168
|
+
usage: job.usage ?? null
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
if (Date.now() - started > timeout) {
|
|
5172
|
+
throw new Error(
|
|
5173
|
+
`PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
await sleep3(interval);
|
|
4387
5177
|
}
|
|
4388
|
-
|
|
4389
|
-
|
|
5178
|
+
}
|
|
5179
|
+
var NORMAL_MAP_COST_GENERATIONS = 0.5;
|
|
5180
|
+
var NORMAL_MAP_MIN_SIDE = 8;
|
|
5181
|
+
var NORMAL_MAP_MAX_SIDE = 512;
|
|
5182
|
+
var NORMAL_MAP_MAX_FRAMES = 32;
|
|
5183
|
+
var NORMAL_MAP_CONVENTIONS = ["opengl", "directx"];
|
|
5184
|
+
function normalMapSizeProblem(width, height, frames = 1) {
|
|
5185
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > NORMAL_MAP_MAX_FRAMES) {
|
|
5186
|
+
return `PixelLab's normal map takes 1 to ${NORMAL_MAP_MAX_FRAMES} frames, not ${frames}`;
|
|
5187
|
+
}
|
|
5188
|
+
if (width % frames !== 0) return `a ${width}px-wide strip does not split into ${frames} equal frames`;
|
|
5189
|
+
const frameWidth = width / frames;
|
|
5190
|
+
if (frameWidth < NORMAL_MAP_MIN_SIDE || height < NORMAL_MAP_MIN_SIDE || frameWidth > NORMAL_MAP_MAX_SIDE || height > NORMAL_MAP_MAX_SIDE) {
|
|
5191
|
+
return `${frames > 1 ? "each frame is " : "it is "}${frameWidth}x${height}; PixelLab's normal map takes ${NORMAL_MAP_MIN_SIDE} to ${NORMAL_MAP_MAX_SIDE} px per side`;
|
|
5192
|
+
}
|
|
5193
|
+
return null;
|
|
5194
|
+
}
|
|
5195
|
+
function defaultNormalMapOut(input) {
|
|
5196
|
+
const ext = import_node_path2.default.extname(input);
|
|
5197
|
+
return `${ext ? input.slice(0, -ext.length) : input}.normal.png`;
|
|
5198
|
+
}
|
|
5199
|
+
function normalMapResultPng(lastResponse) {
|
|
5200
|
+
const image = lastResponse?.image;
|
|
5201
|
+
const base64 = typeof image === "string" ? image : image?.base64;
|
|
5202
|
+
if (typeof base64 !== "string" || !base64) return null;
|
|
5203
|
+
return Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ""), "base64");
|
|
5204
|
+
}
|
|
5205
|
+
async function normalMapFile(client2, input, opts = {}) {
|
|
5206
|
+
const out = import_node_path2.default.resolve(opts.out ?? defaultNormalMapOut(input));
|
|
5207
|
+
if (import_node_path2.default.resolve(input) === out) throw new Error("normal-map will not overwrite its own input; pass a different --out");
|
|
5208
|
+
if ((0, import_node_fs3.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
5209
|
+
const bytes = await (0, import_promises2.readFile)(input);
|
|
5210
|
+
const metadata = imageMetadata(bytes);
|
|
5211
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
5212
|
+
if (metadata.format !== "png") throw new Error(`${input} is a JPEG; normal-map reads a PNG, whose alpha marks where the sprite is`);
|
|
5213
|
+
const { width, height } = metadata;
|
|
5214
|
+
const frames = opts.frames ?? 1;
|
|
5215
|
+
const problem = normalMapSizeProblem(width, height, frames);
|
|
5216
|
+
if (problem) throw new Error(`${input}: ${problem}`);
|
|
5217
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
5218
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
5219
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
5220
|
+
const submitted = await client2.normalMap({
|
|
5221
|
+
image: { base64: bytes.toString("base64"), format: "png" },
|
|
5222
|
+
frames,
|
|
5223
|
+
convention: opts.convention
|
|
5224
|
+
});
|
|
5225
|
+
const jobId = submitted.background_job_id;
|
|
5226
|
+
const started = Date.now();
|
|
5227
|
+
for (; ; ) {
|
|
5228
|
+
const job = await client2.getBackgroundJob(jobId);
|
|
5229
|
+
opts.onProgress?.(job.status);
|
|
5230
|
+
if (job.status === "failed") throw new Error(`PixelLab normal-map job ${jobId} failed upstream`);
|
|
5231
|
+
if (job.status === "completed") {
|
|
5232
|
+
const png = normalMapResultPng(job.last_response);
|
|
5233
|
+
if (!png) throw new Error(`PixelLab normal-map job ${jobId} completed without an image at last_response.image`);
|
|
5234
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(out), { recursive: true });
|
|
5235
|
+
await (0, import_promises2.writeFile)(out, png);
|
|
5236
|
+
const size = imageMetadata(png);
|
|
5237
|
+
return {
|
|
5238
|
+
jobId,
|
|
5239
|
+
out,
|
|
5240
|
+
width: size?.width ?? width,
|
|
5241
|
+
height: size?.height ?? height,
|
|
5242
|
+
frames,
|
|
5243
|
+
convention: opts.convention ?? "opengl",
|
|
5244
|
+
usage: job.usage ?? null
|
|
5245
|
+
};
|
|
5246
|
+
}
|
|
5247
|
+
if (Date.now() - started > timeout) {
|
|
5248
|
+
throw new Error(
|
|
5249
|
+
`PixelLab normal-map job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
await sleep3(interval);
|
|
4390
5253
|
}
|
|
4391
|
-
const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : import_node_path.default.relative(import_node_path.default.dirname(paths.provenance), import_node_path.default.resolve(opts.sprite)).split(import_node_path.default.sep).join("/");
|
|
4392
|
-
const provenance = {
|
|
4393
|
-
version: 1,
|
|
4394
|
-
provider: "pixellab",
|
|
4395
|
-
tool: VFX_TOOL,
|
|
4396
|
-
name,
|
|
4397
|
-
effectId: exported.effectId,
|
|
4398
|
-
editUrl: exported.editUrl,
|
|
4399
|
-
source: {
|
|
4400
|
-
...opts.preset ? { preset: opts.preset } : {},
|
|
4401
|
-
...opts.effect ? { effect: opts.effect } : {},
|
|
4402
|
-
...opts.recipe ? { recipeFile: true } : {},
|
|
4403
|
-
...opts.parts?.length ? { parts: opts.parts } : {},
|
|
4404
|
-
...spriteRecord ? { sprite: spriteRecord } : {}
|
|
4405
|
-
},
|
|
4406
|
-
width: exported.width,
|
|
4407
|
-
height: exported.height,
|
|
4408
|
-
frames: exported.frames,
|
|
4409
|
-
fps: exported.fps,
|
|
4410
|
-
loop: exported.loop,
|
|
4411
|
-
anchor: exported.anchor,
|
|
4412
|
-
files: {
|
|
4413
|
-
sheet: import_node_path.default.basename(paths.sheet),
|
|
4414
|
-
gif: import_node_path.default.basename(paths.gif),
|
|
4415
|
-
atlas: import_node_path.default.basename(paths.atlas),
|
|
4416
|
-
...written.gamePack ? { gamePack: import_node_path.default.basename(written.gamePack) } : {}
|
|
4417
|
-
},
|
|
4418
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4419
|
-
recipe
|
|
4420
|
-
};
|
|
4421
|
-
await (0, import_promises.writeFile)(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
4422
|
-
`);
|
|
4423
|
-
return { ...exported, paths: written, recipe };
|
|
4424
5254
|
}
|
|
4425
5255
|
|
|
4426
5256
|
// src/providers/pixellab.ts
|
|
@@ -4440,6 +5270,20 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
4440
5270
|
if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
|
|
4441
5271
|
return null;
|
|
4442
5272
|
}
|
|
5273
|
+
function vfxArtistSpecQuote(spec) {
|
|
5274
|
+
const sprite = spec.vfx?.sprite;
|
|
5275
|
+
const side = !sprite ? "none" : "file" in sprite && sprite.width != null && sprite.height != null ? Math.max(sprite.width, sprite.height) : "unknown";
|
|
5276
|
+
return vfxArtistQuote(spec.vfx?.artist ?? "lite", side);
|
|
5277
|
+
}
|
|
5278
|
+
function workbenchVfxUrls(exported) {
|
|
5279
|
+
const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
|
|
5280
|
+
const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
|
|
5281
|
+
if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
|
|
5282
|
+
return {
|
|
5283
|
+
sheet: `${exported.downloadBase}${encodeURIComponent(sheetFile)}`,
|
|
5284
|
+
...atlasFile ? { atlas: `${exported.downloadBase}${encodeURIComponent(atlasFile)}` } : {}
|
|
5285
|
+
};
|
|
5286
|
+
}
|
|
4443
5287
|
function billedFromUsage(usage) {
|
|
4444
5288
|
if (!usage) return null;
|
|
4445
5289
|
if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
|
|
@@ -4594,13 +5438,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4594
5438
|
* source from a supplied per-frame skeleton instead of a text description)
|
|
4595
5439
|
* — and `interpolate` (`/interpolation-v2`, the in-betweens
|
|
4596
5440
|
* from one keyframe to another) and `edit-animation`
|
|
4597
|
-
* (`/edit-animation-v2`, one text edit applied across a whole frame set)
|
|
5441
|
+
* (`/edit-animation-v2`, one text edit applied across a whole frame set),
|
|
5442
|
+
* and `normal-map` (`/normal-map`, lighting data for a sprite or a loop).
|
|
4598
5443
|
* `outpaint` does not: there is no canvas-expansion endpoint in the API,
|
|
4599
5444
|
* matching docs/REVISIONS.md's note that no provider ships a tested
|
|
4600
5445
|
* outpaint path yet.
|
|
4601
5446
|
*/
|
|
4602
5447
|
supportsRevision(mode2) {
|
|
4603
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
5448
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "normal-map" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
4604
5449
|
}
|
|
4605
5450
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
4606
5451
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -4620,11 +5465,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4620
5465
|
* is a file read.
|
|
4621
5466
|
*/
|
|
4622
5467
|
static cacheDir(namespace = "pixflux") {
|
|
4623
|
-
const dir =
|
|
4624
|
-
(0,
|
|
5468
|
+
const dir = import_node_path3.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
|
|
5469
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
4625
5470
|
return dir;
|
|
4626
5471
|
}
|
|
4627
5472
|
estimate(spec) {
|
|
5473
|
+
if (spec.vfx?.description) return { unit: "generations", amount: vfxArtistCeiling(vfxArtistSpecQuote(spec)), candidates: 1 };
|
|
4628
5474
|
if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
|
|
4629
5475
|
if (spec.batch?.role === "member") {
|
|
4630
5476
|
return { unit: "generations", amount: 0, candidates: 1 };
|
|
@@ -4632,6 +5478,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4632
5478
|
if (spec.revision?.mode === "remove-background") {
|
|
4633
5479
|
return { unit: "generations", amount: 1, candidates: 1 };
|
|
4634
5480
|
}
|
|
5481
|
+
if (spec.revision?.mode === "normal-map") {
|
|
5482
|
+
return { unit: "generations", amount: NORMAL_MAP_COST_GENERATIONS, candidates: 1 };
|
|
5483
|
+
}
|
|
4635
5484
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
4636
5485
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
4637
5486
|
}
|
|
@@ -4686,12 +5535,40 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4686
5535
|
if (spec.generator === "vfx" && styleImages.length) {
|
|
4687
5536
|
throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
|
|
4688
5537
|
}
|
|
5538
|
+
const sprite = spec.vfx?.description ? spec.vfx.sprite : void 0;
|
|
5539
|
+
if (sprite && "reference" in sprite) {
|
|
5540
|
+
throw new Error("PixelLab's VFX artist takes a sprite's pixels, not a PixelLab reference; point vfx.sprite at a PNG file");
|
|
5541
|
+
}
|
|
5542
|
+
if (sprite?.width != null && sprite.height != null && Math.max(sprite.width, sprite.height) > VFX_ARTIST_MAX_SPRITE) {
|
|
5543
|
+
throw new Error(
|
|
5544
|
+
`vfx sprite ${sprite.file} is ${sprite.width}x${sprite.height}; PixelLab's VFX artist takes up to ${VFX_ARTIST_MAX_SPRITE}x${VFX_ARTIST_MAX_SPRITE} (crop or pad it; never rescale pixel art)`
|
|
5545
|
+
);
|
|
5546
|
+
}
|
|
4689
5547
|
const members = spec.revision?.sourceMembers;
|
|
4690
5548
|
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
4691
5549
|
throw new Error(
|
|
4692
|
-
`PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart,
|
|
5550
|
+
`PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart, edit-animation, or normal-map, which take the whole set`
|
|
4693
5551
|
);
|
|
4694
5552
|
}
|
|
5553
|
+
if (spec.revision?.mode === "normal-map") {
|
|
5554
|
+
const { sourceWidth: width, sourceHeight: height, sourceFormat, sourceAssetId } = spec.revision;
|
|
5555
|
+
if (members && !members.every((member) => member.role.startsWith("frame-"))) {
|
|
5556
|
+
throw new Error(
|
|
5557
|
+
`PixelLab normal-map reads one sprite or one animation loop; ${sourceAssetId} is a set of directions (${members.map((member) => member.role).join(", ")}). Make one normal-map revision per direction, each from an asset whose \`source\` is that direction's file`
|
|
5558
|
+
);
|
|
5559
|
+
}
|
|
5560
|
+
if (members && members.length > NORMAL_MAP_MAX_FRAMES) {
|
|
5561
|
+
throw new Error(
|
|
5562
|
+
`PixelLab normal-map takes at most ${NORMAL_MAP_MAX_FRAMES} frames in one strip; ${sourceAssetId} has ${members.length}`
|
|
5563
|
+
);
|
|
5564
|
+
}
|
|
5565
|
+
if (sourceFormat === "jpeg") {
|
|
5566
|
+
throw new Error(`PixelLab normal-map reads a PNG, whose alpha marks where the sprite is; ${sourceAssetId} is a JPEG`);
|
|
5567
|
+
}
|
|
5568
|
+
const frames = members?.length ?? 1;
|
|
5569
|
+
const problem = width != null && height != null ? normalMapSizeProblem(width * frames, height, frames) : null;
|
|
5570
|
+
if (problem) throw new Error(`PixelLab normal-map source ${sourceAssetId}: ${problem}`);
|
|
5571
|
+
}
|
|
4695
5572
|
if (spec.revision?.engine === "pro-flash") {
|
|
4696
5573
|
const { sourceWidth: width, sourceHeight: height, mode: mode2 } = spec.revision;
|
|
4697
5574
|
if (width != null && height != null && (width < 32 || height < 32 || width > 256 || height > 256 || width % 4 || height % 4)) {
|
|
@@ -4799,6 +5676,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4799
5676
|
`PixelLab imagePro is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling also depends on aspect ratio)`
|
|
4800
5677
|
);
|
|
4801
5678
|
}
|
|
5679
|
+
if (spec.generator === "imagePro" && !spec.revision && spec.palette.length && (spec.palette.length < 2 || spec.palette.length > 256)) {
|
|
5680
|
+
throw new Error(`PixelLab imagePro's target palette takes 2 to 256 colours; the style's palette has ${spec.palette.length}`);
|
|
5681
|
+
}
|
|
4802
5682
|
if (spec.generator === "uiAsset" && (spec.width < 192 || spec.height < 192 || spec.width > 688 || spec.height > 688)) {
|
|
4803
5683
|
throw new Error(
|
|
4804
5684
|
`PixelLab uiAsset is ${spec.width}x${spec.height}; the API takes 192 to 688 pixels per side (the exact ceiling also depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384, 9:16 at 384x688, 4:3 at 600x448, 3:4 at 448x600)`
|
|
@@ -4865,24 +5745,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4865
5745
|
if (spec.detail) {
|
|
4866
5746
|
requirePixelLabOption("terrain", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
|
|
4867
5747
|
}
|
|
4868
|
-
if (spec.
|
|
4869
|
-
|
|
4870
|
-
"PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainSpreadX/terrainSlopeSize/terrainRaggedness"
|
|
4871
|
-
);
|
|
4872
|
-
}
|
|
4873
|
-
if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
|
|
4874
|
-
throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
|
|
4875
|
-
}
|
|
4876
|
-
if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
|
|
4877
|
-
throw new Error(
|
|
4878
|
-
"PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
|
|
4879
|
-
);
|
|
4880
|
-
}
|
|
4881
|
-
if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
4882
|
-
throw new Error(
|
|
4883
|
-
"PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
|
|
4884
|
-
);
|
|
4885
|
-
}
|
|
5748
|
+
if (spec.terrainOrientation === "sidescroller") this.validateSidescrollerTerrain(spec);
|
|
5749
|
+
else this.validateTopDownTerrain(spec);
|
|
4886
5750
|
}
|
|
4887
5751
|
if (spec.generator === "isometricTile") {
|
|
4888
5752
|
if (spec.outline) {
|
|
@@ -4941,6 +5805,87 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4941
5805
|
);
|
|
4942
5806
|
}
|
|
4943
5807
|
}
|
|
5808
|
+
/** `/create-tileset`'s own rules, beyond the enums both terrain endpoints share. */
|
|
5809
|
+
validateTopDownTerrain(spec) {
|
|
5810
|
+
if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
|
|
5811
|
+
throw new Error(
|
|
5812
|
+
"PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainRaggedness/terrainRoundness/terrainBoundaryAmplitude/terrainStepSlope"
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
const pro = spec.terrainMode === "pro";
|
|
5816
|
+
if (spec.terrainRoundness != null && !pro && !spec.terrainShapeStyle) {
|
|
5817
|
+
throw new Error('PixelLab terrain: terrainRoundness needs terrainMode: "pro" or a terrainShapeStyle');
|
|
5818
|
+
}
|
|
5819
|
+
if (spec.terrainRoundness != null && spec.terrainRoundness > 1 && !pro) {
|
|
5820
|
+
throw new Error('PixelLab terrain: terrainRoundness above 1 (corners cut toward 45\xB0) needs terrainMode: "pro"');
|
|
5821
|
+
}
|
|
5822
|
+
if (spec.terrainBoundaryAmplitude != null && !pro && !spec.terrainShapeStyle) {
|
|
5823
|
+
throw new Error('PixelLab terrain: terrainBoundaryAmplitude needs terrainMode: "pro" or a terrainShapeStyle');
|
|
5824
|
+
}
|
|
5825
|
+
if (spec.terrainBoundarySeed != null && spec.terrainBoundaryAmplitude == null) {
|
|
5826
|
+
throw new Error("PixelLab terrain: terrainBoundarySeed shapes terrainBoundaryAmplitude's border; set both or neither");
|
|
5827
|
+
}
|
|
5828
|
+
if (spec.terrainStepSlope != null && !(pro && spec.terrainTransitionSize === 0.25)) {
|
|
5829
|
+
throw new Error('PixelLab terrain: terrainStepSlope needs terrainMode: "pro" with terrainTransitionSize 0.25');
|
|
5830
|
+
}
|
|
5831
|
+
if (spec.terrainConnectDiagonals != null && !pro) {
|
|
5832
|
+
throw new Error('PixelLab terrain: terrainConnectDiagonals needs terrainMode: "pro"');
|
|
5833
|
+
}
|
|
5834
|
+
if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
|
|
5835
|
+
throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
|
|
5836
|
+
}
|
|
5837
|
+
if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
|
|
5838
|
+
throw new Error(
|
|
5839
|
+
"PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
|
|
5840
|
+
);
|
|
5841
|
+
}
|
|
5842
|
+
if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
5843
|
+
throw new Error(
|
|
5844
|
+
"PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
|
|
5845
|
+
);
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
/**
|
|
5849
|
+
* `/create-tileset-sidescroller` takes a platform, its top layer, and the
|
|
5850
|
+
* layer's height, and nothing of the top-down endpoint's mode, camera, or
|
|
5851
|
+
* shape controls; sending one would be silently dropped, so it is refused.
|
|
5852
|
+
*/
|
|
5853
|
+
validateSidescrollerTerrain(spec) {
|
|
5854
|
+
const topDownOnly = {
|
|
5855
|
+
terrainMode: spec.terrainMode,
|
|
5856
|
+
terrainShapeStyle: spec.terrainShapeStyle,
|
|
5857
|
+
terrainView: spec.terrainView,
|
|
5858
|
+
terrainSpreadX: spec.terrainSpreadX,
|
|
5859
|
+
terrainSlopeSize: spec.terrainSlopeSize,
|
|
5860
|
+
terrainRaggedness: spec.terrainRaggedness,
|
|
5861
|
+
terrainRoundness: spec.terrainRoundness,
|
|
5862
|
+
terrainBoundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
5863
|
+
terrainBoundarySeed: spec.terrainBoundarySeed,
|
|
5864
|
+
terrainStepSlope: spec.terrainStepSlope,
|
|
5865
|
+
terrainConnectDiagonals: spec.terrainConnectDiagonals
|
|
5866
|
+
};
|
|
5867
|
+
const set = Object.entries(topDownOnly).filter(([, value]) => value != null).map(([field]) => field);
|
|
5868
|
+
if (set.length) {
|
|
5869
|
+
throw new Error(`PixelLab terrain: a sidescroller set takes no ${set.join(", ")}; those are top-down controls`);
|
|
5870
|
+
}
|
|
5871
|
+
if (spec.terrainTileSize !== 16 && spec.terrainTileSize !== 32) {
|
|
5872
|
+
throw new Error(`PixelLab terrain: a sidescroller set's tiles are 16 or 32px, got ${spec.terrainTileSize}`);
|
|
5873
|
+
}
|
|
5874
|
+
if (spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
5875
|
+
throw new Error("PixelLab terrain: a sidescroller set's terrainTransitionSize must be 0, 0.25, 0.5, or 1");
|
|
5876
|
+
}
|
|
5877
|
+
if (spec.terrainConnect?.upper) {
|
|
5878
|
+
throw new Error(
|
|
5879
|
+
"PixelLab terrain: a sidescroller set's upper terrain is empty air; only its lower (platform) terrain can connect"
|
|
5880
|
+
);
|
|
5881
|
+
}
|
|
5882
|
+
const link = spec.terrainConnect?.lower;
|
|
5883
|
+
if (link?.terrain === "upper") {
|
|
5884
|
+
throw new Error(
|
|
5885
|
+
`PixelLab terrain: ${link.assetId}'s upper terrain is empty air; connect to its lower (platform) terrain`
|
|
5886
|
+
);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
4944
5889
|
validateCharacter(spec, styleImages = []) {
|
|
4945
5890
|
const character = spec.character;
|
|
4946
5891
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
@@ -4970,7 +5915,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4970
5915
|
}
|
|
4971
5916
|
}
|
|
4972
5917
|
if (endFrame) {
|
|
4973
|
-
const start = startFrame ?? (character.parentFile && (0,
|
|
5918
|
+
const start = startFrame ?? (character.parentFile && (0, import_node_fs4.existsSync)(character.parentFile) ? imageMetadata((0, import_node_fs4.readFileSync)(character.parentFile)) : null);
|
|
4974
5919
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
4975
5920
|
throw new Error(
|
|
4976
5921
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -5128,7 +6073,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5128
6073
|
}
|
|
5129
6074
|
}
|
|
5130
6075
|
if (endFrame) {
|
|
5131
|
-
const start = startFrame ?? (object.parentFile && (0,
|
|
6076
|
+
const start = startFrame ?? (object.parentFile && (0, import_node_fs4.existsSync)(object.parentFile) ? imageMetadata((0, import_node_fs4.readFileSync)(object.parentFile)) : null);
|
|
5132
6077
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
5133
6078
|
throw new Error(
|
|
5134
6079
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -5183,13 +6128,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5183
6128
|
);
|
|
5184
6129
|
}
|
|
5185
6130
|
const frames = parentOutputs.slice(0, 16).map((output) => {
|
|
5186
|
-
const bytes = (0,
|
|
6131
|
+
const bytes = (0, import_node_fs4.readFileSync)(output.path);
|
|
5187
6132
|
const dims = imageMetadata(bytes);
|
|
5188
6133
|
if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
|
|
5189
6134
|
return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
|
|
5190
6135
|
});
|
|
5191
6136
|
const outfit = character.outfit;
|
|
5192
|
-
const referenceBytes = (0,
|
|
6137
|
+
const referenceBytes = (0, import_node_fs4.readFileSync)(outfit.reference.path);
|
|
5193
6138
|
if (sha256(referenceBytes) !== outfit.reference.sha256) {
|
|
5194
6139
|
throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
|
|
5195
6140
|
}
|
|
@@ -5232,7 +6177,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5232
6177
|
}
|
|
5233
6178
|
if (character.kind === "portrait") {
|
|
5234
6179
|
const parentFile = character.parentFile;
|
|
5235
|
-
const bytes = (0,
|
|
6180
|
+
const bytes = (0, import_node_fs4.readFileSync)(parentFile);
|
|
5236
6181
|
if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
|
|
5237
6182
|
throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
|
|
5238
6183
|
}
|
|
@@ -5438,8 +6383,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5438
6383
|
const sources = images.map((image, index) => {
|
|
5439
6384
|
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
5440
6385
|
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
5441
|
-
const file =
|
|
5442
|
-
(0,
|
|
6386
|
+
const file = import_node_path3.default.join(dir, `${jobId}-${index}.png`);
|
|
6387
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(image.base64, "base64"));
|
|
5443
6388
|
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
5444
6389
|
});
|
|
5445
6390
|
return {
|
|
@@ -5644,7 +6589,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5644
6589
|
seed: spec.seed
|
|
5645
6590
|
});
|
|
5646
6591
|
const jobId = (0, import_node_crypto2.randomUUID)();
|
|
5647
|
-
(0,
|
|
6592
|
+
(0, import_node_fs4.writeFileSync)(import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
5648
6593
|
return { jobId };
|
|
5649
6594
|
}
|
|
5650
6595
|
if (spec.generator === "tiles") {
|
|
@@ -5673,17 +6618,46 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5673
6618
|
});
|
|
5674
6619
|
return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
5675
6620
|
}
|
|
6621
|
+
if (spec.generator === "terrain" && spec.terrainOrientation === "sidescroller") {
|
|
6622
|
+
const baseTileIds = await this.terrainBaseTileIds(context?.terrainLinks);
|
|
6623
|
+
const res2 = await this.client.createTilesetSidescroller({
|
|
6624
|
+
lowerDescription: spec.terrainLowerDescription,
|
|
6625
|
+
transitionDescription: spec.terrainTransitionDescription,
|
|
6626
|
+
lowerBaseTileId: baseTileIds.lower,
|
|
6627
|
+
tileSize: spec.terrainTileSize,
|
|
6628
|
+
transitionSize: spec.terrainTransitionSize,
|
|
6629
|
+
outline: spec.outline,
|
|
6630
|
+
shading: spec.shading,
|
|
6631
|
+
detail: spec.detail,
|
|
6632
|
+
seed: spec.seed
|
|
6633
|
+
});
|
|
6634
|
+
return {
|
|
6635
|
+
jobId: res2.tileset_id,
|
|
6636
|
+
metadata: {
|
|
6637
|
+
backgroundJobId: res2.background_job_id,
|
|
6638
|
+
...Object.keys(baseTileIds).length ? { baseTileIds } : {}
|
|
6639
|
+
}
|
|
6640
|
+
};
|
|
6641
|
+
}
|
|
5676
6642
|
if (spec.generator === "terrain") {
|
|
6643
|
+
const baseTileIds = await this.terrainBaseTileIds(context?.terrainLinks);
|
|
5677
6644
|
const res2 = await this.client.createTileset({
|
|
5678
6645
|
lowerDescription: spec.terrainLowerDescription,
|
|
5679
6646
|
upperDescription: spec.terrainUpperDescription,
|
|
5680
6647
|
transitionDescription: spec.terrainTransitionDescription,
|
|
6648
|
+
lowerBaseTileId: baseTileIds.lower,
|
|
6649
|
+
upperBaseTileId: baseTileIds.upper,
|
|
5681
6650
|
tileSize: spec.terrainTileSize,
|
|
5682
6651
|
mode: spec.terrainMode,
|
|
5683
6652
|
shapeStyle: spec.terrainShapeStyle,
|
|
5684
6653
|
spreadX: spec.terrainSpreadX,
|
|
5685
6654
|
slopeSize: spec.terrainSlopeSize,
|
|
5686
6655
|
raggedness: spec.terrainRaggedness,
|
|
6656
|
+
roundness: spec.terrainRoundness,
|
|
6657
|
+
boundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
6658
|
+
boundarySeed: spec.terrainBoundarySeed,
|
|
6659
|
+
stepSlope: spec.terrainStepSlope,
|
|
6660
|
+
connectDiagonals: spec.terrainConnectDiagonals,
|
|
5687
6661
|
transitionSize: spec.terrainTransitionSize,
|
|
5688
6662
|
view: spec.terrainView,
|
|
5689
6663
|
outline: spec.outline,
|
|
@@ -5691,7 +6665,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5691
6665
|
detail: spec.detail,
|
|
5692
6666
|
seed: spec.seed
|
|
5693
6667
|
});
|
|
5694
|
-
return {
|
|
6668
|
+
return {
|
|
6669
|
+
jobId: res2.tileset_id,
|
|
6670
|
+
metadata: {
|
|
6671
|
+
backgroundJobId: res2.background_job_id,
|
|
6672
|
+
...Object.keys(baseTileIds).length ? { baseTileIds } : {}
|
|
6673
|
+
}
|
|
6674
|
+
};
|
|
5695
6675
|
}
|
|
5696
6676
|
if (spec.generator === "isometricTile") {
|
|
5697
6677
|
const res2 = await this.client.createIsometricTile({
|
|
@@ -5713,7 +6693,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5713
6693
|
width: spec.width,
|
|
5714
6694
|
height: spec.height,
|
|
5715
6695
|
noBackground: spec.noBackground,
|
|
5716
|
-
seed: spec.seed
|
|
6696
|
+
seed: spec.seed,
|
|
6697
|
+
targetPalette: { colors: spec.palette, name: spec.paletteName, maxColors: spec.maxColors }
|
|
5717
6698
|
});
|
|
5718
6699
|
return { jobId: res2.background_job_id };
|
|
5719
6700
|
}
|
|
@@ -5764,7 +6745,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5764
6745
|
}
|
|
5765
6746
|
const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
|
|
5766
6747
|
const images = sourceFiles.map((member) => {
|
|
5767
|
-
const bytes = (0,
|
|
6748
|
+
const bytes = (0, import_node_fs4.readFileSync)(member.file);
|
|
5768
6749
|
if (sha256(bytes) !== member.sha256) {
|
|
5769
6750
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
5770
6751
|
}
|
|
@@ -5779,7 +6760,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5779
6760
|
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
5780
6761
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
5781
6762
|
}
|
|
5782
|
-
const maskBytes = (0,
|
|
6763
|
+
const maskBytes = (0, import_node_fs4.readFileSync)(revision.maskFile);
|
|
5783
6764
|
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
5784
6765
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
5785
6766
|
}
|
|
@@ -5804,7 +6785,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5804
6785
|
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
5805
6786
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
5806
6787
|
}
|
|
5807
|
-
const maskBytes = (0,
|
|
6788
|
+
const maskBytes = (0, import_node_fs4.readFileSync)(revision.maskFile);
|
|
5808
6789
|
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
5809
6790
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
5810
6791
|
}
|
|
@@ -5825,7 +6806,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5825
6806
|
if (!revision.paletteImageSha256 || !revision.paletteImageFormat) {
|
|
5826
6807
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image is not ready`);
|
|
5827
6808
|
}
|
|
5828
|
-
const paletteBytes = (0,
|
|
6809
|
+
const paletteBytes = (0, import_node_fs4.readFileSync)(revision.paletteImageFile);
|
|
5829
6810
|
if (sha256(paletteBytes) !== revision.paletteImageSha256) {
|
|
5830
6811
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image changed after the manifest was resolved`);
|
|
5831
6812
|
}
|
|
@@ -5856,6 +6837,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5856
6837
|
});
|
|
5857
6838
|
return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
|
|
5858
6839
|
}
|
|
6840
|
+
if (revision.mode === "normal-map") {
|
|
6841
|
+
const strip = roles ? { base64: joinStripPng(images.map((member) => Buffer.from(member.base64, "base64"))).toString("base64"), format: "png" } : image;
|
|
6842
|
+
const res2 = await this.client.normalMap({ image: strip, frames: roles?.length ?? 1, convention: revision.convention });
|
|
6843
|
+
return {
|
|
6844
|
+
jobId: res2.background_job_id,
|
|
6845
|
+
metadata: roles ? { revisionMembers: roles, ...revision.sourceFps != null ? { revisionFps: revision.sourceFps } : {} } : {}
|
|
6846
|
+
};
|
|
6847
|
+
}
|
|
5859
6848
|
if (revision.mode === "interpolate") {
|
|
5860
6849
|
const lastFrame = this.readLastFrame(spec);
|
|
5861
6850
|
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
@@ -5916,7 +6905,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5916
6905
|
if (!revision.keypointsFile || !revision.keypointsSha256 || !revision.skeleton) {
|
|
5917
6906
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints are not ready`);
|
|
5918
6907
|
}
|
|
5919
|
-
const keypointsBytes = (0,
|
|
6908
|
+
const keypointsBytes = (0, import_node_fs4.readFileSync)(revision.keypointsFile);
|
|
5920
6909
|
if (sha256(keypointsBytes) !== revision.keypointsSha256) {
|
|
5921
6910
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints changed after the manifest was resolved`);
|
|
5922
6911
|
}
|
|
@@ -5958,7 +6947,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5958
6947
|
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
5959
6948
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
5960
6949
|
}
|
|
5961
|
-
const bytes = (0,
|
|
6950
|
+
const bytes = (0, import_node_fs4.readFileSync)(revision.lastFrameFile);
|
|
5962
6951
|
if (sha256(bytes) !== revision.lastFrameSha256) {
|
|
5963
6952
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
5964
6953
|
}
|
|
@@ -5973,11 +6962,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5973
6962
|
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
5974
6963
|
const jobId = (0, import_node_crypto2.randomUUID)();
|
|
5975
6964
|
if (!roles) {
|
|
5976
|
-
(0,
|
|
6965
|
+
(0, import_node_fs4.writeFileSync)(import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
5977
6966
|
return { jobId, metadata: { revisionUsage: usage } };
|
|
5978
6967
|
}
|
|
5979
6968
|
for (const [index, png] of pngs.entries()) {
|
|
5980
|
-
(0,
|
|
6969
|
+
(0, import_node_fs4.writeFileSync)(import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
5981
6970
|
}
|
|
5982
6971
|
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
5983
6972
|
}
|
|
@@ -6001,6 +6990,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6001
6990
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
|
|
6002
6991
|
return this.pollCachedRevision(jobId, context);
|
|
6003
6992
|
}
|
|
6993
|
+
if (revisionMode === "normal-map") return this.pollNormalMap(jobId, context);
|
|
6004
6994
|
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
6005
6995
|
return this.pollAnimateRevision(jobId, context);
|
|
6006
6996
|
}
|
|
@@ -6008,8 +6998,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6008
6998
|
if (generator === "vfx") return this.pollVfx(jobId, context);
|
|
6009
6999
|
if (generator === "imageProFlash") return this.pollRevision(jobId);
|
|
6010
7000
|
if (generator === "pixflux") {
|
|
6011
|
-
const file =
|
|
6012
|
-
if ((0,
|
|
7001
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
7002
|
+
if ((0, import_node_fs4.existsSync)(file)) {
|
|
6013
7003
|
const sourceUrl = `file://${file}`;
|
|
6014
7004
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
6015
7005
|
}
|
|
@@ -6065,14 +7055,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6065
7055
|
*/
|
|
6066
7056
|
async submitVfx(spec, context) {
|
|
6067
7057
|
const vfx = spec.vfx;
|
|
7058
|
+
if (vfx.description) return this.submitVfxArtist(spec, context);
|
|
6068
7059
|
if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
|
|
6069
7060
|
let sprite;
|
|
6070
7061
|
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
6071
7062
|
sprite = vfx.sprite.reference;
|
|
6072
7063
|
} else if (vfx.sprite) {
|
|
6073
7064
|
const { file } = vfx.sprite;
|
|
6074
|
-
if (!(0,
|
|
6075
|
-
const bytes = (0,
|
|
7065
|
+
if (!(0, import_node_fs4.existsSync)(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
7066
|
+
const bytes = (0, import_node_fs4.readFileSync)(file);
|
|
6076
7067
|
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
6077
7068
|
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
6078
7069
|
}
|
|
@@ -6119,50 +7110,137 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6119
7110
|
// The effect exists from here on; record it before anything else can fail.
|
|
6120
7111
|
onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
|
|
6121
7112
|
});
|
|
6122
|
-
await this.cacheVfxFrames(jobId, exported);
|
|
7113
|
+
await this.cacheVfxFrames(jobId, exported, workbenchVfxUrls(exported));
|
|
6123
7114
|
return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
|
|
6124
7115
|
}
|
|
7116
|
+
/**
|
|
7117
|
+
* Ask PixelLab's VFX artist (`POST /vfx`) for a `vfx` description: a
|
|
7118
|
+
* plain background job, polled by `pollVfxArtist`. Every submission is a
|
|
7119
|
+
* new effect. The manifest's description is the whole effect, not a change
|
|
7120
|
+
* to an earlier one, so it is never sent with `effect_id`; the effect this
|
|
7121
|
+
* asset made last time stays in My effects and its id is recorded here.
|
|
7122
|
+
*/
|
|
7123
|
+
async submitVfxArtist(spec, context) {
|
|
7124
|
+
const vfx = spec.vfx;
|
|
7125
|
+
let sprite;
|
|
7126
|
+
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
7127
|
+
throw new Error("PixelLab's VFX artist takes a sprite's pixels, not a PixelLab reference; point vfx.sprite at a PNG file");
|
|
7128
|
+
} else if (vfx.sprite) {
|
|
7129
|
+
const { file } = vfx.sprite;
|
|
7130
|
+
if (!(0, import_node_fs4.existsSync)(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
7131
|
+
const bytes = (0, import_node_fs4.readFileSync)(file);
|
|
7132
|
+
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
7133
|
+
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
7134
|
+
}
|
|
7135
|
+
sprite = vfxArtistSprite(bytes, `vfx sprite ${file}`).image;
|
|
7136
|
+
}
|
|
7137
|
+
const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
|
|
7138
|
+
const artist = vfx.artist ?? "lite";
|
|
7139
|
+
const submitted = await this.client.createVfx({ description: vfx.description, artist, sprite, name: vfx.name });
|
|
7140
|
+
return {
|
|
7141
|
+
jobId: submitted.background_job_id,
|
|
7142
|
+
metadata: {
|
|
7143
|
+
vfx: {
|
|
7144
|
+
artist,
|
|
7145
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
7146
|
+
...typeof previous?.effectId === "string" ? { previousEffect: previous.effectId } : {}
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
};
|
|
7150
|
+
}
|
|
6125
7151
|
/** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
|
|
6126
|
-
async cacheVfxFrames(jobId,
|
|
6127
|
-
const
|
|
6128
|
-
const
|
|
6129
|
-
|
|
6130
|
-
const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
|
|
6131
|
-
const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
|
|
6132
|
-
const frames = sliceVfxSheet(sheet, exported, atlas);
|
|
7152
|
+
async cacheVfxFrames(jobId, facts, urls, atlas) {
|
|
7153
|
+
const sheet = await this.client.download(urls.sheet);
|
|
7154
|
+
const frameAtlas = atlas ?? (urls.atlas ? JSON.parse((await this.client.download(urls.atlas)).toString("utf8")) : void 0);
|
|
7155
|
+
const frames = sliceVfxSheet(sheet, facts, frameAtlas);
|
|
6133
7156
|
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
6134
|
-
frames.forEach((frame, index) => (0,
|
|
6135
|
-
(0,
|
|
6136
|
-
|
|
6137
|
-
JSON.stringify({ effectId:
|
|
7157
|
+
frames.forEach((frame, index) => (0, import_node_fs4.writeFileSync)(import_node_path3.default.join(dir, `${jobId}-${index}.png`), frame));
|
|
7158
|
+
(0, import_node_fs4.writeFileSync)(
|
|
7159
|
+
import_node_path3.default.join(dir, `${jobId}.json`),
|
|
7160
|
+
JSON.stringify({ effectId: facts.effectId, count: frames.length, fps: facts.fps, loop: facts.loop })
|
|
6138
7161
|
);
|
|
6139
7162
|
}
|
|
6140
7163
|
async pollVfx(jobId, context) {
|
|
6141
7164
|
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
6142
|
-
const sidecar =
|
|
7165
|
+
const sidecar = import_node_path3.default.join(dir, `${jobId}.json`);
|
|
6143
7166
|
const cached = () => {
|
|
6144
|
-
if (!(0,
|
|
6145
|
-
const facts2 = JSON.parse((0,
|
|
6146
|
-
const files2 = Array.from({ length: facts2.count }, (_, index) =>
|
|
6147
|
-
return files2.every((file) => (0,
|
|
7167
|
+
if (!(0, import_node_fs4.existsSync)(sidecar)) return null;
|
|
7168
|
+
const facts2 = JSON.parse((0, import_node_fs4.readFileSync)(sidecar, "utf8"));
|
|
7169
|
+
const files2 = Array.from({ length: facts2.count }, (_, index) => import_node_path3.default.join(dir, `${jobId}-${index}.png`));
|
|
7170
|
+
return files2.every((file) => (0, import_node_fs4.existsSync)(file)) ? { facts: facts2, files: files2 } : null;
|
|
6148
7171
|
};
|
|
6149
7172
|
let found = cached();
|
|
6150
7173
|
const recorded = context?.metadata?.vfx;
|
|
6151
|
-
|
|
6152
|
-
|
|
7174
|
+
let artistDone;
|
|
7175
|
+
if (!found && recorded?.artist) {
|
|
7176
|
+
if (recorded.fileUrls?.sheet && recorded.effectId) {
|
|
7177
|
+
await this.cacheVfxFrames(jobId, recorded, {
|
|
7178
|
+
sheet: recorded.fileUrls.sheet,
|
|
7179
|
+
atlas: recorded.fileUrls.atlas
|
|
7180
|
+
});
|
|
7181
|
+
} else {
|
|
7182
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
7183
|
+
if (job.status === "failed") return { status: "failed", error: `vfx job failed upstream${vfxJobFailure(job.last_response)}` };
|
|
7184
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
7185
|
+
try {
|
|
7186
|
+
artistDone = await this.cacheVfxArtistResult(jobId, job.last_response ?? {}, recorded);
|
|
7187
|
+
} catch (error) {
|
|
7188
|
+
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
7189
|
+
}
|
|
7190
|
+
artistDone.billed = billedFromUsage(job.usage);
|
|
7191
|
+
}
|
|
7192
|
+
found = cached();
|
|
7193
|
+
} else if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
|
|
7194
|
+
const exported = recorded;
|
|
7195
|
+
await this.cacheVfxFrames(jobId, exported, workbenchVfxUrls(exported));
|
|
6153
7196
|
found = cached();
|
|
6154
7197
|
}
|
|
6155
7198
|
if (!found) {
|
|
6156
|
-
return {
|
|
7199
|
+
return {
|
|
7200
|
+
status: "failed",
|
|
7201
|
+
error: recorded?.artist ? "vfx frames are no longer cached locally and their download links were not recorded; re-run submit for this asset (paid)" : "vfx frames are no longer cached locally; re-run submit for this asset (free)"
|
|
7202
|
+
};
|
|
6157
7203
|
}
|
|
6158
7204
|
const { facts, files } = found;
|
|
6159
7205
|
const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
7206
|
+
const metadata = {
|
|
7207
|
+
...files.length > 1 ? { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } : {},
|
|
7208
|
+
...artistDone ? { vfx: artistDone.vfx } : {}
|
|
7209
|
+
};
|
|
6160
7210
|
return {
|
|
6161
7211
|
status: "ready",
|
|
6162
7212
|
objectId: facts.effectId,
|
|
6163
7213
|
sourceUrl: sources[0].url,
|
|
6164
7214
|
sources,
|
|
6165
|
-
...
|
|
7215
|
+
...Object.keys(metadata).length ? { metadata } : {},
|
|
7216
|
+
billed: artistDone?.billed ?? null
|
|
7217
|
+
};
|
|
7218
|
+
}
|
|
7219
|
+
/**
|
|
7220
|
+
* A delivered VFX artist job: cut its sheet into the cache and return the
|
|
7221
|
+
* effect's facts, merged over what submit recorded (poll replaces the
|
|
7222
|
+
* whole `vfx` key). Download links are kept only when they carry no
|
|
7223
|
+
* credential, so a committed lockfile never holds a signed URL.
|
|
7224
|
+
*/
|
|
7225
|
+
async cacheVfxArtistResult(jobId, done, recorded) {
|
|
7226
|
+
const result = parseVfxArtistResult(done, jobId);
|
|
7227
|
+
const atlas = result.atlas ? JSON.parse((await this.client.download(result.atlas.url)).toString("utf8")) : void 0;
|
|
7228
|
+
const size = vfxArtistFrameFacts(result, atlas);
|
|
7229
|
+
const facts = { effectId: result.effectId, ...size, fps: result.fps ?? 12, loop: result.loop ?? false };
|
|
7230
|
+
await this.cacheVfxFrames(jobId, facts, { sheet: result.sheet.url }, atlas);
|
|
7231
|
+
const urls = [result.sheet.url, ...result.atlas ? [result.atlas.url] : []];
|
|
7232
|
+
return {
|
|
7233
|
+
vfx: {
|
|
7234
|
+
...recorded,
|
|
7235
|
+
...facts,
|
|
7236
|
+
editUrl: result.editUrl,
|
|
7237
|
+
name: result.name,
|
|
7238
|
+
message: result.message,
|
|
7239
|
+
anchor: result.anchor,
|
|
7240
|
+
parts: result.parts,
|
|
7241
|
+
files: result.files.map((file) => file.name),
|
|
7242
|
+
...urls.every(shouldPersistSourceUrl) ? { fileUrls: { sheet: result.sheet.url, ...result.atlas ? { atlas: result.atlas.url } : {} } } : {}
|
|
7243
|
+
},
|
|
6166
7244
|
billed: null
|
|
6167
7245
|
};
|
|
6168
7246
|
}
|
|
@@ -6175,10 +7253,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6175
7253
|
};
|
|
6176
7254
|
if (Array.isArray(roles) && roles.length) {
|
|
6177
7255
|
const sources = roles.map((role, index) => ({
|
|
6178
|
-
url: `file://${
|
|
7256
|
+
url: `file://${import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
6179
7257
|
role: String(role)
|
|
6180
7258
|
}));
|
|
6181
|
-
if (sources.some((source) => !(0,
|
|
7259
|
+
if (sources.some((source) => !(0, import_node_fs4.existsSync)(source.url.slice("file://".length)))) return gone;
|
|
6182
7260
|
const fps = context?.metadata?.revisionFps;
|
|
6183
7261
|
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
6184
7262
|
return {
|
|
@@ -6190,11 +7268,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6190
7268
|
billed: billedFromUsage(usage)
|
|
6191
7269
|
};
|
|
6192
7270
|
}
|
|
6193
|
-
const file =
|
|
6194
|
-
if (!(0,
|
|
7271
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
7272
|
+
if (!(0, import_node_fs4.existsSync)(file)) return gone;
|
|
6195
7273
|
const sourceUrl = `file://${file}`;
|
|
6196
7274
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
6197
7275
|
}
|
|
7276
|
+
/**
|
|
7277
|
+
* A `normal-map` revision: a background job whose map is
|
|
7278
|
+
* `last_response.image`. A loop's map comes back as the strip it was sent
|
|
7279
|
+
* as, and is cut into one file per frame under the parent's roles. Nothing
|
|
7280
|
+
* downstream palette-snaps it: the manifest refuses a normal map in a
|
|
7281
|
+
* style with `enforcePalette` or `quality`.
|
|
7282
|
+
*/
|
|
7283
|
+
async pollNormalMap(jobId, context) {
|
|
7284
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
7285
|
+
if (job.status === "failed") return { status: "failed", error: "normal-map job failed upstream" };
|
|
7286
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
7287
|
+
const billed = billedFromUsage(job.usage);
|
|
7288
|
+
const png = normalMapResultPng(job.last_response);
|
|
7289
|
+
if (!png) {
|
|
7290
|
+
return {
|
|
7291
|
+
status: "failed",
|
|
7292
|
+
error: `Invalid PixelLab response for normal-map job ${jobId}: completed with no image at last_response.image (got: ${Object.keys(job.last_response ?? {}).join(", ") || "no keys"})`
|
|
7293
|
+
};
|
|
7294
|
+
}
|
|
7295
|
+
const roles = context?.metadata?.revisionMembers;
|
|
7296
|
+
if (Array.isArray(roles) && roles.length) {
|
|
7297
|
+
let frames;
|
|
7298
|
+
try {
|
|
7299
|
+
frames = splitStripPng(png, roles.length);
|
|
7300
|
+
} catch (error) {
|
|
7301
|
+
return { status: "failed", error: `normal-map job ${jobId}: ${error.message}` };
|
|
7302
|
+
}
|
|
7303
|
+
const sources = frames.map((frame, index) => {
|
|
7304
|
+
const file2 = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7305
|
+
(0, import_node_fs4.writeFileSync)(file2, frame);
|
|
7306
|
+
return { url: `file://${file2}`, role: String(roles[index]) };
|
|
7307
|
+
});
|
|
7308
|
+
const fps = context?.metadata?.revisionFps;
|
|
7309
|
+
return {
|
|
7310
|
+
status: "ready",
|
|
7311
|
+
objectId: jobId,
|
|
7312
|
+
sourceUrl: sources[0].url,
|
|
7313
|
+
sources,
|
|
7314
|
+
...typeof fps === "number" ? { metadata: { frameSet: { fps, count: sources.length } } } : {},
|
|
7315
|
+
billed
|
|
7316
|
+
};
|
|
7317
|
+
}
|
|
7318
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
7319
|
+
(0, import_node_fs4.writeFileSync)(file, png);
|
|
7320
|
+
const sourceUrl = `file://${file}`;
|
|
7321
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
7322
|
+
}
|
|
6198
7323
|
/**
|
|
6199
7324
|
* `animate`/`animate-pixminimax` revisions: a plain background job, like
|
|
6200
7325
|
* `pollRevision` above, but completing with an ORDERED FRAME LIST instead
|
|
@@ -6247,8 +7372,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6247
7372
|
}
|
|
6248
7373
|
const base64 = extractBase64(item);
|
|
6249
7374
|
if (base64) {
|
|
6250
|
-
const file =
|
|
6251
|
-
(0,
|
|
7375
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7376
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
6252
7377
|
urls.push(`file://${file}`);
|
|
6253
7378
|
continue;
|
|
6254
7379
|
}
|
|
@@ -6298,8 +7423,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6298
7423
|
const candidate = done[key];
|
|
6299
7424
|
const base64 = extractBase64(candidate);
|
|
6300
7425
|
if (base64) {
|
|
6301
|
-
const file =
|
|
6302
|
-
(0,
|
|
7426
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
7427
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
6303
7428
|
const sourceUrl = `file://${file}`;
|
|
6304
7429
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6305
7430
|
}
|
|
@@ -6307,8 +7432,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6307
7432
|
if (Array.isArray(done.images) && done.images.length) {
|
|
6308
7433
|
const base64 = extractBase64(done.images[0]);
|
|
6309
7434
|
if (base64) {
|
|
6310
|
-
const file =
|
|
6311
|
-
(0,
|
|
7435
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
7436
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
6312
7437
|
const sourceUrl = `file://${file}`;
|
|
6313
7438
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6314
7439
|
}
|
|
@@ -6341,8 +7466,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6341
7466
|
for (const [index, candidate] of done.images.entries()) {
|
|
6342
7467
|
const base64 = extractBase64(candidate);
|
|
6343
7468
|
if (!base64) continue;
|
|
6344
|
-
const file =
|
|
6345
|
-
(0,
|
|
7469
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7470
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
6346
7471
|
urls.push(`file://${file}`);
|
|
6347
7472
|
}
|
|
6348
7473
|
if (urls.length) return { status: "review", candidateUrls: urls, billed };
|
|
@@ -6371,8 +7496,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6371
7496
|
for (const [index, candidate] of images.entries()) {
|
|
6372
7497
|
const base64 = extractBase64(candidate);
|
|
6373
7498
|
if (!base64) continue;
|
|
6374
|
-
const file =
|
|
6375
|
-
(0,
|
|
7499
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7500
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
6376
7501
|
urls.push(`file://${file}`);
|
|
6377
7502
|
}
|
|
6378
7503
|
if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
|
|
@@ -6450,6 +7575,26 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6450
7575
|
throw err;
|
|
6451
7576
|
}
|
|
6452
7577
|
}
|
|
7578
|
+
/**
|
|
7579
|
+
* The base tile id of each terrain a connected set shares: the other
|
|
7580
|
+
* set's recorded `terrainIds`, or, for a set downloaded before pixelkiln
|
|
7581
|
+
* recorded them, read from PixelLab's own record of it (a free GET).
|
|
7582
|
+
*/
|
|
7583
|
+
async terrainBaseTileIds(links) {
|
|
7584
|
+
const ids = {};
|
|
7585
|
+
for (const side of ["lower", "upper"]) {
|
|
7586
|
+
const link = links?.[side];
|
|
7587
|
+
if (!link) continue;
|
|
7588
|
+
const id = link.terrainId ?? (await this.client.getTileset(link.objectId)).metadata.terrainIds[link.terrain];
|
|
7589
|
+
if (!id) {
|
|
7590
|
+
throw new Error(
|
|
7591
|
+
`PixelLab terrain: tileset ${link.objectId} has no ${link.terrain} terrain id to connect this set's ${side} terrain to`
|
|
7592
|
+
);
|
|
7593
|
+
}
|
|
7594
|
+
ids[side] = id;
|
|
7595
|
+
}
|
|
7596
|
+
return ids;
|
|
7597
|
+
}
|
|
6453
7598
|
/**
|
|
6454
7599
|
* `/create-tileset` reports progress the same way `tiles` does (423 while
|
|
6455
7600
|
* drawing, 200 once finished), but each tile's image comes back embedded
|
|
@@ -6461,7 +7606,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6461
7606
|
*/
|
|
6462
7607
|
async pollTerrain(tilesetId, context) {
|
|
6463
7608
|
try {
|
|
6464
|
-
const { tileset, usage } = await this.client.getTileset(tilesetId);
|
|
7609
|
+
const { tileset, usage, metadata } = await this.client.getTileset(tilesetId);
|
|
7610
|
+
const sidescroller = metadata.view === "side" || context?.spec?.terrainOrientation === "sidescroller";
|
|
6465
7611
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6466
7612
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
6467
7613
|
const sources = [];
|
|
@@ -6469,8 +7615,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6469
7615
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
6470
7616
|
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
6471
7617
|
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
6472
|
-
const file =
|
|
6473
|
-
(0,
|
|
7618
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
7619
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(tile.image.base64, "base64"));
|
|
6474
7620
|
sources.push({ url: `file://${file}`, role });
|
|
6475
7621
|
terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
|
|
6476
7622
|
}
|
|
@@ -6479,7 +7625,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6479
7625
|
objectId: tilesetId,
|
|
6480
7626
|
sourceUrl: sources[0]?.url ?? null,
|
|
6481
7627
|
sources,
|
|
6482
|
-
metadata: {
|
|
7628
|
+
metadata: {
|
|
7629
|
+
terrainTypes: sidescroller ? ["lower", "upper"] : tileset.terrain_types,
|
|
7630
|
+
...sidescroller ? { providerTerrainTypes: tileset.terrain_types, view: "side" } : {},
|
|
7631
|
+
terrainTiles,
|
|
7632
|
+
// Each terrain's base tile id, which a connected set sends.
|
|
7633
|
+
...Object.keys(metadata.terrainIds).length ? { terrainIds: metadata.terrainIds } : {}
|
|
7634
|
+
},
|
|
6483
7635
|
billed
|
|
6484
7636
|
};
|
|
6485
7637
|
} catch (err) {
|
|
@@ -6499,8 +7651,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6499
7651
|
const { image, usage } = await this.client.getIsometricTile(tileId);
|
|
6500
7652
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6501
7653
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
6502
|
-
const file =
|
|
6503
|
-
(0,
|
|
7654
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
|
|
7655
|
+
(0, import_node_fs4.writeFileSync)(file, Buffer.from(image.base64, "base64"));
|
|
6504
7656
|
return {
|
|
6505
7657
|
status: "ready",
|
|
6506
7658
|
objectId: tileId,
|
|
@@ -6543,8 +7695,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6543
7695
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
6544
7696
|
}
|
|
6545
7697
|
if (generator === "imagePro" || generator === "uiElement") {
|
|
6546
|
-
const file =
|
|
6547
|
-
if (!(0,
|
|
7698
|
+
const file = import_node_path3.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7699
|
+
if (!(0, import_node_fs4.existsSync)(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
6548
7700
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
6549
7701
|
}
|
|
6550
7702
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -6556,7 +7708,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6556
7708
|
return { objectId, sourceUrl: firstUrl(obj?.rotation_urls) ?? obj?.preview_url ?? null };
|
|
6557
7709
|
}
|
|
6558
7710
|
async download(url) {
|
|
6559
|
-
if (url.startsWith("file://")) return (0,
|
|
7711
|
+
if (url.startsWith("file://")) return (0, import_node_fs4.readFileSync)(url.slice("file://".length));
|
|
6560
7712
|
return this.client.download(url);
|
|
6561
7713
|
}
|
|
6562
7714
|
async balance() {
|
|
@@ -6678,7 +7830,7 @@ function extractBase64(value) {
|
|
|
6678
7830
|
function readReference(spec, reference) {
|
|
6679
7831
|
const images = {};
|
|
6680
7832
|
for (const [direction, image] of Object.entries(reference)) {
|
|
6681
|
-
const bytes = (0,
|
|
7833
|
+
const bytes = (0, import_node_fs4.readFileSync)(image.path);
|
|
6682
7834
|
if (sha256(bytes) !== image.sha256) {
|
|
6683
7835
|
throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
|
|
6684
7836
|
}
|
|
@@ -6695,7 +7847,7 @@ function requireStyleObjectId(spec, context) {
|
|
|
6695
7847
|
return context.styleObjectId;
|
|
6696
7848
|
}
|
|
6697
7849
|
function readPose(spec, what, image) {
|
|
6698
|
-
const bytes = (0,
|
|
7850
|
+
const bytes = (0, import_node_fs4.readFileSync)(image.path);
|
|
6699
7851
|
if (sha256(bytes) !== image.sha256) {
|
|
6700
7852
|
throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
|
|
6701
7853
|
}
|
|
@@ -6760,7 +7912,7 @@ function findObjectAnimation(object, name, direction, animationId) {
|
|
|
6760
7912
|
function mapScenePayload(spec) {
|
|
6761
7913
|
const scene = spec.scene;
|
|
6762
7914
|
const read = (file, hash, label) => {
|
|
6763
|
-
const bytes = (0,
|
|
7915
|
+
const bytes = (0, import_node_fs4.readFileSync)(file);
|
|
6764
7916
|
if (!hash || sha256(bytes) !== hash) {
|
|
6765
7917
|
throw new Error(`${spec.styleId}/${spec.assetId}: ${label} changed after the manifest was resolved`);
|
|
6766
7918
|
}
|
|
@@ -7133,9 +8285,9 @@ function retroError(value) {
|
|
|
7133
8285
|
|
|
7134
8286
|
// src/providers/scenario.ts
|
|
7135
8287
|
var import_node_crypto3 = require("crypto");
|
|
7136
|
-
var
|
|
8288
|
+
var import_node_fs5 = require("fs");
|
|
7137
8289
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
7138
|
-
var
|
|
8290
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
7139
8291
|
var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
|
|
7140
8292
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
7141
8293
|
var SUBMIT_RETRIES = 8;
|
|
@@ -7518,7 +8670,7 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
7518
8670
|
}
|
|
7519
8671
|
};
|
|
7520
8672
|
function uploadCachePath() {
|
|
7521
|
-
return process.env.SCENARIO_UPLOAD_CACHE ??
|
|
8673
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path4.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
7522
8674
|
}
|
|
7523
8675
|
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
7524
8676
|
return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
|
|
@@ -7527,7 +8679,7 @@ ${digest2}`).digest("hex");
|
|
|
7527
8679
|
}
|
|
7528
8680
|
function readUploadCache() {
|
|
7529
8681
|
try {
|
|
7530
|
-
const value = JSON.parse((0,
|
|
8682
|
+
const value = JSON.parse((0, import_node_fs5.readFileSync)(uploadCachePath(), "utf8"));
|
|
7531
8683
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7532
8684
|
} catch {
|
|
7533
8685
|
return {};
|
|
@@ -7536,10 +8688,10 @@ function readUploadCache() {
|
|
|
7536
8688
|
function writeUploadCache(key, assetId) {
|
|
7537
8689
|
try {
|
|
7538
8690
|
const file = uploadCachePath();
|
|
7539
|
-
(0,
|
|
8691
|
+
(0, import_node_fs5.mkdirSync)(import_node_path4.default.dirname(file), { recursive: true });
|
|
7540
8692
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7541
|
-
(0,
|
|
7542
|
-
(0,
|
|
8693
|
+
(0, import_node_fs5.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
8694
|
+
(0, import_node_fs5.renameSync)(tmp, file);
|
|
7543
8695
|
} catch {
|
|
7544
8696
|
}
|
|
7545
8697
|
}
|
|
@@ -7566,7 +8718,7 @@ function readRevisionSource(spec) {
|
|
|
7566
8718
|
if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
|
|
7567
8719
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
7568
8720
|
}
|
|
7569
|
-
const bytes = (0,
|
|
8721
|
+
const bytes = (0, import_node_fs5.readFileSync)(revision.sourceFile);
|
|
7570
8722
|
if (sha256(bytes) !== revision.sourceSha256) {
|
|
7571
8723
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
7572
8724
|
}
|
|
@@ -7736,8 +8888,8 @@ function redactScenarioError(value, secrets) {
|
|
|
7736
8888
|
|
|
7737
8889
|
// src/providers/comfyui.ts
|
|
7738
8890
|
var import_node_crypto4 = require("crypto");
|
|
7739
|
-
var
|
|
7740
|
-
var
|
|
8891
|
+
var import_promises3 = require("fs/promises");
|
|
8892
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
7741
8893
|
var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
|
|
7742
8894
|
var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
|
|
7743
8895
|
"prompt",
|
|
@@ -7874,10 +9026,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7874
9026
|
}
|
|
7875
9027
|
async resolveOptions(value, context) {
|
|
7876
9028
|
const options = parseOptions(value);
|
|
7877
|
-
const workflowPath =
|
|
9029
|
+
const workflowPath = import_node_path5.default.resolve(context.root, options.workflowFile);
|
|
7878
9030
|
let raw;
|
|
7879
9031
|
try {
|
|
7880
|
-
raw = await (0,
|
|
9032
|
+
raw = await (0, import_promises3.readFile)(workflowPath, "utf8");
|
|
7881
9033
|
} catch (err) {
|
|
7882
9034
|
throw new Error(
|
|
7883
9035
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7929,10 +9081,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7929
9081
|
if (typeof item !== "string") {
|
|
7930
9082
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
7931
9083
|
}
|
|
7932
|
-
const file =
|
|
9084
|
+
const file = import_node_path5.default.resolve(context.root, item);
|
|
7933
9085
|
let bytes;
|
|
7934
9086
|
try {
|
|
7935
|
-
bytes = await (0,
|
|
9087
|
+
bytes = await (0, import_promises3.readFile)(file);
|
|
7936
9088
|
} catch (error) {
|
|
7937
9089
|
throw new Error(
|
|
7938
9090
|
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -8067,6 +9219,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
8067
9219
|
"ComfyUI has no binding for numColors/paletteImage/dithering \u2014 those are PixelLab's /reduce-colors parameters. A reduce-colors revision on ComfyUI is whatever your own workflow does with bindings.sourceImage; write that behavior into the graph directly."
|
|
8068
9220
|
);
|
|
8069
9221
|
}
|
|
9222
|
+
if (spec.revision.convention) {
|
|
9223
|
+
throw new Error(
|
|
9224
|
+
"ComfyUI has no binding for convention \u2014 that is PixelLab's /normal-map parameter. A normal-map revision on ComfyUI is whatever your own workflow does; build the green-channel convention into the graph."
|
|
9225
|
+
);
|
|
9226
|
+
}
|
|
8070
9227
|
if ((!options.bindings.width || !options.bindings.height) && spec.revision.sourceWidth != null && spec.revision.sourceHeight != null && (spec.width !== spec.revision.sourceWidth || spec.height !== spec.revision.sourceHeight)) {
|
|
8071
9228
|
throw new Error(
|
|
8072
9229
|
"ComfyUI revisions without width/height bindings must keep the source dimensions"
|
|
@@ -8089,7 +9246,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
8089
9246
|
setBinding(workflow2, binding, input);
|
|
8090
9247
|
return;
|
|
8091
9248
|
}
|
|
8092
|
-
const bytes = await (0,
|
|
9249
|
+
const bytes = await (0, import_promises3.readFile)(input.path);
|
|
8093
9250
|
if (sha256(bytes) !== input.sha256) {
|
|
8094
9251
|
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
8095
9252
|
}
|
|
@@ -8156,7 +9313,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
8156
9313
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
8157
9314
|
throw new Error("ComfyUI revision source is not ready");
|
|
8158
9315
|
}
|
|
8159
|
-
const sourceBytes = await (0,
|
|
9316
|
+
const sourceBytes = await (0, import_promises3.readFile)(spec.revision.sourceFile);
|
|
8160
9317
|
if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
|
|
8161
9318
|
throw new Error("ComfyUI revision source changed before upload");
|
|
8162
9319
|
}
|
|
@@ -8170,7 +9327,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
8170
9327
|
if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
|
|
8171
9328
|
throw new Error("ComfyUI revision mask is not ready");
|
|
8172
9329
|
}
|
|
8173
|
-
const maskBytes = await (0,
|
|
9330
|
+
const maskBytes = await (0, import_promises3.readFile)(spec.revision.maskFile);
|
|
8174
9331
|
if (sha256(maskBytes) !== spec.revision.maskSha256) {
|
|
8175
9332
|
throw new Error("ComfyUI revision mask changed before upload");
|
|
8176
9333
|
}
|
|
@@ -8890,9 +10047,9 @@ registerProvider({
|
|
|
8890
10047
|
});
|
|
8891
10048
|
|
|
8892
10049
|
// src/manifest.ts
|
|
8893
|
-
var
|
|
8894
|
-
var
|
|
8895
|
-
var
|
|
10050
|
+
var import_promises5 = require("fs/promises");
|
|
10051
|
+
var import_node_fs8 = require("fs");
|
|
10052
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
8896
10053
|
|
|
8897
10054
|
// src/loop-directions.ts
|
|
8898
10055
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -8980,17 +10137,17 @@ function isObject2(value) {
|
|
|
8980
10137
|
}
|
|
8981
10138
|
|
|
8982
10139
|
// src/outputs.ts
|
|
8983
|
-
var
|
|
8984
|
-
var
|
|
10140
|
+
var import_node_fs7 = require("fs");
|
|
10141
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
8985
10142
|
|
|
8986
10143
|
// src/lock.ts
|
|
8987
|
-
var
|
|
8988
|
-
var
|
|
8989
|
-
var
|
|
10144
|
+
var import_promises4 = require("fs/promises");
|
|
10145
|
+
var import_node_fs6 = require("fs");
|
|
10146
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
8990
10147
|
async function loadLock(lockPath) {
|
|
8991
|
-
if (!(0,
|
|
10148
|
+
if (!(0, import_node_fs6.existsSync)(lockPath)) return { version: 2, entries: {} };
|
|
8992
10149
|
try {
|
|
8993
|
-
return parseLock(JSON.parse(await (0,
|
|
10150
|
+
return parseLock(JSON.parse(await (0, import_promises4.readFile)(lockPath, "utf8")));
|
|
8994
10151
|
} catch (err) {
|
|
8995
10152
|
throw new ProjectError(
|
|
8996
10153
|
`Lockfile at ${lockPath} is malformed:
|
|
@@ -9003,7 +10160,7 @@ var saveQueues = /* @__PURE__ */ new Map();
|
|
|
9003
10160
|
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
9004
10161
|
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
9005
10162
|
function saveLock(lockPath, lock) {
|
|
9006
|
-
const queueKey =
|
|
10163
|
+
const queueKey = import_node_path6.default.resolve(lockPath);
|
|
9007
10164
|
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
9008
10165
|
const next = previous.catch(() => {
|
|
9009
10166
|
}).then(() => writeLockNow(lockPath, lock));
|
|
@@ -9015,7 +10172,7 @@ function saveLock(lockPath, lock) {
|
|
|
9015
10172
|
return next;
|
|
9016
10173
|
}
|
|
9017
10174
|
async function writeLockNow(lockPath, lock) {
|
|
9018
|
-
await (0,
|
|
10175
|
+
await (0, import_promises4.mkdir)(import_node_path6.default.dirname(import_node_path6.default.resolve(lockPath)), { recursive: true });
|
|
9019
10176
|
const release = await acquireFileLock(lockPath);
|
|
9020
10177
|
try {
|
|
9021
10178
|
await writeLockWhileHeld(lockPath, lock);
|
|
@@ -9025,9 +10182,9 @@ async function writeLockNow(lockPath, lock) {
|
|
|
9025
10182
|
}
|
|
9026
10183
|
async function writeLockWhileHeld(file, lock) {
|
|
9027
10184
|
let disk = { version: 2, entries: {} };
|
|
9028
|
-
if ((0,
|
|
10185
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
9029
10186
|
try {
|
|
9030
|
-
disk = parseLock(JSON.parse(await (0,
|
|
10187
|
+
disk = parseLock(JSON.parse(await (0, import_promises4.readFile)(file, "utf8")));
|
|
9031
10188
|
} catch (err) {
|
|
9032
10189
|
throw new ProjectError(
|
|
9033
10190
|
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -9056,10 +10213,10 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
9056
10213
|
}
|
|
9057
10214
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9058
10215
|
try {
|
|
9059
|
-
await (0,
|
|
9060
|
-
await (0,
|
|
10216
|
+
await (0, import_promises4.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
10217
|
+
await (0, import_promises4.rename)(tmp, file);
|
|
9061
10218
|
} finally {
|
|
9062
|
-
await (0,
|
|
10219
|
+
await (0, import_promises4.rm)(tmp, { force: true });
|
|
9063
10220
|
}
|
|
9064
10221
|
const currentDirty = dirtyPatches.get(lock);
|
|
9065
10222
|
if (currentDirty) {
|
|
@@ -9122,18 +10279,18 @@ async function acquireFileLock(file) {
|
|
|
9122
10279
|
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
9123
10280
|
for (; ; ) {
|
|
9124
10281
|
try {
|
|
9125
|
-
await (0,
|
|
10282
|
+
await (0, import_promises4.mkdir)(lockDir);
|
|
9126
10283
|
return async () => {
|
|
9127
|
-
await (0,
|
|
10284
|
+
await (0, import_promises4.rmdir)(lockDir).catch(() => {
|
|
9128
10285
|
});
|
|
9129
10286
|
};
|
|
9130
10287
|
} catch (err) {
|
|
9131
10288
|
const code = err.code;
|
|
9132
10289
|
if (code !== "EEXIST") throw err;
|
|
9133
10290
|
try {
|
|
9134
|
-
const age = Date.now() - (await (0,
|
|
10291
|
+
const age = Date.now() - (await (0, import_promises4.stat)(lockDir)).mtimeMs;
|
|
9135
10292
|
if (age > STALE_FILE_LOCK_MS) {
|
|
9136
|
-
await (0,
|
|
10293
|
+
await (0, import_promises4.rmdir)(lockDir);
|
|
9137
10294
|
continue;
|
|
9138
10295
|
}
|
|
9139
10296
|
} catch (lockErr) {
|
|
@@ -9160,22 +10317,22 @@ function totalSpend(lock, unit = "generations") {
|
|
|
9160
10317
|
|
|
9161
10318
|
// src/outputs.ts
|
|
9162
10319
|
function portableOutputPath(file, manifestDir) {
|
|
9163
|
-
const root =
|
|
9164
|
-
const absolute =
|
|
9165
|
-
const relative =
|
|
9166
|
-
const value =
|
|
9167
|
-
return value.split(
|
|
10320
|
+
const root = import_node_path7.default.resolve(manifestDir);
|
|
10321
|
+
const absolute = import_node_path7.default.isAbsolute(file) ? import_node_path7.default.normalize(file) : import_node_path7.default.resolve(root, file);
|
|
10322
|
+
const relative = import_node_path7.default.relative(root, absolute);
|
|
10323
|
+
const value = import_node_path7.default.isAbsolute(relative) ? absolute : relative;
|
|
10324
|
+
return value.split(import_node_path7.default.sep).join("/");
|
|
9168
10325
|
}
|
|
9169
10326
|
function resolveOutputPath(recordedPath, manifestDir) {
|
|
9170
|
-
if (
|
|
9171
|
-
if (
|
|
9172
|
-
return
|
|
10327
|
+
if (import_node_path7.default.isAbsolute(recordedPath)) return import_node_path7.default.normalize(recordedPath);
|
|
10328
|
+
if (import_node_path7.default.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
10329
|
+
return import_node_path7.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path7.default.sep));
|
|
9173
10330
|
}
|
|
9174
10331
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
9175
10332
|
return memberPath(spec.outFile, role, index, total, mediaType);
|
|
9176
10333
|
}
|
|
9177
10334
|
function memberPath(file, role, index, total, mediaType) {
|
|
9178
|
-
const originalExt =
|
|
10335
|
+
const originalExt = import_node_path7.default.extname(file);
|
|
9179
10336
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
9180
10337
|
const stem = originalExt ? file.slice(0, -originalExt.length) : file;
|
|
9181
10338
|
if (total === 1) return `${stem}${ext}`;
|
|
@@ -9189,10 +10346,10 @@ function isFrameSetEntry(entry) {
|
|
|
9189
10346
|
return entry.generator === "frames" && isMemberSetEntry(entry);
|
|
9190
10347
|
}
|
|
9191
10348
|
function sourceIsStem(source, entry, manifestDir) {
|
|
9192
|
-
return isMemberSetEntry(entry) && !(0,
|
|
10349
|
+
return isMemberSetEntry(entry) && !(0, import_node_fs7.existsSync)(import_node_path7.default.resolve(manifestDir, source));
|
|
9193
10350
|
}
|
|
9194
10351
|
function sourceOutputPath(source, entry, index, manifestDir) {
|
|
9195
|
-
const absolute =
|
|
10352
|
+
const absolute = import_node_path7.default.resolve(manifestDir, source);
|
|
9196
10353
|
if (!sourceIsStem(source, entry, manifestDir)) return absolute;
|
|
9197
10354
|
const output = entry.outputs[index];
|
|
9198
10355
|
return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
|
|
@@ -9279,7 +10436,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
9279
10436
|
id: spec.assetId,
|
|
9280
10437
|
index: 0,
|
|
9281
10438
|
path: spec.outFile,
|
|
9282
|
-
absolutePath:
|
|
10439
|
+
absolutePath: import_node_path7.default.resolve(spec.outFile),
|
|
9283
10440
|
sha256: ""
|
|
9284
10441
|
}];
|
|
9285
10442
|
}
|
|
@@ -9300,9 +10457,9 @@ function frameSetLoop(entry) {
|
|
|
9300
10457
|
|
|
9301
10458
|
// src/manifest.ts
|
|
9302
10459
|
async function loadManifest(manifestPath) {
|
|
9303
|
-
const abs =
|
|
9304
|
-
if (!(0,
|
|
9305
|
-
const expansion = expandLoopDirections(JSON.parse(await (0,
|
|
10460
|
+
const abs = import_node_path8.default.resolve(manifestPath);
|
|
10461
|
+
if (!(0, import_node_fs8.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
10462
|
+
const expansion = expandLoopDirections(JSON.parse(await (0, import_promises5.readFile)(abs, "utf8")));
|
|
9306
10463
|
if (expansion.issues.length) {
|
|
9307
10464
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
9308
10465
|
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
@@ -9399,12 +10556,38 @@ ${issues}`);
|
|
|
9399
10556
|
visited.add(assetId);
|
|
9400
10557
|
};
|
|
9401
10558
|
for (const assetId of Object.keys(parsed.data.assets)) visitParents(assetId, []);
|
|
10559
|
+
const connectVisiting = [];
|
|
10560
|
+
const connectVisited = /* @__PURE__ */ new Set();
|
|
10561
|
+
const visitConnections = (assetId) => {
|
|
10562
|
+
if (connectVisited.has(assetId)) return;
|
|
10563
|
+
const cycleStart = connectVisiting.indexOf(assetId);
|
|
10564
|
+
if (cycleStart >= 0) {
|
|
10565
|
+
unknownReferences.push(
|
|
10566
|
+
`assets.${assetId}.connect: connection cycle ${[...connectVisiting.slice(cycleStart), assetId].join(" -> ")}`
|
|
10567
|
+
);
|
|
10568
|
+
return;
|
|
10569
|
+
}
|
|
10570
|
+
connectVisiting.push(assetId);
|
|
10571
|
+
for (const [side, link] of Object.entries(parsed.data.assets[assetId]?.connect ?? {})) {
|
|
10572
|
+
if (!link) continue;
|
|
10573
|
+
if (!parsed.data.assets[link.of]) {
|
|
10574
|
+
unknownReferences.push(`assets.${assetId}.connect.${side}.of: unknown asset "${link.of}"`);
|
|
10575
|
+
} else if (link.of === assetId) {
|
|
10576
|
+
unknownReferences.push(`assets.${assetId}.connect.${side}.of: a set cannot connect to itself`);
|
|
10577
|
+
} else {
|
|
10578
|
+
visitConnections(link.of);
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
connectVisiting.pop();
|
|
10582
|
+
connectVisited.add(assetId);
|
|
10583
|
+
};
|
|
10584
|
+
for (const assetId of Object.keys(parsed.data.assets)) visitConnections(assetId);
|
|
9402
10585
|
if (unknownReferences.length) {
|
|
9403
10586
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
9404
10587
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
9405
10588
|
}
|
|
9406
10589
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
9407
|
-
return { manifest: parsed.data, root:
|
|
10590
|
+
return { manifest: parsed.data, root: import_node_path8.default.dirname(abs), path: abs, ...loopFamilies };
|
|
9408
10591
|
}
|
|
9409
10592
|
function assetInStyle(asset, styleId, style) {
|
|
9410
10593
|
if (asset.styles.length) return asset.styles.includes(styleId);
|
|
@@ -9413,6 +10596,13 @@ function assetInStyle(asset, styleId, style) {
|
|
|
9413
10596
|
function parentAssetId(asset) {
|
|
9414
10597
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
9415
10598
|
}
|
|
10599
|
+
function dependencyAssetIds(asset) {
|
|
10600
|
+
const parent = parentAssetId(asset);
|
|
10601
|
+
return [
|
|
10602
|
+
...parent ? [parent] : [],
|
|
10603
|
+
...[asset?.connect?.lower?.of, asset?.connect?.upper?.of].filter((id) => Boolean(id))
|
|
10604
|
+
];
|
|
10605
|
+
}
|
|
9416
10606
|
function formatManifestIssues(issues) {
|
|
9417
10607
|
return issues.flatMap((issue) => {
|
|
9418
10608
|
if (issue.code === "invalid_union") {
|
|
@@ -9500,11 +10690,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9500
10690
|
}
|
|
9501
10691
|
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
9502
10692
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
resolutionAssetIds.
|
|
9507
|
-
|
|
10693
|
+
const pending = [...requestedAssetIds];
|
|
10694
|
+
while (pending.length) {
|
|
10695
|
+
for (const dependency of dependencyAssetIds(manifest.assets[pending.pop()])) {
|
|
10696
|
+
if (resolutionAssetIds.has(dependency) || !manifest.assets[dependency]) continue;
|
|
10697
|
+
resolutionAssetIds.add(dependency);
|
|
10698
|
+
pending.push(dependency);
|
|
9508
10699
|
}
|
|
9509
10700
|
}
|
|
9510
10701
|
const styleDependencies = /* @__PURE__ */ new Map();
|
|
@@ -9542,11 +10733,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9542
10733
|
const styleFinalizers = /* @__PURE__ */ new Map();
|
|
9543
10734
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
9544
10735
|
async function loadStyleImage(rel, what = "Style image") {
|
|
9545
|
-
const abs =
|
|
10736
|
+
const abs = import_node_path8.default.resolve(root, rel);
|
|
9546
10737
|
let hit = styleImageCache.get(abs);
|
|
9547
10738
|
if (!hit) {
|
|
9548
|
-
if (!(0,
|
|
9549
|
-
const buf = await (0,
|
|
10739
|
+
if (!(0, import_node_fs8.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
10740
|
+
const buf = await (0, import_promises5.readFile)(abs);
|
|
9550
10741
|
const metadata = imageMetadata(buf);
|
|
9551
10742
|
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
9552
10743
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -9572,7 +10763,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9572
10763
|
styleImageDimensions.push(loadedImage);
|
|
9573
10764
|
}
|
|
9574
10765
|
const resolvedImages = style.styleImages.map((image) => {
|
|
9575
|
-
const hit = styleImageCache.get(
|
|
10766
|
+
const hit = styleImageCache.get(import_node_path8.default.resolve(root, image.path));
|
|
9576
10767
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
9577
10768
|
});
|
|
9578
10769
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -9659,6 +10850,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9659
10850
|
`assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
|
|
9660
10851
|
);
|
|
9661
10852
|
}
|
|
10853
|
+
if (asset.connect && generator !== "terrain") {
|
|
10854
|
+
throw new Error(`assets.${assetId}: connect needs a terrain style; "${styleId}" generates ${generator}`);
|
|
10855
|
+
}
|
|
9662
10856
|
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
9663
10857
|
throw new Error(
|
|
9664
10858
|
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
@@ -9670,7 +10864,18 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9670
10864
|
let terrainUpperDescription;
|
|
9671
10865
|
let terrainTransitionDescription;
|
|
9672
10866
|
let prompt;
|
|
9673
|
-
if (generator === "terrain") {
|
|
10867
|
+
if (generator === "terrain" && style.terrainOrientation === "sidescroller") {
|
|
10868
|
+
const parsed = parseTerrainDescriptions(subject);
|
|
10869
|
+
if (!parsed || parsed.transition) {
|
|
10870
|
+
throw new Error(
|
|
10871
|
+
`assets.${assetId}: a sidescroller terrain needs exactly "1). <platform body> 2). <top layer>" in its prompt (e.g. 1). stone brick texture 2). green grass texture)`
|
|
10872
|
+
);
|
|
10873
|
+
}
|
|
10874
|
+
const wrap = (text) => [style.promptPrefix, text, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
10875
|
+
terrainLowerDescription = wrap(parsed.lower);
|
|
10876
|
+
terrainTransitionDescription = wrap(parsed.upper);
|
|
10877
|
+
prompt = subject.trim();
|
|
10878
|
+
} else if (generator === "terrain") {
|
|
9674
10879
|
const parsed = parseTerrainDescriptions(subject);
|
|
9675
10880
|
if (!parsed) {
|
|
9676
10881
|
throw new Error(
|
|
@@ -9687,12 +10892,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9687
10892
|
} else {
|
|
9688
10893
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
9689
10894
|
}
|
|
9690
|
-
const relFile = asset.file ??
|
|
9691
|
-
const outFile =
|
|
9692
|
-
const qualityOutFile = style.quality ?
|
|
10895
|
+
const relFile = asset.file ?? import_node_path8.default.join(asset.category ?? "", `${assetId}.png`);
|
|
10896
|
+
const outFile = import_node_path8.default.resolve(root, style.outDir, relFile);
|
|
10897
|
+
const qualityOutFile = style.quality ? import_node_path8.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
9693
10898
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
9694
10899
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
9695
|
-
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
10900
|
+
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize, style.terrainMode, style.terrainOrientation) : 0;
|
|
9696
10901
|
const base = {
|
|
9697
10902
|
styleId,
|
|
9698
10903
|
assetId,
|
|
@@ -9715,6 +10920,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9715
10920
|
uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
|
|
9716
10921
|
...generator === "imageProFlash" && style.styleTraits ? { styleTraits: style.styleTraits } : {},
|
|
9717
10922
|
palette: style.palette,
|
|
10923
|
+
...generator === "imagePro" && style.paletteName ? { paletteName: style.paletteName } : {},
|
|
10924
|
+
...generator === "imagePro" && style.maxColors != null ? { maxColors: style.maxColors } : {},
|
|
9718
10925
|
enforcePalette: style.enforcePalette,
|
|
9719
10926
|
noBackground: style.noBackground,
|
|
9720
10927
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
@@ -9749,8 +10956,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9749
10956
|
terrainSpreadX: generator === "terrain" ? style.terrainSpreadX : void 0,
|
|
9750
10957
|
terrainSlopeSize: generator === "terrain" ? style.terrainSlopeSize : void 0,
|
|
9751
10958
|
terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
|
|
10959
|
+
terrainRoundness: generator === "terrain" ? style.terrainRoundness : void 0,
|
|
10960
|
+
terrainBoundaryAmplitude: generator === "terrain" ? style.terrainBoundaryAmplitude : void 0,
|
|
10961
|
+
terrainBoundarySeed: generator === "terrain" ? style.terrainBoundarySeed : void 0,
|
|
10962
|
+
terrainStepSlope: generator === "terrain" ? style.terrainStepSlope : void 0,
|
|
10963
|
+
terrainConnectDiagonals: generator === "terrain" ? style.terrainConnectDiagonals : void 0,
|
|
9752
10964
|
terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
|
|
9753
10965
|
terrainView: generator === "terrain" ? style.terrainView : void 0,
|
|
10966
|
+
terrainOrientation: generator === "terrain" ? style.terrainOrientation : void 0,
|
|
9754
10967
|
isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
|
|
9755
10968
|
isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
|
|
9756
10969
|
// A revision in a character or objectPro style is a provider edit of
|
|
@@ -9761,7 +10974,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9761
10974
|
// objectPro asset is only ever a base, state, or animation.
|
|
9762
10975
|
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
9763
10976
|
...vfx ? { vfx } : {},
|
|
9764
|
-
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ?
|
|
10977
|
+
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? terrainCost(style.terrainMode, size, style.terrainOrientation) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
9765
10978
|
costUnit: "generations",
|
|
9766
10979
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
9767
10980
|
};
|
|
@@ -9785,7 +10998,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9785
10998
|
minGridConfidence: style.quality.minGridConfidence,
|
|
9786
10999
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
9787
11000
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
9788
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
11001
|
+
...style.quality.fixerPython ? { fixerPython: import_node_path8.default.resolve(root, style.quality.fixerPython) } : {},
|
|
9789
11002
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
9790
11003
|
}
|
|
9791
11004
|
} : {},
|
|
@@ -9822,6 +11035,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9822
11035
|
if (sourceSpec.generator === "tiles" || sourceSpec.generator === "terrain") {
|
|
9823
11036
|
throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
|
|
9824
11037
|
}
|
|
11038
|
+
if (sourceSpec.revision?.mode === "normal-map") {
|
|
11039
|
+
throw new Error(
|
|
11040
|
+
`assets.${assetId}: a normal map cannot be mirrored by flipping its pixels; every red channel would still point the old way. Make a normal-map revision of the mirrored art instead`
|
|
11041
|
+
);
|
|
11042
|
+
}
|
|
9825
11043
|
if (sourceSpec.character?.kind === "animation") {
|
|
9826
11044
|
const facing = sourceSpec.character.animation.direction;
|
|
9827
11045
|
const mirrored = MIRRORED_DIRECTION[facing];
|
|
@@ -9868,7 +11086,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9868
11086
|
const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
|
|
9869
11087
|
resolved.character = {
|
|
9870
11088
|
...resolved.character,
|
|
9871
|
-
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0,
|
|
11089
|
+
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs8.existsSync)(file) ? await sha256File(file) : null }
|
|
9872
11090
|
};
|
|
9873
11091
|
}
|
|
9874
11092
|
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
@@ -9881,7 +11099,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9881
11099
|
parentAssetId: characterParent,
|
|
9882
11100
|
parentSpec,
|
|
9883
11101
|
parentFile,
|
|
9884
|
-
parentSha256: (0,
|
|
11102
|
+
parentSha256: (0, import_node_fs8.existsSync)(parentFile) ? await sha256File(parentFile) : null,
|
|
9885
11103
|
// A state and its animations keep the engine and rotations of the base.
|
|
9886
11104
|
mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
|
|
9887
11105
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
@@ -9899,7 +11117,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9899
11117
|
parentAssetId: characterParent,
|
|
9900
11118
|
parentSpec,
|
|
9901
11119
|
parentFile,
|
|
9902
|
-
parentSha256: (0,
|
|
11120
|
+
parentSha256: (0, import_node_fs8.existsSync)(parentFile) ? await sha256File(parentFile) : null,
|
|
9903
11121
|
// A state and its animations keep the base's own rotation count.
|
|
9904
11122
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
9905
11123
|
};
|
|
@@ -9921,7 +11139,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9921
11139
|
animation: sourceSpec.character.animation,
|
|
9922
11140
|
outfit: {
|
|
9923
11141
|
reference: {
|
|
9924
|
-
path:
|
|
11142
|
+
path: import_node_path8.default.resolve(root, asset.outfit.reference),
|
|
9925
11143
|
sha256: referenceImage.hash,
|
|
9926
11144
|
width: referenceImage.width,
|
|
9927
11145
|
height: referenceImage.height,
|
|
@@ -9942,25 +11160,30 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9942
11160
|
`Provider "${activeProvider.id}" does not support ${asset.revision.mode} revisions`
|
|
9943
11161
|
);
|
|
9944
11162
|
}
|
|
11163
|
+
if (asset.revision.mode === "normal-map" && (style.enforcePalette || style.quality)) {
|
|
11164
|
+
throw new Error(
|
|
11165
|
+
`assets.${assetId}: a normal-map revision is lighting data, not art, and style "${styleId}" sets ${style.enforcePalette ? "enforcePalette" : "quality"}, which would recolour it. Put it in a style without enforcePalette or quality, and name the parent's style with revision.fromStyle`
|
|
11166
|
+
);
|
|
11167
|
+
}
|
|
9945
11168
|
const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
|
|
9946
11169
|
const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
|
|
9947
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
11170
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path8.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
9948
11171
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
9949
11172
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
9950
11173
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
9951
11174
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
9952
|
-
const maskFile = asset.revision.mask ?
|
|
11175
|
+
const maskFile = asset.revision.mask ? import_node_path8.default.resolve(root, asset.revision.mask) : void 0;
|
|
9953
11176
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
9954
11177
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
9955
11178
|
throw new Error(
|
|
9956
11179
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
9957
11180
|
);
|
|
9958
11181
|
}
|
|
9959
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
11182
|
+
const paletteImageFile = asset.revision.paletteImage ? import_node_path8.default.resolve(root, asset.revision.paletteImage) : void 0;
|
|
9960
11183
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
9961
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
11184
|
+
const lastFrameFile = asset.revision.lastFrame ? import_node_path8.default.resolve(root, asset.revision.lastFrame) : void 0;
|
|
9962
11185
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
9963
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
11186
|
+
const keypointsFile = asset.revision.keypointsFile ? import_node_path8.default.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
9964
11187
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
9965
11188
|
resolved.revision = {
|
|
9966
11189
|
mode: asset.revision.mode,
|
|
@@ -10016,6 +11239,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10016
11239
|
...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
|
|
10017
11240
|
...asset.revision.description ? { description: asset.revision.description } : {},
|
|
10018
11241
|
...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
|
|
11242
|
+
...asset.revision.convention ? { convention: asset.revision.convention } : {},
|
|
10019
11243
|
...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
|
|
10020
11244
|
...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
|
|
10021
11245
|
...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
|
|
@@ -10072,6 +11296,24 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10072
11296
|
};
|
|
10073
11297
|
}
|
|
10074
11298
|
}
|
|
11299
|
+
if (asset.connect) {
|
|
11300
|
+
const links = {};
|
|
11301
|
+
for (const side of ["lower", "upper"]) {
|
|
11302
|
+
const link = asset.connect[side];
|
|
11303
|
+
if (!link) continue;
|
|
11304
|
+
if (!styleSpecs.has(link.of)) {
|
|
11305
|
+
throw new Error(
|
|
11306
|
+
`assets.${assetId}.connect.${side}.of: "${link.of}" is not in style "${styleId}"; connected terrain sets share one style`
|
|
11307
|
+
);
|
|
11308
|
+
}
|
|
11309
|
+
const linkedSpec = await finalize(link.of);
|
|
11310
|
+
if (linkedSpec.generator !== "terrain") {
|
|
11311
|
+
throw new Error(`assets.${assetId}.connect.${side}.of: "${link.of}" is not a terrain set`);
|
|
11312
|
+
}
|
|
11313
|
+
links[side] = { assetId: link.of, terrain: link.terrain, spec: linkedSpec };
|
|
11314
|
+
}
|
|
11315
|
+
resolved.terrainConnect = links;
|
|
11316
|
+
}
|
|
10075
11317
|
if (asset.scene) resolved.scene = await resolveMapScene(root, styleId, assetId, resolved.generator, asset.scene, activeProvider);
|
|
10076
11318
|
resolved.specHash = specHash(
|
|
10077
11319
|
resolved,
|
|
@@ -10130,7 +11372,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10130
11372
|
return specs;
|
|
10131
11373
|
}
|
|
10132
11374
|
function pngPath(file) {
|
|
10133
|
-
const extension =
|
|
11375
|
+
const extension = import_node_path8.default.extname(file);
|
|
10134
11376
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
10135
11377
|
}
|
|
10136
11378
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -10140,12 +11382,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
10140
11382
|
if (!provider.supportsMapScene) {
|
|
10141
11383
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
10142
11384
|
}
|
|
10143
|
-
const file =
|
|
11385
|
+
const file = import_node_path8.default.resolve(root, scene.image);
|
|
10144
11386
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
10145
11387
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
10146
11388
|
const placement = scene.placement;
|
|
10147
11389
|
if ("mask" in placement) {
|
|
10148
|
-
const maskFile =
|
|
11390
|
+
const maskFile = import_node_path8.default.resolve(root, placement.mask);
|
|
10149
11391
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
10150
11392
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
10151
11393
|
throw new Error(
|
|
@@ -10163,20 +11405,20 @@ async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
|
10163
11405
|
let recipe;
|
|
10164
11406
|
let recordedSprite;
|
|
10165
11407
|
if (vfx.recipe) {
|
|
10166
|
-
const file =
|
|
10167
|
-
if (!(0,
|
|
11408
|
+
const file = import_node_path8.default.resolve(root, vfx.recipe);
|
|
11409
|
+
if (!(0, import_node_fs8.existsSync)(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
|
|
10168
11410
|
const read = await readVfxRecipe(file).catch((error) => {
|
|
10169
11411
|
throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
|
|
10170
11412
|
});
|
|
10171
11413
|
recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
|
|
10172
11414
|
recordedSprite = read.sprite;
|
|
10173
11415
|
}
|
|
10174
|
-
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite :
|
|
11416
|
+
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : import_node_path8.default.resolve(root, vfx.sprite) : recordedSprite;
|
|
10175
11417
|
let sprite;
|
|
10176
11418
|
if (spriteRef && isVfxReference(spriteRef)) {
|
|
10177
11419
|
sprite = { reference: spriteRef };
|
|
10178
11420
|
} else if (spriteRef) {
|
|
10179
|
-
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite
|
|
11421
|
+
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`, vfx.description ? "png" : void 0);
|
|
10180
11422
|
sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
|
|
10181
11423
|
}
|
|
10182
11424
|
return {
|
|
@@ -10185,12 +11427,14 @@ async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
|
10185
11427
|
...vfx.parts?.length ? { parts: vfx.parts } : {},
|
|
10186
11428
|
...sprite ? { sprite } : {},
|
|
10187
11429
|
...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
|
|
10188
|
-
|
|
11430
|
+
// The artist caps a name at 80 characters; a long default is cut, not refused.
|
|
11431
|
+
name: vfx.name ?? (vfx.description ? defaultName.slice(0, 80) : defaultName),
|
|
11432
|
+
...vfx.description ? { description: vfx.description, artist: vfx.artist ?? "lite" } : {}
|
|
10189
11433
|
};
|
|
10190
11434
|
}
|
|
10191
11435
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
10192
|
-
if (!(0,
|
|
10193
|
-
const bytes = await (0,
|
|
11436
|
+
if (!(0, import_node_fs8.existsSync)(file)) return null;
|
|
11437
|
+
const bytes = await (0, import_promises5.readFile)(file);
|
|
10194
11438
|
const metadata = imageMetadata(bytes);
|
|
10195
11439
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
10196
11440
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -10199,8 +11443,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
10199
11443
|
return { hash: sha256(bytes), ...metadata };
|
|
10200
11444
|
}
|
|
10201
11445
|
async function optionalSkeletonSet(file) {
|
|
10202
|
-
if (!(0,
|
|
10203
|
-
const bytes = await (0,
|
|
11446
|
+
if (!(0, import_node_fs8.existsSync)(file)) return null;
|
|
11447
|
+
const bytes = await (0, import_promises5.readFile)(file);
|
|
10204
11448
|
let json;
|
|
10205
11449
|
try {
|
|
10206
11450
|
json = JSON.parse(bytes.toString("utf8"));
|
|
@@ -10210,17 +11454,17 @@ async function optionalSkeletonSet(file) {
|
|
|
10210
11454
|
return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
|
|
10211
11455
|
}
|
|
10212
11456
|
async function revisionSourceMembers(stem, label) {
|
|
10213
|
-
if ((0,
|
|
11457
|
+
if ((0, import_node_fs8.existsSync)(stem)) return null;
|
|
10214
11458
|
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
10215
11459
|
let roles = [];
|
|
10216
11460
|
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
10217
|
-
if (set.every((direction) => (0,
|
|
11461
|
+
if (set.every((direction) => (0, import_node_fs8.existsSync)(at(direction)))) {
|
|
10218
11462
|
roles = [...set];
|
|
10219
11463
|
break;
|
|
10220
11464
|
}
|
|
10221
11465
|
}
|
|
10222
11466
|
if (!roles.length) {
|
|
10223
|
-
for (let index = 0; (0,
|
|
11467
|
+
for (let index = 0; (0, import_node_fs8.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
10224
11468
|
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
10225
11469
|
}
|
|
10226
11470
|
}
|
|
@@ -10246,8 +11490,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
10246
11490
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
10247
11491
|
const out = [];
|
|
10248
11492
|
for (const img of style.styleImages) {
|
|
10249
|
-
const file =
|
|
10250
|
-
const buf = await (0,
|
|
11493
|
+
const file = import_node_path8.default.resolve(loaded.root, img.path);
|
|
11494
|
+
const buf = await (0, import_promises5.readFile)(file);
|
|
10251
11495
|
const metadata = imageMetadata(buf);
|
|
10252
11496
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
10253
11497
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -10277,7 +11521,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
10277
11521
|
};
|
|
10278
11522
|
if (asset.concept) {
|
|
10279
11523
|
const image = await files.load(asset.concept, "Concept image");
|
|
10280
|
-
shape.concept = { path:
|
|
11524
|
+
shape.concept = { path: import_node_path8.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
10281
11525
|
}
|
|
10282
11526
|
if (asset.reference) {
|
|
10283
11527
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -10285,7 +11529,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
10285
11529
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
10286
11530
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
10287
11531
|
shape.reference[direction] = {
|
|
10288
|
-
path:
|
|
11532
|
+
path: import_node_path8.default.resolve(files.root, rel),
|
|
10289
11533
|
sha256: image.hash,
|
|
10290
11534
|
width: image.width,
|
|
10291
11535
|
height: image.height,
|
|
@@ -10306,7 +11550,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
10306
11550
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
10307
11551
|
const pose = async (rel, what) => {
|
|
10308
11552
|
const image = await files.load(rel, what);
|
|
10309
|
-
return { path:
|
|
11553
|
+
return { path: import_node_path8.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
10310
11554
|
};
|
|
10311
11555
|
shape.animation = {
|
|
10312
11556
|
mode: animationMode,
|
|
@@ -10337,7 +11581,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
10337
11581
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
10338
11582
|
if (south) {
|
|
10339
11583
|
const image = await files.load(south, "Reference sprite");
|
|
10340
|
-
shape.reference = { path:
|
|
11584
|
+
shape.reference = { path: import_node_path8.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
10341
11585
|
}
|
|
10342
11586
|
}
|
|
10343
11587
|
if (asset.state) {
|
|
@@ -10365,7 +11609,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
10365
11609
|
}
|
|
10366
11610
|
const pose = async (rel, what) => {
|
|
10367
11611
|
const image = await files.load(rel, what);
|
|
10368
|
-
return { path:
|
|
11612
|
+
return { path: import_node_path8.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
10369
11613
|
};
|
|
10370
11614
|
shape.animation = {
|
|
10371
11615
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -10385,11 +11629,11 @@ function characterDirections(directions) {
|
|
|
10385
11629
|
}
|
|
10386
11630
|
|
|
10387
11631
|
// src/project.ts
|
|
10388
|
-
var
|
|
11632
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
10389
11633
|
|
|
10390
11634
|
// src/env.ts
|
|
10391
|
-
var
|
|
10392
|
-
var
|
|
11635
|
+
var import_node_fs9 = require("fs");
|
|
11636
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
10393
11637
|
function loadEnvFiles(dir) {
|
|
10394
11638
|
const loaded = [];
|
|
10395
11639
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -10403,10 +11647,10 @@ function loadEnvFiles(dir) {
|
|
|
10403
11647
|
function envFiles(dir) {
|
|
10404
11648
|
const out = [];
|
|
10405
11649
|
for (const name of [".env.local", ".env"]) {
|
|
10406
|
-
const file =
|
|
10407
|
-
if (!(0,
|
|
11650
|
+
const file = import_node_path9.default.join(dir, name);
|
|
11651
|
+
if (!(0, import_node_fs9.existsSync)(file)) continue;
|
|
10408
11652
|
try {
|
|
10409
|
-
out.push([file, parseEnv((0,
|
|
11653
|
+
out.push([file, parseEnv((0, import_node_fs9.readFileSync)(file, "utf8"))]);
|
|
10410
11654
|
} catch {
|
|
10411
11655
|
}
|
|
10412
11656
|
}
|
|
@@ -10432,12 +11676,12 @@ function parseEnv(contents) {
|
|
|
10432
11676
|
}
|
|
10433
11677
|
|
|
10434
11678
|
// src/pipeline/plan.ts
|
|
10435
|
-
var
|
|
10436
|
-
var
|
|
11679
|
+
var import_node_fs12 = require("fs");
|
|
11680
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
10437
11681
|
|
|
10438
11682
|
// src/pipeline/mirror.ts
|
|
10439
|
-
var
|
|
10440
|
-
var
|
|
11683
|
+
var import_promises6 = require("fs/promises");
|
|
11684
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
10441
11685
|
|
|
10442
11686
|
// src/palette.ts
|
|
10443
11687
|
function normalizePalette(values) {
|
|
@@ -10574,12 +11818,12 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
10574
11818
|
const outputs = [];
|
|
10575
11819
|
const written = [];
|
|
10576
11820
|
for (const [index, member] of members.entries()) {
|
|
10577
|
-
const bytes = flipHorizontal(await (0,
|
|
11821
|
+
const bytes = flipHorizontal(await (0, import_promises6.readFile)(member.file));
|
|
10578
11822
|
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
10579
|
-
await (0,
|
|
11823
|
+
await (0, import_promises6.mkdir)(import_node_path10.default.dirname(target), { recursive: true });
|
|
10580
11824
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
10581
|
-
await (0,
|
|
10582
|
-
await (0,
|
|
11825
|
+
await (0, import_promises6.writeFile)(tmp, bytes);
|
|
11826
|
+
await (0, import_promises6.rename)(tmp, target);
|
|
10583
11827
|
written.push(target);
|
|
10584
11828
|
outputs.push({
|
|
10585
11829
|
path: portableOutputPath(target, spec.root),
|
|
@@ -10638,20 +11882,20 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
10638
11882
|
}
|
|
10639
11883
|
|
|
10640
11884
|
// src/pipeline/quality-profile.ts
|
|
10641
|
-
var
|
|
10642
|
-
var
|
|
11885
|
+
var import_node_fs10 = require("fs");
|
|
11886
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
10643
11887
|
|
|
10644
11888
|
// src/pipeline/refine.ts
|
|
10645
|
-
var
|
|
11889
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
10646
11890
|
var import_node_child_process2 = require("child_process");
|
|
10647
|
-
var
|
|
11891
|
+
var import_promises8 = require("fs/promises");
|
|
10648
11892
|
var import_node_os3 = require("os");
|
|
10649
11893
|
var import_node_util = require("util");
|
|
10650
11894
|
|
|
10651
11895
|
// src/artifacts.ts
|
|
10652
|
-
var
|
|
11896
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
10653
11897
|
var import_node_crypto6 = require("crypto");
|
|
10654
|
-
var
|
|
11898
|
+
var import_promises7 = require("fs/promises");
|
|
10655
11899
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
10656
11900
|
function message(error) {
|
|
10657
11901
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -10660,7 +11904,7 @@ function digest(data) {
|
|
|
10660
11904
|
return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
|
|
10661
11905
|
}
|
|
10662
11906
|
function portableRelative(from, to) {
|
|
10663
|
-
return
|
|
11907
|
+
return import_node_path11.default.relative(from, import_node_path11.default.resolve(to)).split(import_node_path11.default.sep).join("/") || ".";
|
|
10664
11908
|
}
|
|
10665
11909
|
function canonical2(value) {
|
|
10666
11910
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -10710,12 +11954,12 @@ function parseArtifactManifest(absolute, data) {
|
|
|
10710
11954
|
return raw;
|
|
10711
11955
|
}
|
|
10712
11956
|
async function readArtifactBundleManifest(manifestPath) {
|
|
10713
|
-
const absolute =
|
|
10714
|
-
return parseArtifactManifest(absolute, await (0,
|
|
11957
|
+
const absolute = import_node_path11.default.resolve(manifestPath);
|
|
11958
|
+
return parseArtifactManifest(absolute, await (0, import_promises7.readFile)(absolute, "utf8"));
|
|
10715
11959
|
}
|
|
10716
11960
|
async function readOptional(file) {
|
|
10717
11961
|
try {
|
|
10718
|
-
return await (0,
|
|
11962
|
+
return await (0, import_promises7.readFile)(file);
|
|
10719
11963
|
} catch (error) {
|
|
10720
11964
|
if (isCode(error, "ENOENT")) return null;
|
|
10721
11965
|
throw error;
|
|
@@ -10734,8 +11978,8 @@ function processIsAlive(pid) {
|
|
|
10734
11978
|
}
|
|
10735
11979
|
}
|
|
10736
11980
|
function validTemporaryPath(candidate, destination, type) {
|
|
10737
|
-
return
|
|
10738
|
-
`.${
|
|
11981
|
+
return import_node_path11.default.dirname(candidate) === import_node_path11.default.dirname(destination) && import_node_path11.default.basename(candidate).startsWith(
|
|
11982
|
+
`.${import_node_path11.default.basename(destination)}.pixelkiln-${type}-`
|
|
10739
11983
|
);
|
|
10740
11984
|
}
|
|
10741
11985
|
function parseTransaction(journal, data) {
|
|
@@ -10751,14 +11995,14 @@ function parseTransaction(journal, data) {
|
|
|
10751
11995
|
return raw;
|
|
10752
11996
|
}
|
|
10753
11997
|
async function removeTransactionFiles(journal) {
|
|
10754
|
-
await (0,
|
|
10755
|
-
await (0,
|
|
11998
|
+
await (0, import_promises7.rm)(journal, { force: true });
|
|
11999
|
+
await (0, import_promises7.rm)(transactionMarker(journal), { force: true });
|
|
10756
12000
|
}
|
|
10757
12001
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
10758
|
-
const journal =
|
|
12002
|
+
const journal = import_node_path11.default.resolve(recoveryFile);
|
|
10759
12003
|
const bytes = await readOptional(journal);
|
|
10760
12004
|
if (!bytes) {
|
|
10761
|
-
await (0,
|
|
12005
|
+
await (0, import_promises7.rm)(transactionMarker(journal), { force: true });
|
|
10762
12006
|
return;
|
|
10763
12007
|
}
|
|
10764
12008
|
if (activeTransactions.has(journal)) {
|
|
@@ -10771,7 +12015,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
10771
12015
|
);
|
|
10772
12016
|
}
|
|
10773
12017
|
for (const entry of transaction.entries) {
|
|
10774
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
12018
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path11.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
10775
12019
|
throw new Error(
|
|
10776
12020
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
10777
12021
|
);
|
|
@@ -10797,7 +12041,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
10797
12041
|
}
|
|
10798
12042
|
await remove2(entry.destination, errors);
|
|
10799
12043
|
try {
|
|
10800
|
-
await (0,
|
|
12044
|
+
await (0, import_promises7.rename)(entry.backup, entry.destination);
|
|
10801
12045
|
} catch (error) {
|
|
10802
12046
|
errors.push(
|
|
10803
12047
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -10817,7 +12061,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
10817
12061
|
await removeTransactionFiles(journal);
|
|
10818
12062
|
}
|
|
10819
12063
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
10820
|
-
const root =
|
|
12064
|
+
const root = import_node_path11.default.dirname(import_node_path11.default.resolve(manifestPath));
|
|
10821
12065
|
const sources = provenance.sources.map((source) => ({
|
|
10822
12066
|
...source,
|
|
10823
12067
|
path: portableRelative(root, source.path)
|
|
@@ -10847,11 +12091,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
10847
12091
|
];
|
|
10848
12092
|
}
|
|
10849
12093
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
10850
|
-
const absoluteManifest =
|
|
12094
|
+
const absoluteManifest = import_node_path11.default.resolve(manifestPath);
|
|
10851
12095
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
10852
12096
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
10853
12097
|
absoluteManifest,
|
|
10854
|
-
...outputs.map((output) =>
|
|
12098
|
+
...outputs.map((output) => import_node_path11.default.resolve(output.path))
|
|
10855
12099
|
]);
|
|
10856
12100
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
10857
12101
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -10869,13 +12113,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
10869
12113
|
}
|
|
10870
12114
|
}
|
|
10871
12115
|
if (!options.force) {
|
|
10872
|
-
const root =
|
|
12116
|
+
const root = import_node_path11.default.dirname(absoluteManifest);
|
|
10873
12117
|
const recorded = new Map(
|
|
10874
|
-
previous?.outputs.map((output) => [
|
|
12118
|
+
previous?.outputs.map((output) => [import_node_path11.default.resolve(root, output.path), output.sha256]) ?? []
|
|
10875
12119
|
);
|
|
10876
12120
|
const conflicts = [];
|
|
10877
12121
|
for (const output of outputs) {
|
|
10878
|
-
const destination =
|
|
12122
|
+
const destination = import_node_path11.default.resolve(output.path);
|
|
10879
12123
|
const current = await readOptional(destination);
|
|
10880
12124
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
10881
12125
|
const expected = recorded.get(destination);
|
|
@@ -10897,15 +12141,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
10897
12141
|
);
|
|
10898
12142
|
}
|
|
10899
12143
|
async function verifyArtifactBundle(manifestPath) {
|
|
10900
|
-
const absolute =
|
|
12144
|
+
const absolute = import_node_path11.default.resolve(manifestPath);
|
|
10901
12145
|
const raw = await readArtifactBundleManifest(absolute);
|
|
10902
|
-
const root =
|
|
12146
|
+
const root = import_node_path11.default.dirname(absolute);
|
|
10903
12147
|
const changedSources = [];
|
|
10904
12148
|
const changedOutputs = [];
|
|
10905
12149
|
for (const source of raw.sources) {
|
|
10906
12150
|
let actual = null;
|
|
10907
12151
|
try {
|
|
10908
|
-
actual = digest(await (0,
|
|
12152
|
+
actual = digest(await (0, import_promises7.readFile)(import_node_path11.default.resolve(root, source.path)));
|
|
10909
12153
|
} catch (error) {
|
|
10910
12154
|
if (!isCode(error, "ENOENT")) throw error;
|
|
10911
12155
|
}
|
|
@@ -10913,7 +12157,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
10913
12157
|
}
|
|
10914
12158
|
for (const output of raw.outputs) {
|
|
10915
12159
|
try {
|
|
10916
|
-
if (digest(await (0,
|
|
12160
|
+
if (digest(await (0, import_promises7.readFile)(import_node_path11.default.resolve(root, output.path))) !== output.sha256) {
|
|
10917
12161
|
changedOutputs.push(output.path);
|
|
10918
12162
|
}
|
|
10919
12163
|
} catch (error) {
|
|
@@ -10939,7 +12183,7 @@ function isCode(error, code) {
|
|
|
10939
12183
|
}
|
|
10940
12184
|
async function remove2(file, errors) {
|
|
10941
12185
|
try {
|
|
10942
|
-
await (0,
|
|
12186
|
+
await (0, import_promises7.rm)(file, { force: true });
|
|
10943
12187
|
} catch (error) {
|
|
10944
12188
|
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
10945
12189
|
}
|
|
@@ -10952,7 +12196,7 @@ async function rollback(prepared) {
|
|
|
10952
12196
|
for (const artifact of [...prepared].reverse()) {
|
|
10953
12197
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
10954
12198
|
try {
|
|
10955
|
-
await (0,
|
|
12199
|
+
await (0, import_promises7.rename)(artifact.backup, artifact.destination);
|
|
10956
12200
|
artifact.backedUp = false;
|
|
10957
12201
|
} catch (error) {
|
|
10958
12202
|
errors.push(
|
|
@@ -10969,7 +12213,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10969
12213
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
10970
12214
|
const normalized = files.map((file) => {
|
|
10971
12215
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
10972
|
-
return { destination:
|
|
12216
|
+
return { destination: import_node_path11.default.resolve(file.path), data: Buffer.from(file.data) };
|
|
10973
12217
|
});
|
|
10974
12218
|
const destinations = /* @__PURE__ */ new Set();
|
|
10975
12219
|
for (const artifact of normalized) {
|
|
@@ -10985,7 +12229,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10985
12229
|
const unchanged = [];
|
|
10986
12230
|
for (const artifact of normalized) {
|
|
10987
12231
|
try {
|
|
10988
|
-
const current = await (0,
|
|
12232
|
+
const current = await (0, import_promises7.readFile)(artifact.destination);
|
|
10989
12233
|
if (current.equals(artifact.data)) {
|
|
10990
12234
|
unchanged.push(artifact.destination);
|
|
10991
12235
|
} else {
|
|
@@ -10999,21 +12243,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10999
12243
|
if (!changed.length) return { changed: [], unchanged };
|
|
11000
12244
|
const token = (0, import_node_crypto6.randomUUID)();
|
|
11001
12245
|
for (const [index, artifact] of changed.entries()) {
|
|
11002
|
-
const basename =
|
|
11003
|
-
artifact.stage =
|
|
11004
|
-
|
|
12246
|
+
const basename = import_node_path11.default.basename(artifact.destination);
|
|
12247
|
+
artifact.stage = import_node_path11.default.join(
|
|
12248
|
+
import_node_path11.default.dirname(artifact.destination),
|
|
11005
12249
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
11006
12250
|
);
|
|
11007
12251
|
if (artifact.existed) {
|
|
11008
|
-
artifact.backup =
|
|
11009
|
-
|
|
12252
|
+
artifact.backup = import_node_path11.default.join(
|
|
12253
|
+
import_node_path11.default.dirname(artifact.destination),
|
|
11010
12254
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
11011
12255
|
);
|
|
11012
12256
|
}
|
|
11013
12257
|
}
|
|
11014
|
-
const journal = options.recoveryFile ?
|
|
12258
|
+
const journal = options.recoveryFile ? import_node_path11.default.resolve(options.recoveryFile) : null;
|
|
11015
12259
|
if (journal) {
|
|
11016
|
-
await (0,
|
|
12260
|
+
await (0, import_promises7.mkdir)(import_node_path11.default.dirname(journal), { recursive: true });
|
|
11017
12261
|
const transaction = {
|
|
11018
12262
|
format: "pixelkiln-artifact-transaction",
|
|
11019
12263
|
version: 1,
|
|
@@ -11027,7 +12271,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
11027
12271
|
}))
|
|
11028
12272
|
};
|
|
11029
12273
|
try {
|
|
11030
|
-
await (0,
|
|
12274
|
+
await (0, import_promises7.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
11031
12275
|
activeTransactions.add(journal);
|
|
11032
12276
|
} catch (error) {
|
|
11033
12277
|
throw new Error(
|
|
@@ -11038,9 +12282,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
11038
12282
|
}
|
|
11039
12283
|
try {
|
|
11040
12284
|
for (const [index, artifact] of changed.entries()) {
|
|
11041
|
-
await (0,
|
|
12285
|
+
await (0, import_promises7.mkdir)(import_node_path11.default.dirname(artifact.destination), { recursive: true });
|
|
11042
12286
|
await options.beforeStage?.(artifact.destination, index);
|
|
11043
|
-
await (0,
|
|
12287
|
+
await (0, import_promises7.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
|
|
11044
12288
|
}
|
|
11045
12289
|
} catch (error) {
|
|
11046
12290
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -11053,17 +12297,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
11053
12297
|
try {
|
|
11054
12298
|
for (const artifact of changed) {
|
|
11055
12299
|
if (!artifact.existed) continue;
|
|
11056
|
-
await (0,
|
|
12300
|
+
await (0, import_promises7.rename)(artifact.destination, artifact.backup);
|
|
11057
12301
|
artifact.backedUp = true;
|
|
11058
12302
|
}
|
|
11059
12303
|
for (const [index, artifact] of changed.entries()) {
|
|
11060
12304
|
await options.beforePromote?.(artifact.destination, index);
|
|
11061
|
-
await (0,
|
|
12305
|
+
await (0, import_promises7.rename)(artifact.stage, artifact.destination);
|
|
11062
12306
|
artifact.stage = void 0;
|
|
11063
12307
|
artifact.promoted = true;
|
|
11064
12308
|
}
|
|
11065
12309
|
if (journal) {
|
|
11066
|
-
await (0,
|
|
12310
|
+
await (0, import_promises7.writeFile)(transactionMarker(journal), `${token}
|
|
11067
12311
|
`, { flag: "wx" });
|
|
11068
12312
|
durableCommit = true;
|
|
11069
12313
|
await options.afterCommit?.();
|
|
@@ -11304,16 +12548,16 @@ function sameFrameGrid(first, next) {
|
|
|
11304
12548
|
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
11305
12549
|
}
|
|
11306
12550
|
async function refineAsset(options) {
|
|
11307
|
-
const source =
|
|
11308
|
-
const output =
|
|
12551
|
+
const source = import_node_path12.default.resolve(options.source);
|
|
12552
|
+
const output = import_node_path12.default.resolve(options.output);
|
|
11309
12553
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
11310
|
-
const record =
|
|
12554
|
+
const record = import_node_path12.default.resolve(qualityRecordPath(output));
|
|
11311
12555
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
11312
12556
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
11313
12557
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
11314
12558
|
let sourcePng;
|
|
11315
12559
|
try {
|
|
11316
|
-
sourcePng = decodePng(await (0,
|
|
12560
|
+
sourcePng = decodePng(await (0, import_promises8.readFile)(source));
|
|
11317
12561
|
} catch (error) {
|
|
11318
12562
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
11319
12563
|
}
|
|
@@ -11330,8 +12574,8 @@ async function refineAsset(options) {
|
|
|
11330
12574
|
);
|
|
11331
12575
|
}
|
|
11332
12576
|
}
|
|
11333
|
-
const temp = await (0,
|
|
11334
|
-
const recovered =
|
|
12577
|
+
const temp = await (0, import_promises8.mkdtemp)(import_node_path12.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
|
|
12578
|
+
const recovered = import_node_path12.default.join(temp, "native.png");
|
|
11335
12579
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
11336
12580
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
11337
12581
|
try {
|
|
@@ -11352,7 +12596,7 @@ async function refineAsset(options) {
|
|
|
11352
12596
|
);
|
|
11353
12597
|
}
|
|
11354
12598
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
11355
|
-
const native = decodePng(await (0,
|
|
12599
|
+
const native = decodePng(await (0, import_promises8.readFile)(recovered));
|
|
11356
12600
|
const quantized = quantizeToPalette(native, palette);
|
|
11357
12601
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
11358
12602
|
const audit = auditRefinement(
|
|
@@ -11402,7 +12646,7 @@ async function refineAsset(options) {
|
|
|
11402
12646
|
);
|
|
11403
12647
|
return { source, output, record, detection, palette, audit };
|
|
11404
12648
|
} finally {
|
|
11405
|
-
await (0,
|
|
12649
|
+
await (0, import_promises8.rm)(temp, { recursive: true, force: true });
|
|
11406
12650
|
}
|
|
11407
12651
|
}
|
|
11408
12652
|
async function refineFrameSet(options) {
|
|
@@ -11416,18 +12660,18 @@ async function refineFrameSet(options) {
|
|
|
11416
12660
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
11417
12661
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
11418
12662
|
}
|
|
11419
|
-
const output =
|
|
11420
|
-
const record =
|
|
12663
|
+
const output = import_node_path12.default.resolve(options.output);
|
|
12664
|
+
const record = import_node_path12.default.resolve(qualityRecordPath(output));
|
|
11421
12665
|
const palette = normalizeRefinementPalette(options.palette);
|
|
11422
12666
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
11423
12667
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
11424
12668
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
11425
12669
|
}
|
|
11426
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
12670
|
+
const sourceSet = new Set(options.sources.map((source) => import_node_path12.default.resolve(source.path)));
|
|
11427
12671
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
11428
12672
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
11429
12673
|
}
|
|
11430
|
-
const temp = await (0,
|
|
12674
|
+
const temp = await (0, import_promises8.mkdtemp)(import_node_path12.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
|
|
11431
12675
|
try {
|
|
11432
12676
|
const frames = [];
|
|
11433
12677
|
const outputs = [];
|
|
@@ -11435,8 +12679,8 @@ async function refineFrameSet(options) {
|
|
|
11435
12679
|
const sourcePaths = [];
|
|
11436
12680
|
let firstOptions = null;
|
|
11437
12681
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
11438
|
-
const source =
|
|
11439
|
-
const temporaryOutput =
|
|
12682
|
+
const source = import_node_path12.default.resolve(sourceEntry.path);
|
|
12683
|
+
const temporaryOutput = import_node_path12.default.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
11440
12684
|
const refined = await refineAsset({
|
|
11441
12685
|
source,
|
|
11442
12686
|
output: temporaryOutput,
|
|
@@ -11468,7 +12712,7 @@ async function refineFrameSet(options) {
|
|
|
11468
12712
|
});
|
|
11469
12713
|
outputs.push({
|
|
11470
12714
|
path: finalOutputs[index],
|
|
11471
|
-
data: await (0,
|
|
12715
|
+
data: await (0, import_promises8.readFile)(temporaryOutput)
|
|
11472
12716
|
});
|
|
11473
12717
|
}
|
|
11474
12718
|
const first = frames[0];
|
|
@@ -11511,23 +12755,23 @@ async function refineFrameSet(options) {
|
|
|
11511
12755
|
);
|
|
11512
12756
|
return {
|
|
11513
12757
|
sources: sourcePaths,
|
|
11514
|
-
outputs: outputs.map((item) =>
|
|
12758
|
+
outputs: outputs.map((item) => import_node_path12.default.resolve(item.path)),
|
|
11515
12759
|
record,
|
|
11516
12760
|
frames,
|
|
11517
12761
|
palette,
|
|
11518
12762
|
audit
|
|
11519
12763
|
};
|
|
11520
12764
|
} finally {
|
|
11521
|
-
await (0,
|
|
12765
|
+
await (0, import_promises8.rm)(temp, { recursive: true, force: true });
|
|
11522
12766
|
}
|
|
11523
12767
|
}
|
|
11524
12768
|
async function checkQualityRecord(recordPath) {
|
|
11525
|
-
const record =
|
|
12769
|
+
const record = import_node_path12.default.resolve(recordPath);
|
|
11526
12770
|
const manifest = await readArtifactBundleManifest(record);
|
|
11527
12771
|
const options = parseRecordOptions(manifest, record);
|
|
11528
12772
|
const verification = await verifyArtifactBundle(record);
|
|
11529
|
-
const sources = manifest.sources.map((source2) =>
|
|
11530
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
12773
|
+
const sources = manifest.sources.map((source2) => import_node_path12.default.resolve(import_node_path12.default.dirname(record), source2.path));
|
|
12774
|
+
const outputs = manifest.outputs.map((output2) => import_node_path12.default.resolve(import_node_path12.default.dirname(record), output2.path));
|
|
11531
12775
|
const source = sources[0] ?? "";
|
|
11532
12776
|
const output = outputs[0] ?? "";
|
|
11533
12777
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -11560,7 +12804,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
11560
12804
|
async function approveQualityRecord(recordPath, approval) {
|
|
11561
12805
|
const reviewer = approval.reviewer.trim();
|
|
11562
12806
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
11563
|
-
const record =
|
|
12807
|
+
const record = import_node_path12.default.resolve(recordPath);
|
|
11564
12808
|
const manifest = await readArtifactBundleManifest(record);
|
|
11565
12809
|
const options = parseRecordOptions(manifest, record);
|
|
11566
12810
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -11581,14 +12825,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
11581
12825
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
11582
12826
|
);
|
|
11583
12827
|
}
|
|
11584
|
-
const root =
|
|
12828
|
+
const root = import_node_path12.default.dirname(record);
|
|
11585
12829
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
11586
|
-
const outputPath =
|
|
11587
|
-
return { path: outputPath, data: await (0,
|
|
12830
|
+
const outputPath = import_node_path12.default.resolve(root, entry.path);
|
|
12831
|
+
return { path: outputPath, data: await (0, import_promises8.readFile)(outputPath) };
|
|
11588
12832
|
}));
|
|
11589
12833
|
const sources = manifest.sources.map((source) => ({
|
|
11590
12834
|
...source,
|
|
11591
|
-
path:
|
|
12835
|
+
path: import_node_path12.default.resolve(root, source.path)
|
|
11592
12836
|
}));
|
|
11593
12837
|
const approved = {
|
|
11594
12838
|
...options,
|
|
@@ -11620,8 +12864,8 @@ async function qualitySource(spec, lock) {
|
|
|
11620
12864
|
if (spec.generator === "frames") {
|
|
11621
12865
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
11622
12866
|
}
|
|
11623
|
-
const source =
|
|
11624
|
-
return (0,
|
|
12867
|
+
const source = import_node_path13.default.resolve(spec.root, spec.source);
|
|
12868
|
+
return (0, import_node_fs10.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
11625
12869
|
}
|
|
11626
12870
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
11627
12871
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -11650,7 +12894,7 @@ async function qualitySource(spec, lock) {
|
|
|
11650
12894
|
}
|
|
11651
12895
|
if (role) roles.add(role);
|
|
11652
12896
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
11653
|
-
if (!(0,
|
|
12897
|
+
if (!(0, import_node_fs10.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
11654
12898
|
if (await sha256File(source) !== output.sha256) {
|
|
11655
12899
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
11656
12900
|
}
|
|
@@ -11669,7 +12913,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
11669
12913
|
}
|
|
11670
12914
|
const sourcePaths = source.items.map((item) => item.path);
|
|
11671
12915
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
11672
|
-
if (!(0,
|
|
12916
|
+
if (!(0, import_node_fs10.existsSync)(record)) {
|
|
11673
12917
|
return {
|
|
11674
12918
|
key,
|
|
11675
12919
|
state: "needs-refinement",
|
|
@@ -11687,7 +12931,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
11687
12931
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
11688
12932
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
11689
12933
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
11690
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
12934
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path13.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path13.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path13.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path13.default.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
11691
12935
|
if (contractChanged) {
|
|
11692
12936
|
return {
|
|
11693
12937
|
key,
|
|
@@ -11850,8 +13094,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
11850
13094
|
}
|
|
11851
13095
|
|
|
11852
13096
|
// src/pipeline/revision.ts
|
|
11853
|
-
var
|
|
11854
|
-
var
|
|
13097
|
+
var import_node_fs11 = require("fs");
|
|
13098
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
11855
13099
|
async function inspectRevisionReadiness(spec, lock) {
|
|
11856
13100
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
11857
13101
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -11860,6 +13104,7 @@ async function inspectRevisionReadiness(spec, lock) {
|
|
|
11860
13104
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
11861
13105
|
if (spec.scene) return inspectScene(spec);
|
|
11862
13106
|
if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
|
|
13107
|
+
if (spec.terrainConnect) return inspectTerrainConnect(spec, lock, /* @__PURE__ */ new Set());
|
|
11863
13108
|
if (!spec.revision) return null;
|
|
11864
13109
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
11865
13110
|
}
|
|
@@ -11868,7 +13113,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
|
|
|
11868
13113
|
const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
|
|
11869
13114
|
const dependency = await inspectParent(anchor.spec, lock, seen);
|
|
11870
13115
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11871
|
-
if (!anchor.sha256 || !(0,
|
|
13116
|
+
if (!anchor.sha256 || !(0, import_node_fs11.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11872
13117
|
if (await sha256File(anchor.file) !== anchor.sha256) {
|
|
11873
13118
|
return { ready: false, reason: `${label} changed after the manifest was resolved` };
|
|
11874
13119
|
}
|
|
@@ -11877,19 +13122,31 @@ async function inspectStyleAnchor(spec, lock, seen) {
|
|
|
11877
13122
|
}
|
|
11878
13123
|
return { ready: true, reason: `${label} is current` };
|
|
11879
13124
|
}
|
|
13125
|
+
async function inspectTerrainConnect(spec, lock, seen) {
|
|
13126
|
+
for (const link of [spec.terrainConnect.lower, spec.terrainConnect.upper]) {
|
|
13127
|
+
if (!link) continue;
|
|
13128
|
+
const label = `connected set ${spec.styleId}/${link.assetId}`;
|
|
13129
|
+
const dependency = await inspectParent(link.spec, lock, seen);
|
|
13130
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
13131
|
+
if (!lock.entries[lockKey(link.spec.styleId, link.spec.assetId)]?.objectId) {
|
|
13132
|
+
return { ready: false, reason: `${label} has no PixelLab tileset id recorded` };
|
|
13133
|
+
}
|
|
13134
|
+
}
|
|
13135
|
+
return { ready: true, reason: "connected sets are current" };
|
|
13136
|
+
}
|
|
11880
13137
|
async function inspectScene(spec) {
|
|
11881
13138
|
const scene = spec.scene;
|
|
11882
13139
|
const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
|
|
11883
13140
|
if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
|
|
11884
13141
|
for (const { label, file, sha256: sha2562 } of files) {
|
|
11885
|
-
if (!sha2562 || !(0,
|
|
13142
|
+
if (!sha2562 || !(0, import_node_fs11.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
|
|
11886
13143
|
if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
|
|
11887
13144
|
}
|
|
11888
13145
|
return { ready: true, reason: "scene is current" };
|
|
11889
13146
|
}
|
|
11890
13147
|
async function inspectVfxSprite(spec) {
|
|
11891
13148
|
const sprite = spec.vfx.sprite;
|
|
11892
|
-
if (!sprite.sha256 || !(0,
|
|
13149
|
+
if (!sprite.sha256 || !(0, import_node_fs11.existsSync)(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
|
|
11893
13150
|
if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
|
|
11894
13151
|
return { ready: true, reason: "vfx sprite is current" };
|
|
11895
13152
|
}
|
|
@@ -11905,7 +13162,7 @@ async function inspectOutfit(spec, lock, seen) {
|
|
|
11905
13162
|
const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
|
|
11906
13163
|
const dependency = await inspectParent(character.parentSpec, lock, seen);
|
|
11907
13164
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11908
|
-
if (!(0,
|
|
13165
|
+
if (!(0, import_node_fs11.existsSync)(outfit.reference.path)) {
|
|
11909
13166
|
return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
|
|
11910
13167
|
}
|
|
11911
13168
|
if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
|
|
@@ -11919,7 +13176,7 @@ async function inspectCharacterParent(spec, lock, seen) {
|
|
|
11919
13176
|
const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
|
|
11920
13177
|
const dependency = await inspectParent(parentSpec, lock, seen);
|
|
11921
13178
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11922
|
-
if (!character.parentSha256 || !character.parentFile || !(0,
|
|
13179
|
+
if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs11.existsSync)(character.parentFile)) {
|
|
11923
13180
|
return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11924
13181
|
}
|
|
11925
13182
|
if (await sha256File(character.parentFile) !== character.parentSha256) {
|
|
@@ -11937,7 +13194,7 @@ async function inspectObjectProParent(spec, lock, seen) {
|
|
|
11937
13194
|
const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
|
|
11938
13195
|
const dependency = await inspectParent(parentSpec, lock, seen);
|
|
11939
13196
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11940
|
-
if (!object.parentSha256 || !object.parentFile || !(0,
|
|
13197
|
+
if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs11.existsSync)(object.parentFile)) {
|
|
11941
13198
|
return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11942
13199
|
}
|
|
11943
13200
|
if (await sha256File(object.parentFile) !== object.parentSha256) {
|
|
@@ -11963,7 +13220,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11963
13220
|
const members = await inspectSourceMembers(spec, lock);
|
|
11964
13221
|
if (!members.ready) return members;
|
|
11965
13222
|
} else {
|
|
11966
|
-
if (!revision.sourceSha256 || !(0,
|
|
13223
|
+
if (!revision.sourceSha256 || !(0, import_node_fs11.existsSync)(revision.sourceFile)) {
|
|
11967
13224
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
11968
13225
|
}
|
|
11969
13226
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
@@ -11971,7 +13228,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11971
13228
|
}
|
|
11972
13229
|
}
|
|
11973
13230
|
if (revision.maskFile) {
|
|
11974
|
-
if (!revision.maskSha256 || !(0,
|
|
13231
|
+
if (!revision.maskSha256 || !(0, import_node_fs11.existsSync)(revision.maskFile)) {
|
|
11975
13232
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
11976
13233
|
}
|
|
11977
13234
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -11979,7 +13236,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11979
13236
|
}
|
|
11980
13237
|
}
|
|
11981
13238
|
if (revision.lastFrameFile) {
|
|
11982
|
-
if (!revision.lastFrameSha256 || !(0,
|
|
13239
|
+
if (!revision.lastFrameSha256 || !(0, import_node_fs11.existsSync)(revision.lastFrameFile)) {
|
|
11983
13240
|
return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
|
|
11984
13241
|
}
|
|
11985
13242
|
if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
|
|
@@ -11992,7 +13249,7 @@ async function inspectSourceMembers(spec, lock) {
|
|
|
11992
13249
|
const revision = spec.revision;
|
|
11993
13250
|
const members = revision.sourceMembers;
|
|
11994
13251
|
for (const member of members) {
|
|
11995
|
-
if (!(0,
|
|
13252
|
+
if (!(0, import_node_fs11.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
|
|
11996
13253
|
if (await sha256File(member.file) !== member.sha256) {
|
|
11997
13254
|
return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
|
|
11998
13255
|
}
|
|
@@ -12001,8 +13258,8 @@ async function inspectSourceMembers(spec, lock) {
|
|
|
12001
13258
|
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
12002
13259
|
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
12003
13260
|
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
12004
|
-
const recorded = entry.outputs.map((_, index) =>
|
|
12005
|
-
const found = members.map((member) =>
|
|
13261
|
+
const recorded = entry.outputs.map((_, index) => import_node_path14.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
13262
|
+
const found = members.map((member) => import_node_path14.default.resolve(member.file)).sort();
|
|
12006
13263
|
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
12007
13264
|
return {
|
|
12008
13265
|
ready: false,
|
|
@@ -12043,6 +13300,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
12043
13300
|
const inputs = await inspectObjectProParent(spec, lock, nextSeen);
|
|
12044
13301
|
if (!inputs.ready) return inputs;
|
|
12045
13302
|
}
|
|
13303
|
+
if (spec.terrainConnect) {
|
|
13304
|
+
const inputs = await inspectTerrainConnect(spec, lock, nextSeen);
|
|
13305
|
+
if (!inputs.ready) return inputs;
|
|
13306
|
+
}
|
|
12046
13307
|
if (spec.quality) {
|
|
12047
13308
|
const quality = await inspectQualityProfile(spec, lock);
|
|
12048
13309
|
return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
|
|
@@ -12051,8 +13312,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
12051
13312
|
};
|
|
12052
13313
|
}
|
|
12053
13314
|
if (spec.source) {
|
|
12054
|
-
const source =
|
|
12055
|
-
return (0,
|
|
13315
|
+
const source = import_node_path14.default.resolve(spec.root, spec.source);
|
|
13316
|
+
return (0, import_node_fs11.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
12056
13317
|
}
|
|
12057
13318
|
const entry = lock.entries[key];
|
|
12058
13319
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -12066,7 +13327,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
12066
13327
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
12067
13328
|
const output = entry.outputs[index];
|
|
12068
13329
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
12069
|
-
if (!(0,
|
|
13330
|
+
if (!(0, import_node_fs11.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
12070
13331
|
if (await sha256File(file) !== output.sha256) {
|
|
12071
13332
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
12072
13333
|
}
|
|
@@ -12088,9 +13349,21 @@ function dependencies(spec) {
|
|
|
12088
13349
|
spec.revision?.sourceSpec,
|
|
12089
13350
|
spec.character?.parentSpec,
|
|
12090
13351
|
spec.character?.styleAnchor?.spec,
|
|
12091
|
-
spec.objectPro?.parentSpec
|
|
13352
|
+
spec.objectPro?.parentSpec,
|
|
13353
|
+
spec.terrainConnect?.lower?.spec,
|
|
13354
|
+
spec.terrainConnect?.upper?.spec
|
|
12092
13355
|
].filter((parent) => Boolean(parent));
|
|
12093
13356
|
}
|
|
13357
|
+
function changedTerrainConnection(spec, entry, lock) {
|
|
13358
|
+
for (const side of ["lower", "upper"]) {
|
|
13359
|
+
const link = spec.terrainConnect?.[side];
|
|
13360
|
+
if (!link) continue;
|
|
13361
|
+
const other = lock.entries[lockKey(link.spec.styleId, link.spec.assetId)];
|
|
13362
|
+
const recorded = entry.terrainConnect?.[side];
|
|
13363
|
+
if (!recorded || recorded.sourceObjectId !== other?.objectId) return link.assetId;
|
|
13364
|
+
}
|
|
13365
|
+
return null;
|
|
13366
|
+
}
|
|
12094
13367
|
function resumeCommandForStatus(status) {
|
|
12095
13368
|
if (status === "pending" || status === "processing") return "poll";
|
|
12096
13369
|
if (status === "review") return "pick";
|
|
@@ -12122,6 +13395,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
12122
13395
|
const entry = lock.entries[key];
|
|
12123
13396
|
let state;
|
|
12124
13397
|
let reason;
|
|
13398
|
+
const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
|
|
12125
13399
|
const revision = await inspectRevisionReadiness(spec, lock);
|
|
12126
13400
|
if (revision && !revision.ready) {
|
|
12127
13401
|
state = "blocked";
|
|
@@ -12130,7 +13404,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
12130
13404
|
state = "missing";
|
|
12131
13405
|
reason = "--force";
|
|
12132
13406
|
} else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
|
|
12133
|
-
if ((0,
|
|
13407
|
+
if ((0, import_node_fs12.existsSync)(import_node_path15.default.resolve(spec.root, spec.source))) {
|
|
12134
13408
|
state = "ok";
|
|
12135
13409
|
reason = `placed from ${spec.source}; not generated`;
|
|
12136
13410
|
} else {
|
|
@@ -12138,7 +13412,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
12138
13412
|
reason = `declared source is missing: ${spec.source}`;
|
|
12139
13413
|
}
|
|
12140
13414
|
} else if (!entry) {
|
|
12141
|
-
if ((0,
|
|
13415
|
+
if ((0, import_node_fs12.existsSync)(spec.outFile)) {
|
|
12142
13416
|
state = "untracked";
|
|
12143
13417
|
reason = "file on disk, no provenance recorded";
|
|
12144
13418
|
} else {
|
|
@@ -12151,12 +13425,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
12151
13425
|
} else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
|
|
12152
13426
|
state = "stale";
|
|
12153
13427
|
reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
|
|
12154
|
-
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0,
|
|
13428
|
+
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs12.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
|
|
12155
13429
|
state = "stale";
|
|
12156
13430
|
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
12157
13431
|
} else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
|
|
12158
13432
|
state = "stale";
|
|
12159
13433
|
reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
|
|
13434
|
+
} else if (changedConnection) {
|
|
13435
|
+
state = "stale";
|
|
13436
|
+
reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
|
|
12160
13437
|
} else if (entry.status === "download-failed") {
|
|
12161
13438
|
state = "recoverable";
|
|
12162
13439
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -12179,9 +13456,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
12179
13456
|
} else if (entry.status !== "downloaded") {
|
|
12180
13457
|
state = "in-flight";
|
|
12181
13458
|
reason = `awaiting ${entry.status}`;
|
|
12182
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0,
|
|
13459
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs12.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
|
|
12183
13460
|
state = "orphaned";
|
|
12184
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0,
|
|
13461
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs12.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
12185
13462
|
} else if (await anyOutputModified(entry, spec)) {
|
|
12186
13463
|
state = "orphaned";
|
|
12187
13464
|
reason = "output modified since download";
|
|
@@ -12272,13 +13549,13 @@ function summarize(plan) {
|
|
|
12272
13549
|
|
|
12273
13550
|
// src/project.ts
|
|
12274
13551
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
12275
|
-
const absolute =
|
|
12276
|
-
const root =
|
|
13552
|
+
const absolute = import_node_path16.default.resolve(manifestPath);
|
|
13553
|
+
const root = import_node_path16.default.dirname(absolute);
|
|
12277
13554
|
if (opts.env !== false) {
|
|
12278
13555
|
loadEnvFiles(root);
|
|
12279
|
-
if (
|
|
13556
|
+
if (import_node_path16.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
12280
13557
|
}
|
|
12281
|
-
const lockPath =
|
|
13558
|
+
const lockPath = import_node_path16.default.resolve(opts.lockPath ?? import_node_path16.default.join(root, "pixelkiln.lock.json"));
|
|
12282
13559
|
const loaded = await loadManifest(absolute);
|
|
12283
13560
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
12284
13561
|
const lock = await loadLock(lockPath);
|
|
@@ -12307,47 +13584,10 @@ async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {})
|
|
|
12307
13584
|
};
|
|
12308
13585
|
}
|
|
12309
13586
|
|
|
12310
|
-
// src/source-url.ts
|
|
12311
|
-
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
12312
|
-
"access_token",
|
|
12313
|
-
"apikey",
|
|
12314
|
-
"api_key",
|
|
12315
|
-
"auth",
|
|
12316
|
-
"authorization",
|
|
12317
|
-
"awsaccesskeyid",
|
|
12318
|
-
"googleaccessid",
|
|
12319
|
-
"key",
|
|
12320
|
-
"sig"
|
|
12321
|
-
]);
|
|
12322
|
-
function isSensitiveSourceUrl(value) {
|
|
12323
|
-
let parsed;
|
|
12324
|
-
try {
|
|
12325
|
-
parsed = new URL(value);
|
|
12326
|
-
} catch {
|
|
12327
|
-
return false;
|
|
12328
|
-
}
|
|
12329
|
-
if (parsed.username || parsed.password) return true;
|
|
12330
|
-
for (const key of parsed.searchParams.keys()) {
|
|
12331
|
-
const normalized = key.toLowerCase();
|
|
12332
|
-
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
12333
|
-
}
|
|
12334
|
-
return false;
|
|
12335
|
-
}
|
|
12336
|
-
function shouldPersistSourceUrl(value) {
|
|
12337
|
-
let parsed;
|
|
12338
|
-
try {
|
|
12339
|
-
parsed = new URL(value);
|
|
12340
|
-
} catch {
|
|
12341
|
-
return true;
|
|
12342
|
-
}
|
|
12343
|
-
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
12344
|
-
return !isSensitiveSourceUrl(value);
|
|
12345
|
-
}
|
|
12346
|
-
|
|
12347
13587
|
// src/recipes.ts
|
|
12348
|
-
var
|
|
12349
|
-
var
|
|
12350
|
-
var
|
|
13588
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
13589
|
+
var import_node_fs13 = require("fs");
|
|
13590
|
+
var import_promises9 = require("fs/promises");
|
|
12351
13591
|
var import_node_url = require("url");
|
|
12352
13592
|
var import_zod4 = require("zod");
|
|
12353
13593
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -12356,7 +13596,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
12356
13596
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
12357
13597
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
12358
13598
|
var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
|
|
12359
|
-
if (value.includes("\\") ||
|
|
13599
|
+
if (value.includes("\\") || import_node_path17.default.posix.isAbsolute(value)) return false;
|
|
12360
13600
|
const parts = value.split("/");
|
|
12361
13601
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
12362
13602
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -12532,10 +13772,10 @@ function message3(error) {
|
|
|
12532
13772
|
return error instanceof Error ? error.message : String(error);
|
|
12533
13773
|
}
|
|
12534
13774
|
async function readRecipeFile(recipePath, bundled) {
|
|
12535
|
-
const absolute =
|
|
13775
|
+
const absolute = import_node_path17.default.resolve(recipePath);
|
|
12536
13776
|
let raw;
|
|
12537
13777
|
try {
|
|
12538
|
-
raw = JSON.parse(await (0,
|
|
13778
|
+
raw = JSON.parse(await (0, import_promises9.readFile)(absolute, "utf8"));
|
|
12539
13779
|
} catch (error) {
|
|
12540
13780
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
12541
13781
|
}
|
|
@@ -12546,13 +13786,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12546
13786
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12547
13787
|
);
|
|
12548
13788
|
}
|
|
12549
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
13789
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path17.default.dirname(absolute), bundled };
|
|
12550
13790
|
}
|
|
12551
13791
|
async function walkRecipeFiles(root) {
|
|
12552
|
-
if (!(0,
|
|
13792
|
+
if (!(0, import_node_fs13.existsSync)(root)) return [];
|
|
12553
13793
|
const out = [];
|
|
12554
|
-
for (const entry of await (0,
|
|
12555
|
-
const resolved =
|
|
13794
|
+
for (const entry of await (0, import_promises9.readdir)(root, { withFileTypes: true })) {
|
|
13795
|
+
const resolved = import_node_path17.default.join(root, entry.name);
|
|
12556
13796
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12557
13797
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12558
13798
|
}
|
|
@@ -12563,7 +13803,7 @@ function bundledRecipeRoot() {
|
|
|
12563
13803
|
}
|
|
12564
13804
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12565
13805
|
const recipes = await Promise.all(
|
|
12566
|
-
(await walkRecipeFiles(
|
|
13806
|
+
(await walkRecipeFiles(import_node_path17.default.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12567
13807
|
);
|
|
12568
13808
|
return recipes.sort(
|
|
12569
13809
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12586,9 +13826,9 @@ function parseSelector(selector) {
|
|
|
12586
13826
|
return { id: match[1], version: match[2] };
|
|
12587
13827
|
}
|
|
12588
13828
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12589
|
-
const local =
|
|
12590
|
-
if ((0,
|
|
12591
|
-
const recipePath =
|
|
13829
|
+
const local = import_node_path17.default.resolve(target);
|
|
13830
|
+
if ((0, import_node_fs13.existsSync)(local)) {
|
|
13831
|
+
const recipePath = import_node_path17.default.basename(local) === RECIPE_FILE ? local : import_node_path17.default.join(local, RECIPE_FILE);
|
|
12592
13832
|
return readRecipeFile(recipePath, false);
|
|
12593
13833
|
}
|
|
12594
13834
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12602,8 +13842,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12602
13842
|
return matches[0];
|
|
12603
13843
|
}
|
|
12604
13844
|
async function verifyFile(root, file) {
|
|
12605
|
-
const absolute =
|
|
12606
|
-
if (!(0,
|
|
13845
|
+
const absolute = import_node_path17.default.resolve(root, ...file.path.split("/"));
|
|
13846
|
+
if (!(0, import_node_fs13.existsSync)(absolute)) {
|
|
12607
13847
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12608
13848
|
}
|
|
12609
13849
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12618,7 +13858,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12618
13858
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12619
13859
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
12620
13860
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12621
|
-
const modelRoot = options.modelRoot ?
|
|
13861
|
+
const modelRoot = options.modelRoot ? import_node_path17.default.resolve(options.modelRoot) : null;
|
|
12622
13862
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12623
13863
|
...await verifyFile(modelRoot, model),
|
|
12624
13864
|
source: model.source,
|
|
@@ -12652,7 +13892,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12652
13892
|
};
|
|
12653
13893
|
}
|
|
12654
13894
|
function renderStyle(style, destination, cwd) {
|
|
12655
|
-
const portableDir =
|
|
13895
|
+
const portableDir = import_node_path17.default.relative(cwd, destination).split(import_node_path17.default.sep).join("/") || ".";
|
|
12656
13896
|
const replace = (value) => {
|
|
12657
13897
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12658
13898
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12664,25 +13904,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
12664
13904
|
return replace(style);
|
|
12665
13905
|
}
|
|
12666
13906
|
async function installRecipe(target, options = {}) {
|
|
12667
|
-
const cwd =
|
|
13907
|
+
const cwd = import_node_path17.default.resolve(options.cwd ?? process.cwd());
|
|
12668
13908
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12669
13909
|
const verification = await verifyRecipe(loaded.path);
|
|
12670
13910
|
if (!verification.ok) {
|
|
12671
13911
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12672
13912
|
}
|
|
12673
|
-
const destination = options.out ?
|
|
13913
|
+
const destination = options.out ? import_node_path17.default.resolve(cwd, options.out) : import_node_path17.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12674
13914
|
const sources = [
|
|
12675
13915
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12676
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
13916
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path17.default.join(loaded.dir, ...file.path.split("/")) }))
|
|
12677
13917
|
];
|
|
12678
13918
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12679
|
-
path:
|
|
12680
|
-
data: await (0,
|
|
13919
|
+
path: import_node_path17.default.join(destination, ...file.path.split("/")),
|
|
13920
|
+
data: await (0, import_promises9.readFile)(file.source)
|
|
12681
13921
|
})));
|
|
12682
13922
|
if (!options.force) {
|
|
12683
13923
|
for (const file of files) {
|
|
12684
|
-
if (!(0,
|
|
12685
|
-
const current = await (0,
|
|
13924
|
+
if (!(0, import_node_fs13.existsSync)(file.path)) continue;
|
|
13925
|
+
const current = await (0, import_promises9.readFile)(file.path);
|
|
12686
13926
|
if (!current.equals(file.data)) {
|
|
12687
13927
|
throw new OverwriteRefusedError(
|
|
12688
13928
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -12706,9 +13946,9 @@ async function installRecipe(target, options = {}) {
|
|
|
12706
13946
|
}
|
|
12707
13947
|
|
|
12708
13948
|
// src/pipeline/audit.ts
|
|
12709
|
-
var
|
|
12710
|
-
var
|
|
12711
|
-
var
|
|
13949
|
+
var import_promises10 = require("fs/promises");
|
|
13950
|
+
var import_node_fs14 = require("fs");
|
|
13951
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
12712
13952
|
function paletteDistance(asset, reference) {
|
|
12713
13953
|
if (!asset.length || !reference.length) return 0;
|
|
12714
13954
|
let total = 0;
|
|
@@ -12744,12 +13984,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12744
13984
|
const unreadable = [];
|
|
12745
13985
|
for (const spec of mine) {
|
|
12746
13986
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
12747
|
-
if (!(0,
|
|
13987
|
+
if (!(0, import_node_fs14.existsSync)(output.absolutePath)) {
|
|
12748
13988
|
missing.push(output.id);
|
|
12749
13989
|
continue;
|
|
12750
13990
|
}
|
|
12751
13991
|
try {
|
|
12752
|
-
const png = decodePng(await (0,
|
|
13992
|
+
const png = decodePng(await (0, import_promises10.readFile)(output.absolutePath));
|
|
12753
13993
|
const palette = extractPalette(png, 12);
|
|
12754
13994
|
assets.push({
|
|
12755
13995
|
assetId: spec.assetId,
|
|
@@ -12773,10 +14013,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12773
14013
|
let referenceFromStyleImages = false;
|
|
12774
14014
|
const refPalettes = [];
|
|
12775
14015
|
for (const img of style.styleImages) {
|
|
12776
|
-
const abs =
|
|
12777
|
-
if (!(0,
|
|
14016
|
+
const abs = import_node_path18.default.resolve(loaded.root, img.path);
|
|
14017
|
+
if (!(0, import_node_fs14.existsSync)(abs)) continue;
|
|
12778
14018
|
try {
|
|
12779
|
-
refPalettes.push(extractPalette(decodePng(await (0,
|
|
14019
|
+
refPalettes.push(extractPalette(decodePng(await (0, import_promises10.readFile)(abs)), 12));
|
|
12780
14020
|
} catch {
|
|
12781
14021
|
}
|
|
12782
14022
|
}
|
|
@@ -12851,144 +14091,6 @@ function hex(c) {
|
|
|
12851
14091
|
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
12852
14092
|
}
|
|
12853
14093
|
|
|
12854
|
-
// src/pixellab-utils.ts
|
|
12855
|
-
var import_node_fs14 = require("fs");
|
|
12856
|
-
var import_promises10 = require("fs/promises");
|
|
12857
|
-
var import_node_path18 = __toESM(require("path"), 1);
|
|
12858
|
-
var UNZOOM_MIN_SIDE = 256;
|
|
12859
|
-
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
12860
|
-
function defaultUnzoomOut(input) {
|
|
12861
|
-
const ext = import_node_path18.default.extname(input);
|
|
12862
|
-
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
12863
|
-
}
|
|
12864
|
-
async function unzoomFile(client2, input, opts = {}) {
|
|
12865
|
-
const out = import_node_path18.default.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
12866
|
-
if (import_node_path18.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
12867
|
-
if ((0, import_node_fs14.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
12868
|
-
const bytes = await (0, import_promises10.readFile)(input);
|
|
12869
|
-
const metadata = imageMetadata(bytes);
|
|
12870
|
-
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
12871
|
-
const { width, height, format } = metadata;
|
|
12872
|
-
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
12873
|
-
throw new Error(
|
|
12874
|
-
`${input} is ${width}x${height}; PixelLab's unzoom needs at least ${UNZOOM_MIN_SIDE}x${UNZOOM_MIN_SIDE} to find the pixel grid, so this is probably already at its native size`
|
|
12875
|
-
);
|
|
12876
|
-
}
|
|
12877
|
-
if (width * height > UNZOOM_MAX_AREA) {
|
|
12878
|
-
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
12879
|
-
}
|
|
12880
|
-
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
12881
|
-
await (0, import_promises10.mkdir)(import_node_path18.default.dirname(out), { recursive: true });
|
|
12882
|
-
await (0, import_promises10.writeFile)(out, res.png);
|
|
12883
|
-
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
12884
|
-
}
|
|
12885
|
-
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
12886
|
-
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
12887
|
-
var FONT_COST_GENERATIONS = 15;
|
|
12888
|
-
function fontOutputPaths(out) {
|
|
12889
|
-
const base = import_node_path18.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
12890
|
-
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
12891
|
-
}
|
|
12892
|
-
async function generateFont(client2, opts) {
|
|
12893
|
-
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
12894
|
-
if (!opts.force) {
|
|
12895
|
-
for (const file of [ttf, atlas]) {
|
|
12896
|
-
if ((0, import_node_fs14.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
12897
|
-
}
|
|
12898
|
-
}
|
|
12899
|
-
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
12900
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
12901
|
-
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
12902
|
-
const submitted = await client2.generateFontPro({
|
|
12903
|
-
description: opts.description,
|
|
12904
|
-
weight: opts.weight,
|
|
12905
|
-
glyphPx: opts.glyphPx,
|
|
12906
|
-
seed: opts.seed,
|
|
12907
|
-
fontName: opts.fontName
|
|
12908
|
-
});
|
|
12909
|
-
const jobId = submitted.background_job_id;
|
|
12910
|
-
const started = Date.now();
|
|
12911
|
-
for (; ; ) {
|
|
12912
|
-
const job = await client2.getFontJob(jobId);
|
|
12913
|
-
opts.onProgress?.(job.status);
|
|
12914
|
-
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
12915
|
-
if (job.status === "completed") {
|
|
12916
|
-
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
12917
|
-
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
12918
|
-
}
|
|
12919
|
-
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
12920
|
-
client2.download(job.download_ttf_url),
|
|
12921
|
-
client2.download(job.download_atlas_url)
|
|
12922
|
-
]);
|
|
12923
|
-
await (0, import_promises10.mkdir)(import_node_path18.default.dirname(ttf), { recursive: true });
|
|
12924
|
-
await (0, import_promises10.writeFile)(ttf, ttfBytes);
|
|
12925
|
-
await (0, import_promises10.writeFile)(atlas, atlasBytes);
|
|
12926
|
-
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
12927
|
-
}
|
|
12928
|
-
if (Date.now() - started > timeout) {
|
|
12929
|
-
throw new Error(
|
|
12930
|
-
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
12931
|
-
);
|
|
12932
|
-
}
|
|
12933
|
-
await sleep3(interval);
|
|
12934
|
-
}
|
|
12935
|
-
}
|
|
12936
|
-
var PIXELATE_COST_GENERATIONS = 6;
|
|
12937
|
-
var PIXELATE_MAX_SIDE = 2048;
|
|
12938
|
-
function defaultPixelateOut(input) {
|
|
12939
|
-
const ext = import_node_path18.default.extname(input);
|
|
12940
|
-
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
12941
|
-
}
|
|
12942
|
-
async function pixelateFile(client2, input, opts = {}) {
|
|
12943
|
-
const out = import_node_path18.default.resolve(opts.out ?? defaultPixelateOut(input));
|
|
12944
|
-
if (import_node_path18.default.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
12945
|
-
if ((0, import_node_fs14.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
12946
|
-
const bytes = await (0, import_promises10.readFile)(input);
|
|
12947
|
-
const metadata = imageMetadata(bytes);
|
|
12948
|
-
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
12949
|
-
const { width, height, format } = metadata;
|
|
12950
|
-
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
12951
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
12952
|
-
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
12953
|
-
const submitted = await client2.imageToPixelartProFlash({
|
|
12954
|
-
image: { base64: bytes.toString("base64"), format },
|
|
12955
|
-
description: opts.description,
|
|
12956
|
-
seed: opts.seed
|
|
12957
|
-
});
|
|
12958
|
-
const jobId = submitted.background_job_id;
|
|
12959
|
-
const started = Date.now();
|
|
12960
|
-
for (; ; ) {
|
|
12961
|
-
const job = await client2.getBackgroundJob(jobId);
|
|
12962
|
-
opts.onProgress?.(job.status);
|
|
12963
|
-
if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
|
|
12964
|
-
if (job.status === "completed") {
|
|
12965
|
-
const images = job.last_response?.images;
|
|
12966
|
-
const first = Array.isArray(images) ? images[0] : void 0;
|
|
12967
|
-
if (!first || typeof first.base64 !== "string" || !first.base64) {
|
|
12968
|
-
throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
|
|
12969
|
-
}
|
|
12970
|
-
const png = Buffer.from(first.base64, "base64");
|
|
12971
|
-
const size = imageMetadata(png);
|
|
12972
|
-
await (0, import_promises10.mkdir)(import_node_path18.default.dirname(out), { recursive: true });
|
|
12973
|
-
await (0, import_promises10.writeFile)(out, png);
|
|
12974
|
-
return {
|
|
12975
|
-
jobId,
|
|
12976
|
-
out,
|
|
12977
|
-
original: { width, height },
|
|
12978
|
-
pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
|
|
12979
|
-
imageId: submitted.image_id,
|
|
12980
|
-
usage: job.usage ?? null
|
|
12981
|
-
};
|
|
12982
|
-
}
|
|
12983
|
-
if (Date.now() - started > timeout) {
|
|
12984
|
-
throw new Error(
|
|
12985
|
-
`PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
12986
|
-
);
|
|
12987
|
-
}
|
|
12988
|
-
await sleep3(interval);
|
|
12989
|
-
}
|
|
12990
|
-
}
|
|
12991
|
-
|
|
12992
14094
|
// src/pipeline/quality-regression.ts
|
|
12993
14095
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
12994
14096
|
var import_node_fs15 = require("fs");
|
|
@@ -13800,6 +14902,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
13800
14902
|
billed: entry.billed,
|
|
13801
14903
|
provider: entry.provider,
|
|
13802
14904
|
postprocess: entry.postprocess,
|
|
14905
|
+
...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
|
|
13803
14906
|
retiredAt
|
|
13804
14907
|
};
|
|
13805
14908
|
}
|
|
@@ -13891,6 +14994,9 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
13891
14994
|
billed: generation.billed,
|
|
13892
14995
|
provider: generation.provider,
|
|
13893
14996
|
postprocess: generation.postprocess,
|
|
14997
|
+
// A connected set comes back with the connection it was drawn from, so
|
|
14998
|
+
// the plan compares the restored generation, not the one it replaced.
|
|
14999
|
+
terrainConnect: generation.terrainConnect,
|
|
13894
15000
|
history
|
|
13895
15001
|
});
|
|
13896
15002
|
await saveLock(lockPath, lock);
|
|
@@ -14911,6 +16017,22 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14911
16017
|
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
14912
16018
|
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
14913
16019
|
const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
16020
|
+
const terrainLinks = {};
|
|
16021
|
+
const terrainConnect = {};
|
|
16022
|
+
for (const side of ["lower", "upper"]) {
|
|
16023
|
+
const link = spec.terrainConnect?.[side];
|
|
16024
|
+
if (!link) continue;
|
|
16025
|
+
const other = lock.entries[lockKey(link.spec.styleId, link.spec.assetId)];
|
|
16026
|
+
const objectId = other.objectId;
|
|
16027
|
+
const terrainIds = other.providerMetadata?.[provider.id]?.terrainIds;
|
|
16028
|
+
const terrainId = terrainIds?.[link.terrain];
|
|
16029
|
+
terrainLinks[side] = {
|
|
16030
|
+
objectId,
|
|
16031
|
+
terrain: link.terrain,
|
|
16032
|
+
...typeof terrainId === "string" && terrainId ? { terrainId } : {}
|
|
16033
|
+
};
|
|
16034
|
+
terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
|
|
16035
|
+
}
|
|
14914
16036
|
upsert(lock, key, {
|
|
14915
16037
|
styleId: spec.styleId,
|
|
14916
16038
|
assetId: spec.assetId,
|
|
@@ -14940,6 +16062,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14940
16062
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
14941
16063
|
...spec.revision.description ? { description: spec.revision.description } : {},
|
|
14942
16064
|
...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
|
|
16065
|
+
...spec.revision.convention ? { convention: spec.revision.convention } : {},
|
|
14943
16066
|
...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
|
|
14944
16067
|
...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
|
|
14945
16068
|
...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
|
|
@@ -14950,6 +16073,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14950
16073
|
sourceSha256: mirrorSourceHash(outfitParentEntry),
|
|
14951
16074
|
referenceSha256: spec.character.outfit.reference.sha256
|
|
14952
16075
|
} : null,
|
|
16076
|
+
terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
|
|
14953
16077
|
status: "pending",
|
|
14954
16078
|
jobId: previousJobId ?? null,
|
|
14955
16079
|
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
@@ -14996,6 +16120,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14996
16120
|
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
14997
16121
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
14998
16122
|
...parentOutputs ? { parentOutputs } : {},
|
|
16123
|
+
...spec.terrainConnect ? { terrainLinks } : {},
|
|
14999
16124
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
15000
16125
|
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
15001
16126
|
...previousMetadata ? { previousMetadata } : {},
|
|
@@ -16494,6 +17619,7 @@ var NewAssetSchema = import_zod8.z.object({
|
|
|
16494
17619
|
/** A short description of the foreground, sent as PixelLab's text hint. */
|
|
16495
17620
|
description: import_zod8.z.string().min(1).optional()
|
|
16496
17621
|
}).strict(),
|
|
17622
|
+
import_zod8.z.object({ mode: import_zod8.z.literal("normal-map"), from: import_zod8.z.string().min(1), convention: NormalMapConventionSchema.optional() }).strict(),
|
|
16497
17623
|
import_zod8.z.object({
|
|
16498
17624
|
mode: import_zod8.z.enum(["animate", "animate-pixminimax"]),
|
|
16499
17625
|
from: import_zod8.z.string().min(1),
|
|
@@ -19973,6 +21099,12 @@ async function workspaceStatus(ws, dir) {
|
|
|
19973
21099
|
MapScenePlacementSchema,
|
|
19974
21100
|
MapSceneSchema,
|
|
19975
21101
|
MediaType,
|
|
21102
|
+
NORMAL_MAP_CONVENTIONS,
|
|
21103
|
+
NORMAL_MAP_COST_GENERATIONS,
|
|
21104
|
+
NORMAL_MAP_MAX_FRAMES,
|
|
21105
|
+
NORMAL_MAP_MAX_SIDE,
|
|
21106
|
+
NORMAL_MAP_MIN_SIDE,
|
|
21107
|
+
NormalMapConventionSchema,
|
|
19976
21108
|
OverwriteRefusedError,
|
|
19977
21109
|
PIXELATE_COST_GENERATIONS,
|
|
19978
21110
|
PIXELATE_MAX_SIDE,
|
|
@@ -20000,12 +21132,17 @@ async function workspaceStatus(ws, dir) {
|
|
|
20000
21132
|
ScenarioProvider,
|
|
20001
21133
|
StyleInputSchema,
|
|
20002
21134
|
StyleSchema,
|
|
21135
|
+
TerrainConnectSchema,
|
|
21136
|
+
TerrainLinkSchema,
|
|
21137
|
+
TerrainSideSchema,
|
|
20003
21138
|
UNZOOM_MAX_AREA,
|
|
20004
21139
|
UNZOOM_MIN_SIDE,
|
|
20005
21140
|
UiElementSchema,
|
|
20006
21141
|
UiPieceSchema,
|
|
20007
21142
|
UnsupportedCapabilityError,
|
|
20008
21143
|
UsageError,
|
|
21144
|
+
VFX_ARTISTS,
|
|
21145
|
+
VFX_ARTIST_MAX_SPRITE,
|
|
20009
21146
|
VFX_TOOL,
|
|
20010
21147
|
VfxPartSchema,
|
|
20011
21148
|
WorkspaceProjectSchema,
|
|
@@ -20038,6 +21175,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
20038
21175
|
createProvider,
|
|
20039
21176
|
currentEntryOutputPath,
|
|
20040
21177
|
currentOutputPath,
|
|
21178
|
+
defaultNormalMapOut,
|
|
20041
21179
|
defaultOpenCommand,
|
|
20042
21180
|
defaultPixelateOut,
|
|
20043
21181
|
defaultUnzoomOut,
|
|
@@ -20099,6 +21237,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
20099
21237
|
loadWorkspace,
|
|
20100
21238
|
lockKey,
|
|
20101
21239
|
makeVfx,
|
|
21240
|
+
makeVfxWithArtist,
|
|
20102
21241
|
matchOrphanStyle,
|
|
20103
21242
|
mcpClientFromEnv,
|
|
20104
21243
|
measureBalanceChange,
|
|
@@ -20110,6 +21249,9 @@ async function workspaceStatus(ws, dir) {
|
|
|
20110
21249
|
mergePalettes,
|
|
20111
21250
|
mountSprites,
|
|
20112
21251
|
mountStyle,
|
|
21252
|
+
normalMapFile,
|
|
21253
|
+
normalMapResultPng,
|
|
21254
|
+
normalMapSizeProblem,
|
|
20113
21255
|
normalizeLockOutputPaths,
|
|
20114
21256
|
normalizeRefinementPalette,
|
|
20115
21257
|
normalizeTerrainTiles,
|
|
@@ -20127,6 +21269,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
20127
21269
|
parseMcpBody,
|
|
20128
21270
|
parseProFlashQuote,
|
|
20129
21271
|
parseTerrainDescriptions,
|
|
21272
|
+
parseVfxArtistResult,
|
|
20130
21273
|
parseVfxExport,
|
|
20131
21274
|
parseVfxRecipe,
|
|
20132
21275
|
parseVfxStep,
|
|
@@ -20150,6 +21293,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
20150
21293
|
readHandEditCompanion,
|
|
20151
21294
|
readQualityBaseline,
|
|
20152
21295
|
readSavedVfxRecipe,
|
|
21296
|
+
readVfxConversation,
|
|
20153
21297
|
readVfxRecipe,
|
|
20154
21298
|
recipeDigest,
|
|
20155
21299
|
referencedHashes,
|
|
@@ -20212,6 +21356,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
20212
21356
|
submit,
|
|
20213
21357
|
summarize,
|
|
20214
21358
|
tagAdopted,
|
|
21359
|
+
terrainCost,
|
|
20215
21360
|
terrainTileCount,
|
|
20216
21361
|
tileFeatureOutputCount,
|
|
20217
21362
|
tileVariationCount,
|
|
@@ -20225,6 +21370,13 @@ async function workspaceStatus(ws, dir) {
|
|
|
20225
21370
|
validateWorkspace,
|
|
20226
21371
|
verifyArtifactBundle,
|
|
20227
21372
|
verifyRecipe,
|
|
21373
|
+
vfxArtistCeiling,
|
|
21374
|
+
vfxArtistFiles,
|
|
21375
|
+
vfxArtistFrameFacts,
|
|
21376
|
+
vfxArtistQuote,
|
|
21377
|
+
vfxArtistSprite,
|
|
21378
|
+
vfxAtlasFrames,
|
|
21379
|
+
vfxJobFailure,
|
|
20228
21380
|
vfxOutputPaths,
|
|
20229
21381
|
vfxRecipeSha256,
|
|
20230
21382
|
withArtifactManifest,
|