@openfairygui/functions 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +48 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +16 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +3 -3594
- package/dist/node.cjs +256 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +254 -0
- package/dist/publish-BJ_eelME.js +3267 -0
- package/dist/publish-xFWT9Slz.cjs +3338 -0
- package/dist/restore-BW2xacB3.cjs +936 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-Clk62n0O.js +931 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +44 -1
- package/dist/uam-transaction.d.cts +16 -1
- package/dist/uam-transaction.d.ts +16 -1
- package/dist/uam-transaction.js +44 -1
- package/dist/web.cjs +274 -0
- package/dist/web.d.cts +41 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.js +273 -0
- package/package.json +28 -4
- package/src/adapters/node/plugins.ts +82 -0
- package/src/adapters/node/publish.ts +130 -0
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +159 -0
- package/src/adapters/web/raster.ts +251 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +515 -0
- package/src/atlas/jta.ts +211 -0
- package/src/atlas/packing.ts +767 -0
- package/src/atlas.ts +116 -1221
- package/src/codegen.ts +106 -67
- package/src/index.ts +43 -3
- package/src/node.ts +8 -0
- package/src/plugins/types.ts +56 -0
- package/src/publish/contracts.ts +80 -0
- package/src/publish/external-resources.ts +117 -0
- package/src/publish/options.ts +180 -0
- package/src/publish/package-context.ts +608 -0
- package/src/publish/resource-references.ts +210 -0
- package/src/publish.ts +290 -968
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +164 -0
- package/src/restore.ts +112 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +68 -0
- package/src/utils.ts +28 -0
- package/src/web.ts +11 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import type { Document, ILogger, Package } from '@openfairygui/core';
|
|
2
|
+
import type { AtlasOptions } from '../atlas.js';
|
|
3
|
+
import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from '../max-rects-compat.js';
|
|
4
|
+
import { MaxRectsPackerCompat } from '../max-rects-packer-compat.js';
|
|
5
|
+
import type { AtlasRasterBackend } from '../publish/contracts.js';
|
|
6
|
+
import { parseTextureSetMode, type TextureSetMode } from '../utils.js';
|
|
7
|
+
import {
|
|
8
|
+
getPublishedItemId,
|
|
9
|
+
isFontResource,
|
|
10
|
+
isImageResource,
|
|
11
|
+
resolveImageFileName,
|
|
12
|
+
resolveImagePath,
|
|
13
|
+
type FontResourceExtras,
|
|
14
|
+
type InputItem,
|
|
15
|
+
type PackageResource,
|
|
16
|
+
type PackInputResource,
|
|
17
|
+
type PagedAtlasGroup,
|
|
18
|
+
type StandaloneAtlasGroup,
|
|
19
|
+
} from './inputs.js';
|
|
20
|
+
|
|
21
|
+
interface BranchAtlasGroup {
|
|
22
|
+
branchName: string;
|
|
23
|
+
branchOrdinal: number;
|
|
24
|
+
inputs: InputItem[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function emitAtlasInputs(input: {
|
|
28
|
+
doc: Document;
|
|
29
|
+
pkg: Package;
|
|
30
|
+
allResources: PackageResource[];
|
|
31
|
+
inputs: InputItem[];
|
|
32
|
+
options: AtlasOptions;
|
|
33
|
+
encoder: AtlasRasterBackend | undefined;
|
|
34
|
+
logger: ILogger;
|
|
35
|
+
}): Promise<void> {
|
|
36
|
+
const { doc, pkg, allResources, inputs, options, encoder, logger } = input;
|
|
37
|
+
let totalPageCount = 0;
|
|
38
|
+
let usedDirectOutput = false;
|
|
39
|
+
const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(
|
|
40
|
+
doc,
|
|
41
|
+
inputs,
|
|
42
|
+
options,
|
|
43
|
+
);
|
|
44
|
+
const branchGroups = buildBranchAtlasGroups(doc, autoInputs, options);
|
|
45
|
+
const branchPageOffsets = new Map<number, number>();
|
|
46
|
+
|
|
47
|
+
for (const group of branchGroups) {
|
|
48
|
+
const directOutput =
|
|
49
|
+
fixedPageGroups.length === 0 && standaloneGroups.length === 0
|
|
50
|
+
? resolveDirectImageOutput(group.inputs, options)
|
|
51
|
+
: null;
|
|
52
|
+
if (directOutput) {
|
|
53
|
+
await emitDirectImageOutput(
|
|
54
|
+
doc,
|
|
55
|
+
pkg,
|
|
56
|
+
directOutput,
|
|
57
|
+
encoder,
|
|
58
|
+
options,
|
|
59
|
+
logger,
|
|
60
|
+
group.branchName,
|
|
61
|
+
group.branchOrdinal,
|
|
62
|
+
);
|
|
63
|
+
usedDirectOutput = true;
|
|
64
|
+
totalPageCount += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const pageStart = reserveAutoPageStart(branchPageOffsets, group.branchOrdinal, reservedPageIndexes);
|
|
68
|
+
const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
|
|
69
|
+
branchName: group.branchName,
|
|
70
|
+
branchOrdinal: group.branchOrdinal,
|
|
71
|
+
pageStart,
|
|
72
|
+
fileNameAt: (pageIndex) => resolveAtlasOutputFileName(pkg, pageIndex, group.branchName),
|
|
73
|
+
options,
|
|
74
|
+
encoder,
|
|
75
|
+
logger,
|
|
76
|
+
});
|
|
77
|
+
totalPageCount += emittedPageCount;
|
|
78
|
+
branchPageOffsets.set(group.branchOrdinal, pageStart + emittedPageCount);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const group of fixedPageGroups) {
|
|
82
|
+
const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
|
|
83
|
+
branchName: group.branchName,
|
|
84
|
+
branchOrdinal: group.branchOrdinal,
|
|
85
|
+
pageStart: group.pageIndex,
|
|
86
|
+
forceSinglePage: true,
|
|
87
|
+
fileNameAt: () => resolveAtlasOutputFileName(pkg, group.pageIndex, group.branchName),
|
|
88
|
+
options,
|
|
89
|
+
encoder,
|
|
90
|
+
logger,
|
|
91
|
+
});
|
|
92
|
+
totalPageCount += emittedPageCount;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const standalonePageOffsets = new Map(branchPageOffsets);
|
|
96
|
+
for (const group of fixedPageGroups) {
|
|
97
|
+
const nextPageIndex = group.pageIndex + 1;
|
|
98
|
+
const current = standalonePageOffsets.get(group.branchOrdinal) ?? 0;
|
|
99
|
+
if (nextPageIndex > current) standalonePageOffsets.set(group.branchOrdinal, nextPageIndex);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const group of standaloneGroups) {
|
|
103
|
+
const emittedPageCount = await emitStandaloneAtlasGroup(doc, pkg, group, {
|
|
104
|
+
atlasIndexStart: standalonePageOffsets.get(group.branchOrdinal) ?? 0,
|
|
105
|
+
options,
|
|
106
|
+
encoder,
|
|
107
|
+
logger,
|
|
108
|
+
});
|
|
109
|
+
totalPageCount += emittedPageCount;
|
|
110
|
+
standalonePageOffsets.set(
|
|
111
|
+
group.branchOrdinal,
|
|
112
|
+
(standalonePageOffsets.get(group.branchOrdinal) ?? 0) + emittedPageCount,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (usedDirectOutput) {
|
|
117
|
+
logger.info(`atlas: Direct output for single image package "${pkg.getName()}".`);
|
|
118
|
+
}
|
|
119
|
+
logger.info(
|
|
120
|
+
`atlas: Packed ${inputs.length} images into ${totalPageCount} atlas(es) for package "${pkg.getName()}".`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
function buildBranchAtlasGroups(doc: Document, inputs: InputItem[], options: AtlasOptions): BranchAtlasGroup[] {
|
|
126
|
+
if (!options.separatedAtlasForBranch) {
|
|
127
|
+
return [{ branchName: '', branchOrdinal: 0, inputs }];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const discoveredBranchNames = [
|
|
131
|
+
...new Set(inputs.map((input) => getInputBranchName(input)).filter((branchName) => !!branchName)),
|
|
132
|
+
];
|
|
133
|
+
if (discoveredBranchNames.length === 0) {
|
|
134
|
+
return [{ branchName: '', branchOrdinal: 0, inputs }];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const orderedBranchNames = doc
|
|
138
|
+
.getRoot()
|
|
139
|
+
.listBranches()
|
|
140
|
+
.filter((branchName) => discoveredBranchNames.includes(branchName));
|
|
141
|
+
for (const branchName of discoveredBranchNames) {
|
|
142
|
+
if (!orderedBranchNames.includes(branchName)) orderedBranchNames.push(branchName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const groups = new Map<string, InputItem[]>();
|
|
146
|
+
groups.set('', []);
|
|
147
|
+
for (const branchName of orderedBranchNames) {
|
|
148
|
+
groups.set(branchName, []);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const input of inputs) {
|
|
152
|
+
const branchName = getInputBranchName(input);
|
|
153
|
+
const key = groups.has(branchName) ? branchName : '';
|
|
154
|
+
groups.get(key)!.push(input);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const orderedKeys = [''];
|
|
158
|
+
for (const branchName of orderedBranchNames) {
|
|
159
|
+
if ((groups.get(branchName)?.length ?? 0) > 0) orderedKeys.push(branchName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return orderedKeys
|
|
163
|
+
.filter((branchName) => (groups.get(branchName)?.length ?? 0) > 0)
|
|
164
|
+
.map((branchName, index) => ({
|
|
165
|
+
branchName,
|
|
166
|
+
branchOrdinal: index,
|
|
167
|
+
inputs: groups.get(branchName) ?? [],
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function reserveAutoPageStart(
|
|
172
|
+
branchPageOffsets: Map<number, number>,
|
|
173
|
+
branchOrdinal: number,
|
|
174
|
+
reservedPageIndexes: Set<number>,
|
|
175
|
+
): number {
|
|
176
|
+
let pageIndex = branchPageOffsets.get(branchOrdinal) ?? 0;
|
|
177
|
+
while (branchOrdinal === 0 && reservedPageIndexes.has(pageIndex)) {
|
|
178
|
+
pageIndex += 1;
|
|
179
|
+
}
|
|
180
|
+
return pageIndex;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function emitPagedAtlasGroup(
|
|
184
|
+
doc: Document,
|
|
185
|
+
pkg: Package,
|
|
186
|
+
allResources: PackageResource[],
|
|
187
|
+
inputs: InputItem[],
|
|
188
|
+
context: {
|
|
189
|
+
branchName: string;
|
|
190
|
+
branchOrdinal: number;
|
|
191
|
+
pageStart: number;
|
|
192
|
+
fileNameAt: (pageIndex: number) => string;
|
|
193
|
+
options: AtlasOptions;
|
|
194
|
+
encoder: AtlasRasterBackend | undefined;
|
|
195
|
+
logger: ILogger;
|
|
196
|
+
forceSinglePage?: boolean;
|
|
197
|
+
},
|
|
198
|
+
): Promise<number> {
|
|
199
|
+
if (inputs.length === 0) return 0;
|
|
200
|
+
const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
|
|
201
|
+
assertPackedInputCoverage(pages, inputs.length, `package "${pkg.getName()}"`);
|
|
202
|
+
|
|
203
|
+
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
204
|
+
const page = pages[pageOffset];
|
|
205
|
+
const pageIndex = context.pageStart + pageOffset;
|
|
206
|
+
const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(context.branchOrdinal, pageIndex)}`);
|
|
207
|
+
atlasNode.setIndex(resolveAtlasIndex(context.branchOrdinal, pageIndex));
|
|
208
|
+
atlasNode.setFile(context.fileNameAt(pageIndex));
|
|
209
|
+
atlasNode.setWidth(page.width);
|
|
210
|
+
atlasNode.setHeight(page.height);
|
|
211
|
+
pkg.addAtlas(atlasNode);
|
|
212
|
+
|
|
213
|
+
attachSpritesToAtlas(doc, allResources, inputs, page.outputRects, atlasNode);
|
|
214
|
+
await writeAtlasPageImage(
|
|
215
|
+
pkg,
|
|
216
|
+
inputs,
|
|
217
|
+
page,
|
|
218
|
+
atlasNode.getFile(),
|
|
219
|
+
context.encoder,
|
|
220
|
+
context.options,
|
|
221
|
+
context.logger,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return pages.length;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function emitStandaloneAtlasGroup(
|
|
229
|
+
doc: Document,
|
|
230
|
+
pkg: Package,
|
|
231
|
+
group: StandaloneAtlasGroup,
|
|
232
|
+
context: {
|
|
233
|
+
atlasIndexStart: number;
|
|
234
|
+
options: AtlasOptions;
|
|
235
|
+
encoder: AtlasRasterBackend | undefined;
|
|
236
|
+
logger: ILogger;
|
|
237
|
+
},
|
|
238
|
+
): Promise<number> {
|
|
239
|
+
if (group.inputs.length === 0) return 0;
|
|
240
|
+
const pages = packAtlasPages(
|
|
241
|
+
group.inputs,
|
|
242
|
+
context.options,
|
|
243
|
+
true,
|
|
244
|
+
group.sizeMode === 'npot'
|
|
245
|
+
? { powerOfTwo: false, multipleOfFour: false, square: false }
|
|
246
|
+
: group.sizeMode === 'multipleOf4'
|
|
247
|
+
? { powerOfTwo: false, multipleOfFour: true, square: false }
|
|
248
|
+
: undefined,
|
|
249
|
+
);
|
|
250
|
+
assertPackedInputCoverage(pages, group.inputs.length, `standalone texture in package "${pkg.getName()}"`);
|
|
251
|
+
|
|
252
|
+
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
253
|
+
const page = pages[pageOffset];
|
|
254
|
+
const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
|
|
255
|
+
const atlasFileName = pages.length <= 1 ? baseFileName : insertFileNameSuffix(baseFileName, `_${pageOffset}`);
|
|
256
|
+
const atlasIndex = context.atlasIndexStart + pageOffset;
|
|
257
|
+
const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, atlasIndex)}`);
|
|
258
|
+
atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, atlasIndex));
|
|
259
|
+
atlasNode.setFile(atlasFileName);
|
|
260
|
+
const standaloneSize = resolveStandaloneAtlasSize(page.width, page.height, group.sizeMode, context.options);
|
|
261
|
+
atlasNode.setWidth(standaloneSize.width);
|
|
262
|
+
atlasNode.setHeight(standaloneSize.height);
|
|
263
|
+
pkg.addAtlas(atlasNode);
|
|
264
|
+
|
|
265
|
+
attachSpritesToAtlas(doc, [], group.inputs, page.outputRects, atlasNode);
|
|
266
|
+
await writeAtlasPageImage(
|
|
267
|
+
pkg,
|
|
268
|
+
group.inputs,
|
|
269
|
+
{ ...page, width: standaloneSize.width, height: standaloneSize.height },
|
|
270
|
+
atlasFileName,
|
|
271
|
+
context.encoder,
|
|
272
|
+
context.options,
|
|
273
|
+
context.logger,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return pages.length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function packAtlasPages(
|
|
281
|
+
inputs: InputItem[],
|
|
282
|
+
options: AtlasOptions,
|
|
283
|
+
forceSinglePage: boolean,
|
|
284
|
+
sizeOverrides?: {
|
|
285
|
+
powerOfTwo: boolean;
|
|
286
|
+
multipleOfFour: boolean;
|
|
287
|
+
square: boolean;
|
|
288
|
+
},
|
|
289
|
+
): ReturnType<MaxRectsPackerCompat['pack']> {
|
|
290
|
+
const hasDuplicatePadding = inputs.some((input) => {
|
|
291
|
+
return isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
|
|
292
|
+
});
|
|
293
|
+
const packer = new MaxRectsPackerCompat({
|
|
294
|
+
pot: sizeOverrides?.powerOfTwo ?? options.powerOfTwo,
|
|
295
|
+
mof: sizeOverrides?.multipleOfFour ?? !options.powerOfTwo,
|
|
296
|
+
padding: options.padding,
|
|
297
|
+
rotation: options.allowRotation,
|
|
298
|
+
minWidth: 16,
|
|
299
|
+
minHeight: 16,
|
|
300
|
+
maxWidth: options.maxSize,
|
|
301
|
+
maxHeight: options.maxSize,
|
|
302
|
+
square: sizeOverrides?.square ?? options.square,
|
|
303
|
+
fast: options.fast,
|
|
304
|
+
edgePadding: false,
|
|
305
|
+
duplicatePadding: hasDuplicatePadding,
|
|
306
|
+
multiPage: forceSinglePage ? false : options.multiPage,
|
|
307
|
+
preserveInputOrderOnTie: options.preserveInputOrderOnTie,
|
|
308
|
+
});
|
|
309
|
+
return packer.pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function assertPackedInputCoverage(
|
|
313
|
+
pages: Array<{ outputRects: Array<{ index: number }> }>,
|
|
314
|
+
inputCount: number,
|
|
315
|
+
label: string,
|
|
316
|
+
): void {
|
|
317
|
+
const packedIndexes = new Set<number>();
|
|
318
|
+
for (const page of pages) {
|
|
319
|
+
for (const outputRect of page.outputRects) packedIndexes.add(outputRect.index);
|
|
320
|
+
}
|
|
321
|
+
const hasEveryInput = Array.from({ length: inputCount }, (_, index) => packedIndexes.has(index)).every(Boolean);
|
|
322
|
+
if (packedIndexes.size !== inputCount || !hasEveryInput) {
|
|
323
|
+
throw new Error(`atlas: Could not pack every input for ${label}.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function attachSpritesToAtlas(
|
|
328
|
+
doc: Document,
|
|
329
|
+
allResources: PackageResource[],
|
|
330
|
+
inputs: InputItem[],
|
|
331
|
+
outputRects: Array<{ index: number; x: number; y: number; width: number; height: number; rotated: boolean }>,
|
|
332
|
+
atlasNode: ReturnType<Document['createAtlas']>,
|
|
333
|
+
): void {
|
|
334
|
+
for (const packedRect of outputRects) {
|
|
335
|
+
const input = inputs[packedRect.index];
|
|
336
|
+
if (!input) continue;
|
|
337
|
+
const packedSize = resolvePackedRectSize(input, packedRect.width, packedRect.height, packedRect.rotated);
|
|
338
|
+
const sprite = doc.createSprite();
|
|
339
|
+
sprite.setItemId(input.id);
|
|
340
|
+
sprite.setRectX(packedRect.x);
|
|
341
|
+
sprite.setRectY(packedRect.y);
|
|
342
|
+
sprite.setRectWidth(packedSize.width);
|
|
343
|
+
sprite.setRectHeight(packedSize.height);
|
|
344
|
+
sprite.setRotated(packedRect.rotated);
|
|
345
|
+
sprite.setOffsetX(input.offsetX);
|
|
346
|
+
sprite.setOffsetY(input.offsetY);
|
|
347
|
+
sprite.setOriginalWidth(input.originalWidth);
|
|
348
|
+
sprite.setOriginalHeight(input.originalHeight);
|
|
349
|
+
sprite.setAtlas(atlasNode);
|
|
350
|
+
atlasNode.addSprite(sprite);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const resource of allResources) {
|
|
354
|
+
if (!isFontResource(resource)) continue;
|
|
355
|
+
const extras = resource.getExtras() as FontResourceExtras;
|
|
356
|
+
const alias = extras?._fontSpriteAlias;
|
|
357
|
+
if (!alias) continue;
|
|
358
|
+
const imageSprite = outputRects.find((result) => inputs[result.index]?.id === alias.textureId);
|
|
359
|
+
if (!imageSprite) continue;
|
|
360
|
+
const imageInput = inputs[imageSprite.index];
|
|
361
|
+
const fontSprite = doc.createSprite();
|
|
362
|
+
fontSprite.setItemId(alias.fontId);
|
|
363
|
+
fontSprite.setRectX(imageSprite.x);
|
|
364
|
+
fontSprite.setRectY(imageSprite.y);
|
|
365
|
+
fontSprite.setRectWidth(imageSprite.width);
|
|
366
|
+
fontSprite.setRectHeight(imageSprite.height);
|
|
367
|
+
fontSprite.setRotated(imageSprite.rotated);
|
|
368
|
+
if (imageInput) {
|
|
369
|
+
fontSprite.setOffsetX(imageInput.offsetX);
|
|
370
|
+
fontSprite.setOffsetY(imageInput.offsetY);
|
|
371
|
+
fontSprite.setOriginalWidth(imageInput.originalWidth);
|
|
372
|
+
fontSprite.setOriginalHeight(imageInput.originalHeight);
|
|
373
|
+
}
|
|
374
|
+
fontSprite.setAtlas(atlasNode);
|
|
375
|
+
atlasNode.addSprite(fontSprite);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function writeAtlasPageImage(
|
|
380
|
+
pkg: Package,
|
|
381
|
+
inputs: InputItem[],
|
|
382
|
+
page: {
|
|
383
|
+
width: number;
|
|
384
|
+
height: number;
|
|
385
|
+
outputRects: Array<{ index: number; x: number; y: number; width: number; height: number; rotated: boolean }>;
|
|
386
|
+
},
|
|
387
|
+
atlasFileName: string,
|
|
388
|
+
encoder: AtlasRasterBackend | undefined,
|
|
389
|
+
options: AtlasOptions,
|
|
390
|
+
logger: ILogger,
|
|
391
|
+
): Promise<void> {
|
|
392
|
+
if (!encoder || !options.outputPath) return;
|
|
393
|
+
if (options.mkdir) {
|
|
394
|
+
await options.mkdir(options.outputPath);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const compositeInputs: Array<{ input: Uint8Array; left: number; top: number }> = [];
|
|
398
|
+
for (const packedRect of page.outputRects) {
|
|
399
|
+
const input = inputs[packedRect.index];
|
|
400
|
+
if (!input) continue;
|
|
401
|
+
if (packedRect.width <= 0 || packedRect.height <= 0 || input.width <= 0 || input.height <= 0) continue;
|
|
402
|
+
try {
|
|
403
|
+
let imageBuffer: Uint8Array;
|
|
404
|
+
if (input.trimBuffer) {
|
|
405
|
+
imageBuffer = input.trimBuffer;
|
|
406
|
+
if (imageBuffer.length === 0) continue;
|
|
407
|
+
} else if (input.rasterizedBuffer) {
|
|
408
|
+
imageBuffer = input.rasterizedBuffer;
|
|
409
|
+
} else {
|
|
410
|
+
if (!isImageResource(input.resource)) {
|
|
411
|
+
const message = `atlas: Non-image input "${input.id}" is missing inline buffer.`;
|
|
412
|
+
if (options.strictOutput) throw new Error(message);
|
|
413
|
+
logger.warn(`${message} Skipping compositing.`);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const filePath = resolveImagePath(input.resource, pkg, options.basePath!);
|
|
417
|
+
imageBuffer = await encoder(filePath).toBuffer();
|
|
418
|
+
}
|
|
419
|
+
if (packedRect.rotated) imageBuffer = await encoder(imageBuffer).rotate(270).toBuffer();
|
|
420
|
+
compositeInputs.push({
|
|
421
|
+
input: imageBuffer,
|
|
422
|
+
left: packedRect.x,
|
|
423
|
+
top: packedRect.y,
|
|
424
|
+
});
|
|
425
|
+
} catch {
|
|
426
|
+
const message = `atlas: Could not read image "${input.id}" for compositing.`;
|
|
427
|
+
if (options.strictOutput) throw new Error(message);
|
|
428
|
+
logger.warn(message);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
433
|
+
await encoder({
|
|
434
|
+
create: {
|
|
435
|
+
width: page.width,
|
|
436
|
+
height: page.height,
|
|
437
|
+
channels: 4 as const,
|
|
438
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
439
|
+
},
|
|
440
|
+
})
|
|
441
|
+
.composite(compositeInputs)
|
|
442
|
+
.toFile(outputFile);
|
|
443
|
+
|
|
444
|
+
logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function inputToCompatRect(input: InputItem, index: number): CompatNodeRect {
|
|
448
|
+
const duplicatePadding = isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
|
|
449
|
+
return {
|
|
450
|
+
x: 0,
|
|
451
|
+
y: 0,
|
|
452
|
+
width: input.width,
|
|
453
|
+
height: input.height,
|
|
454
|
+
rotated: false,
|
|
455
|
+
index,
|
|
456
|
+
subIndex: -1,
|
|
457
|
+
flags: duplicatePadding ? COMPAT_NODE_RECT_FLAGS.DUPLICATE_PADDING : 0,
|
|
458
|
+
score1: 0,
|
|
459
|
+
score2: 0,
|
|
460
|
+
sourceKind: input.sourceKind,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolvePackedRectSize(
|
|
465
|
+
input: InputItem,
|
|
466
|
+
width: number,
|
|
467
|
+
height: number,
|
|
468
|
+
rectRotated: boolean,
|
|
469
|
+
): { width: number; height: number } {
|
|
470
|
+
if (!rectRotated) return { width, height };
|
|
471
|
+
return {
|
|
472
|
+
width: input.height,
|
|
473
|
+
height: input.width,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function resolveDirectImageOutput(inputs: InputItem[], options: AtlasOptions): InputItem | null {
|
|
478
|
+
if (!options.directSingleImageOutput || options.extractAlpha) return null;
|
|
479
|
+
if (inputs.length !== 1) return null;
|
|
480
|
+
const [input] = inputs;
|
|
481
|
+
if (!input || input.sourceKind !== 'image' || !isImageResource(input.resource)) return null;
|
|
482
|
+
if (input.resource.getDuplicatePadding?.() === true) return null;
|
|
483
|
+
if (input.width !== input.originalWidth || input.height !== input.originalHeight) return null;
|
|
484
|
+
const fileName = resolveImageFileName(input.resource).toLowerCase();
|
|
485
|
+
if (!fileName.endsWith('.png')) return null;
|
|
486
|
+
return input;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveDirectOutputAtlasSize(
|
|
490
|
+
width: number,
|
|
491
|
+
height: number,
|
|
492
|
+
options: AtlasOptions,
|
|
493
|
+
): { width: number; height: number } {
|
|
494
|
+
let resolvedWidth = width;
|
|
495
|
+
let resolvedHeight = height;
|
|
496
|
+
if (options.square) {
|
|
497
|
+
const side = Math.max(resolvedWidth, resolvedHeight);
|
|
498
|
+
resolvedWidth = side;
|
|
499
|
+
resolvedHeight = side;
|
|
500
|
+
}
|
|
501
|
+
if (options.powerOfTwo) {
|
|
502
|
+
resolvedWidth = nextPow2(resolvedWidth);
|
|
503
|
+
resolvedHeight = nextPow2(resolvedHeight);
|
|
504
|
+
}
|
|
505
|
+
return { width: resolvedWidth, height: resolvedHeight };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function emitDirectImageOutput(
|
|
509
|
+
doc: Document,
|
|
510
|
+
pkg: Package,
|
|
511
|
+
input: InputItem,
|
|
512
|
+
encoder: AtlasRasterBackend | undefined,
|
|
513
|
+
options: AtlasOptions,
|
|
514
|
+
logger: ILogger,
|
|
515
|
+
branchName: string = '',
|
|
516
|
+
branchOrdinal: number = 0,
|
|
517
|
+
): Promise<void> {
|
|
518
|
+
const atlasFileName = resolveAtlasOutputFileName(pkg, 0, branchName);
|
|
519
|
+
const atlasSize = resolveDirectOutputAtlasSize(input.originalWidth, input.originalHeight, options);
|
|
520
|
+
const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(branchOrdinal, 0)}`);
|
|
521
|
+
atlasNode.setIndex(resolveAtlasIndex(branchOrdinal, 0));
|
|
522
|
+
atlasNode.setFile(atlasFileName);
|
|
523
|
+
atlasNode.setWidth(atlasSize.width);
|
|
524
|
+
atlasNode.setHeight(atlasSize.height);
|
|
525
|
+
pkg.addAtlas(atlasNode);
|
|
526
|
+
|
|
527
|
+
const sprite = doc.createSprite();
|
|
528
|
+
sprite.setItemId(input.id);
|
|
529
|
+
sprite.setRectX(0);
|
|
530
|
+
sprite.setRectY(0);
|
|
531
|
+
sprite.setRectWidth(input.originalWidth);
|
|
532
|
+
sprite.setRectHeight(input.originalHeight);
|
|
533
|
+
sprite.setRotated(false);
|
|
534
|
+
sprite.setOffsetX(0);
|
|
535
|
+
sprite.setOffsetY(0);
|
|
536
|
+
sprite.setOriginalWidth(input.originalWidth);
|
|
537
|
+
sprite.setOriginalHeight(input.originalHeight);
|
|
538
|
+
sprite.setAtlas(atlasNode);
|
|
539
|
+
atlasNode.addSprite(sprite);
|
|
540
|
+
|
|
541
|
+
if (!encoder || !options.outputPath || !isImageResource(input.resource) || !options.basePath) return;
|
|
542
|
+
if (options.mkdir) {
|
|
543
|
+
await options.mkdir(options.outputPath);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
547
|
+
const filePath = resolveImagePath(input.resource, pkg, options.basePath);
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
if (atlasSize.width === input.originalWidth && atlasSize.height === input.originalHeight) {
|
|
551
|
+
await encoder(filePath).png().toFile(outputFile);
|
|
552
|
+
} else {
|
|
553
|
+
const imageBuffer = await encoder(filePath).png().toBuffer();
|
|
554
|
+
await encoder({
|
|
555
|
+
create: {
|
|
556
|
+
width: atlasSize.width,
|
|
557
|
+
height: atlasSize.height,
|
|
558
|
+
channels: 4 as const,
|
|
559
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
.composite([{ input: imageBuffer, left: 0, top: 0 }])
|
|
563
|
+
.png()
|
|
564
|
+
.toFile(outputFile);
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
const message = `atlas: Could not write direct-output atlas "${atlasFileName}".`;
|
|
568
|
+
if (options.strictOutput) throw new Error(message);
|
|
569
|
+
logger.warn(message);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function getInputBranchName(input: InputItem): string {
|
|
574
|
+
return (input.resource as { getBranch?(): string }).getBranch?.() ?? '';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function resolveAtlasIndex(branchOrdinal: number, pageIndex: number): number {
|
|
578
|
+
if (branchOrdinal <= 0) return pageIndex;
|
|
579
|
+
return branchOrdinal * 100 + pageIndex;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function resolveAtlasOutputFileName(pkg: Package, pageIndex: number, branchName: string): string {
|
|
583
|
+
const suffix = branchName ? `_${branchName}` : '';
|
|
584
|
+
return `${pkg.getPublishName() || pkg.getName()}_atlas${pageIndex}${suffix}.png`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function resolveStandaloneAtlasOutputFileName(pkg: Package, resource: PackInputResource, branchName: string): string {
|
|
588
|
+
const baseName = `${pkg.getPublishName() || pkg.getName()}_atlas_${getPublishedItemId(resource)}`;
|
|
589
|
+
const suffix = branchName ? `_${branchName}` : '';
|
|
590
|
+
if (isImageResource(resource)) {
|
|
591
|
+
const ext = extname(resolveImageFileName(resource)) || '.png';
|
|
592
|
+
return `${baseName}${suffix}${ext}`;
|
|
593
|
+
}
|
|
594
|
+
return `${baseName}${suffix}.png`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function resolveStandaloneAtlasSize(
|
|
598
|
+
width: number,
|
|
599
|
+
height: number,
|
|
600
|
+
sizeMode: StandaloneAtlasGroup['sizeMode'],
|
|
601
|
+
options: AtlasOptions,
|
|
602
|
+
): { width: number; height: number } {
|
|
603
|
+
if (sizeMode === 'npot') {
|
|
604
|
+
return { width, height };
|
|
605
|
+
}
|
|
606
|
+
if (sizeMode === 'multipleOf4') {
|
|
607
|
+
return {
|
|
608
|
+
width: roundUpToMultiple(width, 4),
|
|
609
|
+
height: roundUpToMultiple(height, 4),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return resolveDirectOutputAtlasSize(width, height, options);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function extname(fileName: string): string {
|
|
616
|
+
const normalized = fileName.replace(/\\/g, '/');
|
|
617
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
618
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
619
|
+
if (lastDot <= lastSlash) return '';
|
|
620
|
+
return normalized.slice(lastDot);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function insertFileNameSuffix(fileName: string, suffix: string): string {
|
|
624
|
+
const extension = extname(fileName);
|
|
625
|
+
if (!extension) return `${fileName}${suffix}`;
|
|
626
|
+
return `${fileName.slice(0, -extension.length)}${suffix}${extension}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function nextPow2(value: number): number {
|
|
630
|
+
if (value <= 1) return 1;
|
|
631
|
+
return 2 ** Math.ceil(Math.log2(value));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function roundUpToMultiple(value: number, base: number): number {
|
|
635
|
+
if (value <= 0) return 0;
|
|
636
|
+
return Math.ceil(value / base) * base;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function sortResourcesByOrder(
|
|
640
|
+
resources: PackageResource[],
|
|
641
|
+
orderMap: Map<string, number>,
|
|
642
|
+
inputOrderMap: Map<string, number>,
|
|
643
|
+
): PackageResource[] {
|
|
644
|
+
const ordered = [...resources];
|
|
645
|
+
ordered.sort((left, right) => {
|
|
646
|
+
const leftId = left.getId();
|
|
647
|
+
const rightId = right.getId();
|
|
648
|
+
const leftOrder =
|
|
649
|
+
leftId && orderMap.has(leftId)
|
|
650
|
+
? (orderMap.get(leftId) ?? Number.MAX_SAFE_INTEGER)
|
|
651
|
+
: Number.MAX_SAFE_INTEGER;
|
|
652
|
+
const rightOrder =
|
|
653
|
+
rightId && orderMap.has(rightId)
|
|
654
|
+
? (orderMap.get(rightId) ?? Number.MAX_SAFE_INTEGER)
|
|
655
|
+
: Number.MAX_SAFE_INTEGER;
|
|
656
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
657
|
+
const leftInputOrder =
|
|
658
|
+
leftId && inputOrderMap.has(leftId)
|
|
659
|
+
? (inputOrderMap.get(leftId) ?? Number.MAX_SAFE_INTEGER)
|
|
660
|
+
: Number.MAX_SAFE_INTEGER;
|
|
661
|
+
const rightInputOrder =
|
|
662
|
+
rightId && inputOrderMap.has(rightId)
|
|
663
|
+
? (inputOrderMap.get(rightId) ?? Number.MAX_SAFE_INTEGER)
|
|
664
|
+
: Number.MAX_SAFE_INTEGER;
|
|
665
|
+
if (leftInputOrder !== rightInputOrder) return leftInputOrder - rightInputOrder;
|
|
666
|
+
return (leftId ?? '').localeCompare(rightId ?? '');
|
|
667
|
+
});
|
|
668
|
+
return ordered;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function getResourceTextureSetMode(resource: PackInputResource): TextureSetMode {
|
|
672
|
+
if (isImageResource(resource)) {
|
|
673
|
+
return parseTextureSetMode(resource.getTextureSetMode?.());
|
|
674
|
+
}
|
|
675
|
+
return parseTextureSetMode(resource.getTextureSetMode?.());
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function groupStandaloneInputs(
|
|
679
|
+
doc: Document,
|
|
680
|
+
inputs: InputItem[],
|
|
681
|
+
options: AtlasOptions,
|
|
682
|
+
): {
|
|
683
|
+
autoInputs: InputItem[];
|
|
684
|
+
fixedPageGroups: PagedAtlasGroup[];
|
|
685
|
+
standaloneGroups: StandaloneAtlasGroup[];
|
|
686
|
+
reservedPageIndexes: Set<number>;
|
|
687
|
+
} {
|
|
688
|
+
const autoInputs: InputItem[] = [];
|
|
689
|
+
const fixedInputsByPage = new Map<string, PagedAtlasGroup>();
|
|
690
|
+
const standaloneGroups = new Map<string, StandaloneAtlasGroup>();
|
|
691
|
+
const reservedPageIndexes = new Set<number>();
|
|
692
|
+
const discoveredBranchNames = [
|
|
693
|
+
...new Set(inputs.map((input) => getInputBranchName(input)).filter((branchName) => !!branchName)),
|
|
694
|
+
];
|
|
695
|
+
const orderedBranchNames = doc
|
|
696
|
+
.getRoot()
|
|
697
|
+
.listBranches()
|
|
698
|
+
.filter((branchName) => discoveredBranchNames.includes(branchName));
|
|
699
|
+
for (const branchName of discoveredBranchNames) {
|
|
700
|
+
if (!orderedBranchNames.includes(branchName)) orderedBranchNames.push(branchName);
|
|
701
|
+
}
|
|
702
|
+
const branchOrdinalByName = new Map<string, number>();
|
|
703
|
+
branchOrdinalByName.set('', 0);
|
|
704
|
+
if (options.separatedAtlasForBranch) {
|
|
705
|
+
let ordinal = 1;
|
|
706
|
+
for (const branchName of orderedBranchNames) {
|
|
707
|
+
branchOrdinalByName.set(branchName, ordinal++);
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
for (const branchName of orderedBranchNames) {
|
|
711
|
+
branchOrdinalByName.set(branchName, 0);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const input of inputs) {
|
|
716
|
+
const branchName = getInputBranchName(input);
|
|
717
|
+
const branchOrdinal = branchOrdinalByName.get(branchName) ?? 0;
|
|
718
|
+
const mode = getResourceTextureSetMode(input.resource);
|
|
719
|
+
if (mode.kind === 'standalone') {
|
|
720
|
+
const resourceId = getPublishedItemId(input.resource);
|
|
721
|
+
const key = `${branchName}\u0000${resourceId}`;
|
|
722
|
+
const existing = standaloneGroups.get(key);
|
|
723
|
+
if (existing) {
|
|
724
|
+
existing.inputs.push(input);
|
|
725
|
+
} else {
|
|
726
|
+
standaloneGroups.set(key, {
|
|
727
|
+
resource: input.resource,
|
|
728
|
+
branchName,
|
|
729
|
+
branchOrdinal,
|
|
730
|
+
sizeMode: mode.sizeMode,
|
|
731
|
+
inputs: [input],
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (mode.kind === 'page') {
|
|
737
|
+
reservedPageIndexes.add(mode.pageIndex);
|
|
738
|
+
const key = `${branchName}\u0000${mode.pageIndex}`;
|
|
739
|
+
const existing = fixedInputsByPage.get(key);
|
|
740
|
+
if (existing) {
|
|
741
|
+
existing.inputs.push(input);
|
|
742
|
+
} else {
|
|
743
|
+
fixedInputsByPage.set(key, {
|
|
744
|
+
pageIndex: mode.pageIndex,
|
|
745
|
+
branchName,
|
|
746
|
+
branchOrdinal,
|
|
747
|
+
inputs: [input],
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
autoInputs.push(input);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
autoInputs,
|
|
757
|
+
fixedPageGroups: [...fixedInputsByPage.values()].sort(
|
|
758
|
+
(left, right) => left.branchOrdinal - right.branchOrdinal || left.pageIndex - right.pageIndex,
|
|
759
|
+
),
|
|
760
|
+
standaloneGroups: [...standaloneGroups.values()].sort(
|
|
761
|
+
(left, right) =>
|
|
762
|
+
left.branchOrdinal - right.branchOrdinal ||
|
|
763
|
+
getPublishedItemId(left.resource).localeCompare(getPublishedItemId(right.resource)),
|
|
764
|
+
),
|
|
765
|
+
reservedPageIndexes,
|
|
766
|
+
};
|
|
767
|
+
}
|