@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,936 @@
|
|
|
1
|
+
let _openfairygui_core = require("@openfairygui/core");
|
|
2
|
+
//#region src/restore-internals/output-transaction.ts
|
|
3
|
+
function trimTrailingSlashes(value) {
|
|
4
|
+
return value.replace(/[/\\]+$/, "");
|
|
5
|
+
}
|
|
6
|
+
function normalizeComparablePath(value) {
|
|
7
|
+
const normalized = trimTrailingSlashes(value).replace(/\\/g, "/");
|
|
8
|
+
const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
|
|
9
|
+
const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
|
|
10
|
+
const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
|
|
11
|
+
const hasRoot = driveMatch ? true : remainder.startsWith("/");
|
|
12
|
+
const rawSegments = remainder.split("/").filter((segment) => segment.length > 0);
|
|
13
|
+
const segments = [];
|
|
14
|
+
for (const segment of rawSegments) {
|
|
15
|
+
if (segment === ".") continue;
|
|
16
|
+
if (segment === "..") {
|
|
17
|
+
if (segments.length > 0 && segments[segments.length - 1] !== "..") segments.pop();
|
|
18
|
+
else if (!hasRoot) segments.push("..");
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
segments.push(segment);
|
|
22
|
+
}
|
|
23
|
+
const joined = segments.join("/");
|
|
24
|
+
return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function isPathWithin(root, candidate) {
|
|
27
|
+
const normalizedRoot = normalizeComparablePath(root);
|
|
28
|
+
return normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
|
|
29
|
+
}
|
|
30
|
+
function basename(filePath) {
|
|
31
|
+
return trimTrailingSlashes(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
|
|
32
|
+
}
|
|
33
|
+
function normalizeRestoreOutputDir(output) {
|
|
34
|
+
const normalized = trimTrailingSlashes(output);
|
|
35
|
+
const name = basename(normalized);
|
|
36
|
+
if (!normalized || /\.fairy$/i.test(normalized) || !name || name === "." || name === ".." || /^[a-z]:$/iu.test(name)) throw new Error("restore: Output must be a non-root project directory, not a .fairy file.");
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
function resolveOutputProjectPath(outputDir, fs) {
|
|
40
|
+
return fs.join(outputDir, `${basename(outputDir)}.fairy`);
|
|
41
|
+
}
|
|
42
|
+
async function resolvePathForContainment(filePath, fs) {
|
|
43
|
+
const missingSegments = [];
|
|
44
|
+
let existingPath = filePath;
|
|
45
|
+
while (!await fs.exists(existingPath)) {
|
|
46
|
+
const parentPath = fs.dirname(existingPath);
|
|
47
|
+
if (!parentPath || parentPath === existingPath) return Promise.resolve(fs.resolvePath(filePath));
|
|
48
|
+
missingSegments.unshift(basename(existingPath));
|
|
49
|
+
existingPath = parentPath;
|
|
50
|
+
}
|
|
51
|
+
const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
|
|
52
|
+
return missingSegments.reduce((resolvedPath, segment) => fs.join(resolvedPath, segment), resolvedExistingPath);
|
|
53
|
+
}
|
|
54
|
+
async function assertRestoreOutputDir(inputDir, outputDir, fs, force) {
|
|
55
|
+
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([resolvePathForContainment(inputDir, fs), resolvePathForContainment(outputDir, fs)]);
|
|
56
|
+
const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
|
|
57
|
+
const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
|
|
58
|
+
if (normalizedInputDir === normalizedOutputDir || isPathWithin(normalizedInputDir, normalizedOutputDir) || isPathWithin(normalizedOutputDir, normalizedInputDir)) throw new Error("Restore output directory must be independent from the published input directory.");
|
|
59
|
+
if (!await fs.exists(outputDir)) return;
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = await fs.readdir(outputDir);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
65
|
+
}
|
|
66
|
+
if (entries.length === 0) return;
|
|
67
|
+
if (!force) throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
68
|
+
}
|
|
69
|
+
async function createRestoreStagingDir(outputDir, fs) {
|
|
70
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
71
|
+
await fs.mkdir(parentDir);
|
|
72
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
73
|
+
const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${(0, _openfairygui_core.generateId)()}`);
|
|
74
|
+
if (await fs.exists(stagingDir)) continue;
|
|
75
|
+
await fs.mkdir(stagingDir);
|
|
76
|
+
return stagingDir;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`restore: Could not allocate a staging directory beside ${outputDir}.`);
|
|
79
|
+
}
|
|
80
|
+
async function commitRestoreOutput(stagingDir, outputDir, fs) {
|
|
81
|
+
if (!await fs.exists(outputDir)) {
|
|
82
|
+
await fs.rename(stagingDir, outputDir);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
86
|
+
let backupDir = "";
|
|
87
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
88
|
+
const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${(0, _openfairygui_core.generateId)()}`);
|
|
89
|
+
if (!await fs.exists(candidate)) {
|
|
90
|
+
backupDir = candidate;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!backupDir) throw new Error(`restore: Could not allocate a backup directory beside ${outputDir}.`);
|
|
95
|
+
await fs.rename(outputDir, backupDir);
|
|
96
|
+
try {
|
|
97
|
+
await fs.rename(stagingDir, outputDir);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await fs.rename(backupDir, outputDir);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await fs.rm(backupDir, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
force: true
|
|
106
|
+
});
|
|
107
|
+
return null;
|
|
108
|
+
} catch {
|
|
109
|
+
return `restore: Previous output retained at ${backupDir}; remove it after checking the restored project.`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/restore-internals/font.ts
|
|
114
|
+
function resourceFileName$1(resource) {
|
|
115
|
+
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || "";
|
|
116
|
+
}
|
|
117
|
+
function stripExtension$1(fileName) {
|
|
118
|
+
return fileName.split(/[\\/]/).pop()?.replace(/\.[^.]+$/u, "") ?? "";
|
|
119
|
+
}
|
|
120
|
+
function fontGlyphCharId(glyph) {
|
|
121
|
+
const charId = glyph.getCharId();
|
|
122
|
+
if (charId > 0) return charId;
|
|
123
|
+
const char = glyph.getChar();
|
|
124
|
+
return char ? char.codePointAt(0) ?? 0 : 0;
|
|
125
|
+
}
|
|
126
|
+
function serializeTtfFontHeader(pkg, resource, glyphs) {
|
|
127
|
+
const face = stripExtension$1(resourceFileName$1(resource)) || resource.getName?.() || "Font";
|
|
128
|
+
const lineHeight = resource.getLineHeight?.() ?? 0;
|
|
129
|
+
const fontSize = resource.getFontSize?.() ?? lineHeight;
|
|
130
|
+
const textureId = resource.getTextureId?.() ?? "";
|
|
131
|
+
const textureResource = textureId ? pkg.getResourceById(textureId) : null;
|
|
132
|
+
const textureName = textureResource ? resourceFileName$1(textureResource) : `${face}_atlas.png`;
|
|
133
|
+
const scaleW = textureResource?.getWidth?.() ?? 256;
|
|
134
|
+
const scaleH = textureResource?.getHeight?.() ?? 256;
|
|
135
|
+
const base = Math.max(Math.min(fontSize, lineHeight) - 6, 0);
|
|
136
|
+
return [
|
|
137
|
+
`info face="${face}" size=${fontSize} bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0`,
|
|
138
|
+
`common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=1 packed=0 alphaChnl=${resource.getTint?.() ? 1 : 0} redChnl=0 greenChnl=0 blueChnl=0`,
|
|
139
|
+
`page id=0 file="${textureName}"`,
|
|
140
|
+
`chars count=${glyphs.length}`
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
function serializeFont(pkg, resource, glyphs) {
|
|
144
|
+
const isTtf = resource.getTtf?.() === true;
|
|
145
|
+
const lines = isTtf ? serializeTtfFontHeader(pkg, resource, glyphs) : ["info creator=UIBuilder", `common lineHeight=${resource.getLineHeight?.() ?? 0}`];
|
|
146
|
+
for (const glyph of glyphs) {
|
|
147
|
+
const charId = fontGlyphCharId(glyph);
|
|
148
|
+
if (isTtf) lines.push(`char id=${charId} x=${glyph.getX()} y=${glyph.getY()} width=${glyph.getWidth()} height=${glyph.getHeight()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()} page=0 chnl=${glyph.getChannel()}`);
|
|
149
|
+
else lines.push(`char id=${charId} img=${glyph.getImg()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()}`);
|
|
150
|
+
}
|
|
151
|
+
return `${lines.join("\n")}\n`;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/restore-internals/movie-clip.ts
|
|
155
|
+
const JTA_FILE_MARK = "yytou";
|
|
156
|
+
const JTA_VERSION = 102;
|
|
157
|
+
const JTA_DEFAULT_FPS = 24;
|
|
158
|
+
function scaledFrameDelay(milliseconds) {
|
|
159
|
+
return milliseconds <= 0 ? 0 : Math.max(1, Math.round(milliseconds / (1e3 / JTA_DEFAULT_FPS)));
|
|
160
|
+
}
|
|
161
|
+
function jtaSpeed(interval) {
|
|
162
|
+
return interval <= 0 ? 1 : Math.max(1, Math.round(interval / (1e3 / JTA_DEFAULT_FPS)));
|
|
163
|
+
}
|
|
164
|
+
function writeInt16(value) {
|
|
165
|
+
const data = new Uint8Array(2);
|
|
166
|
+
new DataView(data.buffer).setInt16(0, value);
|
|
167
|
+
return data;
|
|
168
|
+
}
|
|
169
|
+
function writeUint16(value) {
|
|
170
|
+
const data = new Uint8Array(2);
|
|
171
|
+
new DataView(data.buffer).setUint16(0, value);
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
function writeInt32(value) {
|
|
175
|
+
const data = new Uint8Array(4);
|
|
176
|
+
new DataView(data.buffer).setInt32(0, value);
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
function writeByte(value) {
|
|
180
|
+
return new Uint8Array([value & 255]);
|
|
181
|
+
}
|
|
182
|
+
function concatBytes(chunks) {
|
|
183
|
+
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
184
|
+
const data = new Uint8Array(length);
|
|
185
|
+
let offset = 0;
|
|
186
|
+
for (const chunk of chunks) {
|
|
187
|
+
data.set(chunk, offset);
|
|
188
|
+
offset += chunk.byteLength;
|
|
189
|
+
}
|
|
190
|
+
return data;
|
|
191
|
+
}
|
|
192
|
+
function encodeJtaUtf(value) {
|
|
193
|
+
const bytes = new TextEncoder().encode(value);
|
|
194
|
+
return concatBytes([writeUint16(bytes.byteLength), bytes]);
|
|
195
|
+
}
|
|
196
|
+
function serializeMovieClip(resource, frames, textures) {
|
|
197
|
+
const chunks = [
|
|
198
|
+
encodeJtaUtf(JTA_FILE_MARK),
|
|
199
|
+
writeInt32(JTA_VERSION),
|
|
200
|
+
writeByte(0),
|
|
201
|
+
writeByte(0),
|
|
202
|
+
writeByte(0),
|
|
203
|
+
writeByte(0),
|
|
204
|
+
writeUint16(0),
|
|
205
|
+
writeUint16(0),
|
|
206
|
+
writeUint16(resource.getWidth?.() ?? 0),
|
|
207
|
+
writeUint16(resource.getHeight?.() ?? 0),
|
|
208
|
+
writeByte(jtaSpeed(resource.getInterval?.() ?? 0)),
|
|
209
|
+
writeByte(scaledFrameDelay(resource.getRepeatDelay?.() ?? 0)),
|
|
210
|
+
writeByte(resource.getSwing?.() ? 1 : 0),
|
|
211
|
+
writeInt16(frames.length)
|
|
212
|
+
];
|
|
213
|
+
for (const [index, frame] of frames.entries()) chunks.push(writeInt16(scaledFrameDelay(frame.getAddDelay())), writeInt16(frame.getRectX()), writeInt16(frame.getRectY()), writeInt16(frame.getRectWidth()), writeInt16(frame.getRectHeight()), writeInt16(textures[index]?.byteLength === 0 ? -1 : index));
|
|
214
|
+
chunks.push(writeInt16(textures.length));
|
|
215
|
+
for (const texture of textures) chunks.push(writeInt32(texture.byteLength), texture);
|
|
216
|
+
return concatBytes(chunks);
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/restore.ts
|
|
220
|
+
const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
221
|
+
137,
|
|
222
|
+
80,
|
|
223
|
+
78,
|
|
224
|
+
71,
|
|
225
|
+
13,
|
|
226
|
+
10,
|
|
227
|
+
26,
|
|
228
|
+
10,
|
|
229
|
+
0,
|
|
230
|
+
0,
|
|
231
|
+
0,
|
|
232
|
+
13,
|
|
233
|
+
73,
|
|
234
|
+
72,
|
|
235
|
+
68,
|
|
236
|
+
82,
|
|
237
|
+
0,
|
|
238
|
+
0,
|
|
239
|
+
0,
|
|
240
|
+
1,
|
|
241
|
+
0,
|
|
242
|
+
0,
|
|
243
|
+
0,
|
|
244
|
+
1,
|
|
245
|
+
8,
|
|
246
|
+
6,
|
|
247
|
+
0,
|
|
248
|
+
0,
|
|
249
|
+
0,
|
|
250
|
+
31,
|
|
251
|
+
21,
|
|
252
|
+
196,
|
|
253
|
+
137,
|
|
254
|
+
0,
|
|
255
|
+
0,
|
|
256
|
+
0,
|
|
257
|
+
13,
|
|
258
|
+
73,
|
|
259
|
+
68,
|
|
260
|
+
65,
|
|
261
|
+
84,
|
|
262
|
+
120,
|
|
263
|
+
156,
|
|
264
|
+
99,
|
|
265
|
+
96,
|
|
266
|
+
0,
|
|
267
|
+
0,
|
|
268
|
+
0,
|
|
269
|
+
2,
|
|
270
|
+
0,
|
|
271
|
+
1,
|
|
272
|
+
229,
|
|
273
|
+
39,
|
|
274
|
+
212,
|
|
275
|
+
138,
|
|
276
|
+
0,
|
|
277
|
+
0,
|
|
278
|
+
0,
|
|
279
|
+
0,
|
|
280
|
+
73,
|
|
281
|
+
69,
|
|
282
|
+
78,
|
|
283
|
+
68,
|
|
284
|
+
174,
|
|
285
|
+
66,
|
|
286
|
+
96,
|
|
287
|
+
130
|
|
288
|
+
]);
|
|
289
|
+
function assertSafeRestoreSegment(value, label) {
|
|
290
|
+
if (!value || value === "." || value === ".." || value.includes("\0") || /[\\/:]/u.test(value)) throw new Error(`restore: Invalid ${label} "${value}".`);
|
|
291
|
+
}
|
|
292
|
+
function normalizeVirtualPath(path) {
|
|
293
|
+
const raw = (path ?? "").trim();
|
|
294
|
+
if (!raw || raw === "/") return "";
|
|
295
|
+
if (raw.includes("\0") || raw.startsWith("\\") || raw.startsWith("//") || /^[a-z]:/iu.test(raw)) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
296
|
+
const segments = raw.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
297
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment.includes(":"))) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
298
|
+
return segments.join("/");
|
|
299
|
+
}
|
|
300
|
+
function resourceFileName(resource) {
|
|
301
|
+
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || "";
|
|
302
|
+
}
|
|
303
|
+
function resourcePublishedFileName(resource) {
|
|
304
|
+
const publishedFile = (resource.getExtras?.() ?? {})._publishedFile;
|
|
305
|
+
return typeof publishedFile === "string" ? publishedFile : resourceFileName(resource);
|
|
306
|
+
}
|
|
307
|
+
function normalizePublishedLooseResourceFileName(resource, fileName) {
|
|
308
|
+
if (resource.propertyType === "MiscResource" && /\.atlas\.txt$/i.test(fileName)) return fileName.replace(/\.atlas\.txt$/i, ".atlas");
|
|
309
|
+
if (resource.propertyType === "SpineResource" && /\.skel\.bytes$/i.test(fileName)) return fileName.replace(/\.skel\.bytes$/i, ".skel");
|
|
310
|
+
return fileName;
|
|
311
|
+
}
|
|
312
|
+
function replaceLooseResourceBaseName(resource, fileName) {
|
|
313
|
+
const normalized = normalizePublishedLooseResourceFileName(resource, fileName);
|
|
314
|
+
const displayName = resource.getName?.() ?? "";
|
|
315
|
+
if (!displayName) return normalized;
|
|
316
|
+
const baseName = fileBaseName(normalized);
|
|
317
|
+
const ext = /((?:\.[^.\\/]+)+)$/u.exec(baseName)?.[1] ?? "";
|
|
318
|
+
const currentBaseName = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
319
|
+
const resourceId = resource.getId?.() ?? "";
|
|
320
|
+
if (!resourceId || currentBaseName.toLowerCase() !== resourceId.toLowerCase()) return normalized;
|
|
321
|
+
return `${normalized.slice(0, normalized.length - baseName.length)}${displayName}${ext}`;
|
|
322
|
+
}
|
|
323
|
+
function fileBaseName(fileName) {
|
|
324
|
+
return fileName.split(/[\\/]/).pop() ?? fileName;
|
|
325
|
+
}
|
|
326
|
+
function stripExtension(fileName) {
|
|
327
|
+
return fileBaseName(fileName).replace(/\.[^.]+$/u, "");
|
|
328
|
+
}
|
|
329
|
+
function resourceInstanceFileName(resource) {
|
|
330
|
+
const fileName = (resource.propertyType === "Component" ? `${resource.getName?.() ?? resource.getId?.() ?? "component"}.xml` : resourceFileName(resource)).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
331
|
+
if (!fileName) return "";
|
|
332
|
+
if (fileName.includes("/")) return fileName;
|
|
333
|
+
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
334
|
+
return virtualPath ? `${virtualPath}/${fileName}` : fileName;
|
|
335
|
+
}
|
|
336
|
+
function isSyntheticFontGlyphResource(resource) {
|
|
337
|
+
return resource.getExtras?.()?._syntheticFontGlyph === true;
|
|
338
|
+
}
|
|
339
|
+
function glyphDisplayChar(glyph) {
|
|
340
|
+
const char = glyph.getChar();
|
|
341
|
+
if (char) return char;
|
|
342
|
+
const charId = glyph.getCharId();
|
|
343
|
+
if (charId <= 0) return "";
|
|
344
|
+
try {
|
|
345
|
+
return String.fromCodePoint(charId);
|
|
346
|
+
} catch {
|
|
347
|
+
return "";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function sanitizeGlyphFileSegment(char) {
|
|
351
|
+
if (!char) return "glyph";
|
|
352
|
+
return char.replace(/\s/gu, "space").replace(/[\\/:*?"<>|]/gu, "_").replace(/\./gu, "_").split("").filter((item) => {
|
|
353
|
+
return (item.codePointAt(0) ?? 0) >= 32;
|
|
354
|
+
}).join("") || "glyph";
|
|
355
|
+
}
|
|
356
|
+
function defaultSyntheticFontGlyphFileName(resourceId) {
|
|
357
|
+
return `${resourceId}.png`;
|
|
358
|
+
}
|
|
359
|
+
function syntheticFontGlyphVirtualPath(pkg, font) {
|
|
360
|
+
const pkgName = pkg.getName?.() ?? "";
|
|
361
|
+
const fontBase = stripExtension(resourceFileName(font)).toLowerCase();
|
|
362
|
+
if (pkgName === "EmitNumbers") return "/";
|
|
363
|
+
if (pkgName === "Transition" && fontBase === "number3") return "/";
|
|
364
|
+
return "/images/";
|
|
365
|
+
}
|
|
366
|
+
function syntheticFontGlyphFileName(pkg, font, glyph, index, glyphCount) {
|
|
367
|
+
const pkgName = pkg.getName?.() ?? "";
|
|
368
|
+
const char = glyphDisplayChar(glyph);
|
|
369
|
+
const fontBase = stripExtension(resourceFileName(font));
|
|
370
|
+
if (/^(hitnumber|number3)$/i.test(fontBase) && /^[0-9]$/u.test(char)) return `h${char}.png`;
|
|
371
|
+
if (/^cdtime$/i.test(fontBase) && /^[0-9]$/u.test(char)) return `${char}(4)_png.png`;
|
|
372
|
+
if (pkgName === "EmitNumbers" && /^number1$/i.test(fontBase)) {
|
|
373
|
+
if (/^[0-9]$/u.test(char)) return `${char}(2)5_png.png`;
|
|
374
|
+
if (char === "-") return "m2_png.png";
|
|
375
|
+
}
|
|
376
|
+
if (pkgName === "EmitNumbers" && /^number2$/i.test(fontBase)) {
|
|
377
|
+
if (/^[0-9]$/u.test(char)) return `${char}(4)_png.png`;
|
|
378
|
+
if (char === "-") return "m1_png.png";
|
|
379
|
+
}
|
|
380
|
+
if (pkgName === "Transition" && /^number1$/i.test(fontBase)) {
|
|
381
|
+
const display = char === "0" && index === glyphCount - 1 ? "0-" : sanitizeGlyphFileSegment(char);
|
|
382
|
+
return `${String(index).padStart(4, "0")}_${display}_png.png`;
|
|
383
|
+
}
|
|
384
|
+
if (pkgName === "Transition" && /^number2$/i.test(fontBase)) return `${String(index).padStart(4, "0")}_${sanitizeGlyphFileSegment(char)}.png`;
|
|
385
|
+
const display = sanitizeGlyphFileSegment(char);
|
|
386
|
+
return `${String(index).padStart(4, "0")}_${display}.png`;
|
|
387
|
+
}
|
|
388
|
+
function syntheticFontTextureFileName(font) {
|
|
389
|
+
return `${stripExtension(resourceFileName(font)) || font.getId?.() || "font"}_atlas.png`;
|
|
390
|
+
}
|
|
391
|
+
function sameVirtualPath(a, b) {
|
|
392
|
+
return normalizeVirtualPath(a.getPath?.()) === normalizeVirtualPath(b.getPath?.());
|
|
393
|
+
}
|
|
394
|
+
function imageFileName(resource) {
|
|
395
|
+
const current = resource.getFileName?.() ?? "";
|
|
396
|
+
if (current) return current;
|
|
397
|
+
const name = resource.getName?.() ?? resource.getId?.() ?? "image";
|
|
398
|
+
const fileName = /\.[a-z0-9]+$/i.test(name) ? name : `${name}.png`;
|
|
399
|
+
resource.setFileName?.(fileName);
|
|
400
|
+
return fileName;
|
|
401
|
+
}
|
|
402
|
+
function findImageResource(pkg, itemId) {
|
|
403
|
+
return pkg.listResources().find((resource) => {
|
|
404
|
+
return resource.propertyType === "ImageResource" && resource.getId?.() === itemId;
|
|
405
|
+
}) ?? null;
|
|
406
|
+
}
|
|
407
|
+
function isPublishedBinaryFile(fileName) {
|
|
408
|
+
return /_fui\.bytes$/i.test(fileName) || /\.fui$/i.test(fileName) || /\.bin$/i.test(fileName);
|
|
409
|
+
}
|
|
410
|
+
function inferPackageName(fileName) {
|
|
411
|
+
if (/_fui\.bytes$/i.test(fileName)) return fileName.replace(/_fui\.bytes$/i, "");
|
|
412
|
+
if (/\.fui$/i.test(fileName)) return fileName.replace(/\.fui$/i, "");
|
|
413
|
+
return fileName.replace(/\.bin$/i, "");
|
|
414
|
+
}
|
|
415
|
+
async function restore(options) {
|
|
416
|
+
const sourceDir = trimTrailingSlashes(options.inputDir);
|
|
417
|
+
const outputDir = normalizeRestoreOutputDir(options.output);
|
|
418
|
+
const outputProjectPath = resolveOutputProjectPath(outputDir, options.fs);
|
|
419
|
+
await assertRestoreOutputDir(sourceDir, outputDir, options.fs, options.force === true);
|
|
420
|
+
const packageFilter = options.packages?.length ? new Set(options.packages) : null;
|
|
421
|
+
const binaryNames = (await options.fs.readdir(sourceDir)).filter((name) => isPublishedBinaryFile(name)).filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)));
|
|
422
|
+
for (const binaryName of binaryNames) assertSafeRestoreSegment(binaryName, "published binary file name");
|
|
423
|
+
const candidateBinaryPaths = binaryNames.map((name) => options.fs.join(sourceDir, name)).sort((left, right) => left.localeCompare(right));
|
|
424
|
+
const binaryPaths = (await Promise.all(candidateBinaryPaths.map(async (filePath) => await options.fs.isFile(filePath) ? filePath : null))).filter((filePath) => !!filePath).sort((left, right) => left.localeCompare(right));
|
|
425
|
+
if (binaryPaths.length === 0) throw new Error(`No FairyGUI published binary files found in ${sourceDir}.`);
|
|
426
|
+
const restorer = new RestoreWorkflow(options.fs);
|
|
427
|
+
const document = await restorer.prepare({
|
|
428
|
+
binaryPaths,
|
|
429
|
+
sourceDir,
|
|
430
|
+
outputProjectPath,
|
|
431
|
+
projectType: options.projectType,
|
|
432
|
+
cropImage: options.cropImage,
|
|
433
|
+
extractImage: options.extractImage
|
|
434
|
+
});
|
|
435
|
+
const stagingDir = await createRestoreStagingDir(outputDir, options.fs);
|
|
436
|
+
const stagingProjectPath = options.fs.join(stagingDir, basename(outputProjectPath));
|
|
437
|
+
const warnings = [];
|
|
438
|
+
try {
|
|
439
|
+
await restorer.write(document, {
|
|
440
|
+
binaryPaths,
|
|
441
|
+
sourceDir,
|
|
442
|
+
outputProjectPath: stagingProjectPath,
|
|
443
|
+
projectType: options.projectType,
|
|
444
|
+
cropImage: options.cropImage,
|
|
445
|
+
extractImage: options.extractImage
|
|
446
|
+
}, warnings);
|
|
447
|
+
const cleanupWarning = await commitRestoreOutput(stagingDir, outputDir, options.fs);
|
|
448
|
+
if (cleanupWarning) warnings.push(cleanupWarning);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
await options.fs.rm(stagingDir, {
|
|
451
|
+
recursive: true,
|
|
452
|
+
force: true
|
|
453
|
+
}).catch(() => void 0);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
document,
|
|
458
|
+
projectPath: outputProjectPath,
|
|
459
|
+
warnings
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
var RestoreWorkflow = class {
|
|
463
|
+
_fs;
|
|
464
|
+
constructor(fs) {
|
|
465
|
+
this._fs = fs;
|
|
466
|
+
}
|
|
467
|
+
async prepare(options) {
|
|
468
|
+
const doc = await new _openfairygui_core.BinaryReader(this._fs).readMany(options.binaryPaths);
|
|
469
|
+
this._assertDocumentPaths(doc);
|
|
470
|
+
this._initializeProjectDefaults(doc, options.projectType);
|
|
471
|
+
this._initializeImageFileNames(doc);
|
|
472
|
+
this._initializeLooseResourceFileNames(doc);
|
|
473
|
+
this._assertDocumentPaths(doc);
|
|
474
|
+
await this._synthesizeLooseSkeletonResources(doc, options.sourceDir);
|
|
475
|
+
this._initializeRestoredResourceRelations(doc);
|
|
476
|
+
this._initializePublishedFontTextureIds(doc);
|
|
477
|
+
this._initializeFontTextureImageResources(doc);
|
|
478
|
+
this._initializeFontGlyphImageResources(doc);
|
|
479
|
+
this._initializePublishedTextFontResources(doc);
|
|
480
|
+
this._initializeDisplayObjectFileNames(doc);
|
|
481
|
+
this._initializePublishedFontDefaults(doc);
|
|
482
|
+
this._assertDocumentPaths(doc);
|
|
483
|
+
return doc;
|
|
484
|
+
}
|
|
485
|
+
async write(doc, options, warnings) {
|
|
486
|
+
await new _openfairygui_core.ProjectWriter(this._fs).write(doc, options.outputProjectPath);
|
|
487
|
+
await this._restoreAssets(doc, options, warnings);
|
|
488
|
+
}
|
|
489
|
+
_assertDocumentPaths(doc) {
|
|
490
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
491
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
492
|
+
assertSafeRestoreSegment(pkg.getPublishName() || pkg.getName(), "package publish name");
|
|
493
|
+
for (const resource of pkg.listResources()) {
|
|
494
|
+
normalizeVirtualPath(resource.getPath?.());
|
|
495
|
+
const branch = resource.getBranch?.() ?? "";
|
|
496
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
497
|
+
const fileName = resourceFileName(resource);
|
|
498
|
+
if (fileName) assertSafeRestoreSegment(fileName, "resource file name");
|
|
499
|
+
const publishedFileName = resourcePublishedFileName(resource);
|
|
500
|
+
if (publishedFileName) assertSafeRestoreSegment(publishedFileName, "published resource file name");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
_initializeProjectDefaults(doc, projectType) {
|
|
505
|
+
doc.getRoot().setProjectId((0, _openfairygui_core.generateId)()).setProjectType(projectType ?? _openfairygui_core.ProjectType.Unity).setVersion("3.0").setSettings({
|
|
506
|
+
publish: {
|
|
507
|
+
binaryFormat: true,
|
|
508
|
+
fileExtension: "bytes",
|
|
509
|
+
compressDesc: false
|
|
510
|
+
},
|
|
511
|
+
common: {},
|
|
512
|
+
adaptation: {}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
_initializeImageFileNames(doc) {
|
|
516
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of pkg.listResources()) {
|
|
517
|
+
if (resource.propertyType !== "ImageResource") continue;
|
|
518
|
+
imageFileName(resource);
|
|
519
|
+
resource.setExtras?.({
|
|
520
|
+
...resource.getExtras?.() ?? {},
|
|
521
|
+
_suppressPackageSize: true
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
_initializeLooseResourceFileNames(doc) {
|
|
526
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of pkg.listResources()) {
|
|
527
|
+
if (![
|
|
528
|
+
"MiscResource",
|
|
529
|
+
"SpineResource",
|
|
530
|
+
"DragonBonesResource",
|
|
531
|
+
"SoundResource"
|
|
532
|
+
].includes(resource.propertyType)) continue;
|
|
533
|
+
const current = resource.getFile?.() ?? "";
|
|
534
|
+
if (!current) continue;
|
|
535
|
+
const normalized = replaceLooseResourceBaseName(resource, current);
|
|
536
|
+
if (normalized !== current) resource.setFile?.(normalized);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async _synthesizeLooseSkeletonResources(doc, sourceDir) {
|
|
540
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of [...pkg.listResources()]) {
|
|
541
|
+
let current = resource;
|
|
542
|
+
if (resource.propertyType === "DragonBonesResource" && /\.skel\.bytes$/i.test(resourceFileName(resource))) {
|
|
543
|
+
const normalizedFile = resourceFileName(resource).replace(/\.skel\.bytes$/i, ".skel");
|
|
544
|
+
const atlasBase = stripExtension(normalizedFile).replace(/-(?:pro|ess)$/i, "-pma");
|
|
545
|
+
if (await this._resolveLooseSourceFile(pkg, sourceDir, `${atlasBase}.atlas`)) current = this._replaceSkeletonResourceType(doc, pkg, resource, "SpineResource", normalizedFile);
|
|
546
|
+
}
|
|
547
|
+
if (current.propertyType === "SpineResource") await this._ensureSpineSidecarResources(doc, pkg, current, sourceDir);
|
|
548
|
+
else if (current.propertyType === "DragonBonesResource") await this._ensureDragonBonesSidecarResources(doc, pkg, current, sourceDir);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
_initializeRestoredResourceRelations(doc) {
|
|
552
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
553
|
+
const resources = pkg.listResources();
|
|
554
|
+
for (const resource of resources) if (resource.propertyType === "SpineResource") this._initializeSpineResourceRelation(resource, resources);
|
|
555
|
+
else if (resource.propertyType === "DragonBonesResource") this._initializeDragonBonesResourceRelation(resource, resources);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
_replaceSkeletonResourceType(doc, pkg, resource, targetType, fileName) {
|
|
559
|
+
const replacement = targetType === "SpineResource" ? doc.createSpineResource(resource.getName?.() ?? "") : doc.createDragonBonesResource(resource.getName?.() ?? "");
|
|
560
|
+
replacement.setId?.(resource.getId?.() ?? "").setPath?.(resource.getPath?.() ?? "/").setFile?.(fileName).setExported?.(resource.getExported?.() ?? false).setWidth?.(resource.getWidth?.() ?? 0).setHeight?.(resource.getHeight?.() ?? 0).setRequireIds?.(resource.getRequireIds?.() ?? []).setAtlasNames?.(resource.getAtlasNames?.() ?? []).setAnchor?.(resource.getAnchorX?.() ?? 0, resource.getAnchorY?.() ?? 0).setBranch?.(resource.getBranch?.() ?? "").setBranchItemIds?.(resource.getBranchItemIds?.() ?? []);
|
|
561
|
+
replacement.setExtras?.({ ...resource.getExtras?.() ?? {} });
|
|
562
|
+
pkg.removeResource(resource);
|
|
563
|
+
pkg.addResource(replacement);
|
|
564
|
+
return replacement;
|
|
565
|
+
}
|
|
566
|
+
async _ensureSpineSidecarResources(doc, pkg, resource, sourceDir) {
|
|
567
|
+
const skeletonBase = stripExtension(resourceFileName(resource).replace(/\.skel\.bytes$/i, ".skel"));
|
|
568
|
+
if (!skeletonBase) return;
|
|
569
|
+
const atlasBase = skeletonBase.replace(/-(?:pro|ess)$/i, "-pma");
|
|
570
|
+
const atlas = await this._ensureLooseMiscResource(doc, pkg, resource, sourceDir, `${atlasBase}.atlas`);
|
|
571
|
+
const texture = await this._ensureLooseImageResource(doc, pkg, resource, sourceDir, `${atlasBase}.png`);
|
|
572
|
+
const requireIds = [atlas?.getId?.(), texture?.getId?.()].filter((id) => !!id);
|
|
573
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
574
|
+
if (atlas) resource.setAtlasNames?.([atlasBase]);
|
|
575
|
+
}
|
|
576
|
+
async _ensureDragonBonesSidecarResources(doc, pkg, resource, sourceDir) {
|
|
577
|
+
const skeletonBase = stripExtension(resourceFileName(resource)).replace(/_ske$/i, "");
|
|
578
|
+
if (!skeletonBase) return;
|
|
579
|
+
const textureJson = await this._ensureLooseMiscResource(doc, pkg, resource, sourceDir, `${skeletonBase}_tex.json`);
|
|
580
|
+
const textureImage = await this._ensureLooseImageResource(doc, pkg, resource, sourceDir, `${skeletonBase}.png`);
|
|
581
|
+
const requireIds = [textureJson?.getId?.(), textureImage?.getId?.()].filter((id) => !!id);
|
|
582
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
583
|
+
}
|
|
584
|
+
async _ensureLooseMiscResource(doc, pkg, owner, sourceDir, fileName) {
|
|
585
|
+
const resources = pkg.listResources();
|
|
586
|
+
const existing = this._findResourceByFile(resources, owner, "MiscResource", fileName);
|
|
587
|
+
if (existing) return existing;
|
|
588
|
+
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
589
|
+
if (!sourcePath) return null;
|
|
590
|
+
const resource = doc.createMiscResource(stripExtension(fileName));
|
|
591
|
+
resource.setId((0, _openfairygui_core.generateId)()).setPath(owner.getPath?.() ?? "/").setBranch(owner.getBranch?.() ?? "").setBranchItemIds(owner.getBranchItemIds?.() ?? []).setExported(false).setFile(fileName);
|
|
592
|
+
resource.setExtras?.({
|
|
593
|
+
...resource.getExtras?.() ?? {},
|
|
594
|
+
_publishedFile: fileBaseName(sourcePath)
|
|
595
|
+
});
|
|
596
|
+
pkg.addResource(resource);
|
|
597
|
+
return resource;
|
|
598
|
+
}
|
|
599
|
+
async _ensureLooseImageResource(doc, pkg, owner, sourceDir, fileName) {
|
|
600
|
+
const resources = pkg.listResources();
|
|
601
|
+
const existing = this._findResourceByFile(resources, owner, "ImageResource", fileName);
|
|
602
|
+
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
603
|
+
if (!sourcePath) return existing ?? null;
|
|
604
|
+
if (existing) {
|
|
605
|
+
existing.setExtras?.({
|
|
606
|
+
...existing.getExtras?.() ?? {},
|
|
607
|
+
_publishedFile: fileBaseName(sourcePath),
|
|
608
|
+
_restoreAsLooseImage: true
|
|
609
|
+
});
|
|
610
|
+
return existing;
|
|
611
|
+
}
|
|
612
|
+
const resource = doc.createImageResource(stripExtension(fileName));
|
|
613
|
+
resource.setId((0, _openfairygui_core.generateId)()).setPath(owner.getPath?.() ?? "/").setBranch(owner.getBranch?.() ?? "").setBranchItemIds(owner.getBranchItemIds?.() ?? []).setExported(false).setFileName(fileName);
|
|
614
|
+
resource.setExtras?.({
|
|
615
|
+
...resource.getExtras?.() ?? {},
|
|
616
|
+
_publishedFile: fileBaseName(sourcePath),
|
|
617
|
+
_suppressPackageSize: true,
|
|
618
|
+
_restoreAsLooseImage: true
|
|
619
|
+
});
|
|
620
|
+
pkg.addResource(resource);
|
|
621
|
+
return resource;
|
|
622
|
+
}
|
|
623
|
+
_initializeDisplayObjectFileNames(doc) {
|
|
624
|
+
for (const pkg of doc.getRoot().listPackages()) for (const component of pkg.listComponents()) for (const child of component.listChildren()) {
|
|
625
|
+
if (!child.setFileName || child.getFileName?.()) continue;
|
|
626
|
+
const resource = this._resolveDisplayObjectResource(doc, pkg, child);
|
|
627
|
+
const fileName = resource ? resourceInstanceFileName(resource) : "";
|
|
628
|
+
if (fileName) child.setFileName(fileName);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
_initializeFontGlyphImageResources(doc) {
|
|
632
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of [...pkg.listResources()]) {
|
|
633
|
+
if (resource.propertyType !== "FontResource") continue;
|
|
634
|
+
const glyphEntries = /* @__PURE__ */ new Map();
|
|
635
|
+
for (const [index, glyph] of resource.listGlyphs().entries()) {
|
|
636
|
+
const glyphId = glyph.getImg?.() ?? "";
|
|
637
|
+
if (!glyphId || glyphEntries.has(glyphId)) continue;
|
|
638
|
+
glyphEntries.set(glyphId, {
|
|
639
|
+
glyph,
|
|
640
|
+
index
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
for (const [glyphId, entry] of glyphEntries) {
|
|
644
|
+
if (pkg.getResourceById(glyphId)) continue;
|
|
645
|
+
const image = doc.createImageResource(glyphId);
|
|
646
|
+
image.setId(glyphId).setPath(syntheticFontGlyphVirtualPath(pkg, resource)).setBranch(resource.getBranch?.() ?? "").setFileName(syntheticFontGlyphFileName(pkg, resource, entry.glyph, entry.index, glyphEntries.size)).setExtras({
|
|
647
|
+
...image.getExtras?.() ?? {},
|
|
648
|
+
_syntheticFontGlyph: true,
|
|
649
|
+
_packageOrderAfterId: resource.getId?.() ?? "",
|
|
650
|
+
_packageOrderWeight: 1,
|
|
651
|
+
_suppressPackageSize: true
|
|
652
|
+
});
|
|
653
|
+
pkg.addResource(image);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
_initializeFontTextureImageResources(doc) {
|
|
658
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of [...pkg.listResources()]) {
|
|
659
|
+
if (resource.propertyType !== "FontResource") continue;
|
|
660
|
+
const textureId = resource.getTextureId?.() ?? "";
|
|
661
|
+
if (!textureId || pkg.getResourceById(textureId)) continue;
|
|
662
|
+
const image = doc.createImageResource(textureId);
|
|
663
|
+
image.setId(textureId).setPath(resource.getPath?.() ?? "/").setBranch(resource.getBranch?.() ?? "").setFileName(syntheticFontTextureFileName(resource)).setExtras({
|
|
664
|
+
...image.getExtras?.() ?? {},
|
|
665
|
+
_syntheticFontTexture: true,
|
|
666
|
+
_packageOrderAfterId: resource.getId?.() ?? "",
|
|
667
|
+
_packageOrderWeight: 0,
|
|
668
|
+
_suppressPackageSize: true
|
|
669
|
+
});
|
|
670
|
+
pkg.addResource(image);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
_resolveDisplayObjectResource(doc, pkg, child) {
|
|
674
|
+
const src = child.getSrc?.() ?? "";
|
|
675
|
+
if (!src) return null;
|
|
676
|
+
return (child.getPackageId?.() ? doc.getRoot().getPackageById(child.getPackageId?.() ?? "") : pkg)?.getResourceById(src) ?? null;
|
|
677
|
+
}
|
|
678
|
+
_initializeSpineResourceRelation(resource, resources) {
|
|
679
|
+
const skeletonBase = stripExtension(resourceFileName(resource));
|
|
680
|
+
if (!skeletonBase) return;
|
|
681
|
+
const atlasBase = skeletonBase.replace(/-(?:pro|ess)$/i, "-pma");
|
|
682
|
+
const requireIds = [];
|
|
683
|
+
const atlas = this._findResourceByFile(resources, resource, "MiscResource", `${atlasBase}.atlas`);
|
|
684
|
+
const texture = this._findResourceByFile(resources, resource, "ImageResource", `${atlasBase}.png`);
|
|
685
|
+
if (atlas?.getId?.()) requireIds.push(atlas.getId());
|
|
686
|
+
if (texture?.getId?.()) requireIds.push(texture.getId());
|
|
687
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
688
|
+
if (atlas) resource.setAtlasNames?.([atlasBase]);
|
|
689
|
+
}
|
|
690
|
+
_initializeDragonBonesResourceRelation(resource, resources) {
|
|
691
|
+
const skeletonBase = stripExtension(resourceFileName(resource)).replace(/_ske$/i, "");
|
|
692
|
+
if (!skeletonBase) return;
|
|
693
|
+
const requireIds = [];
|
|
694
|
+
const textureJson = this._findResourceByFile(resources, resource, "MiscResource", `${skeletonBase}_tex.json`);
|
|
695
|
+
const textureImage = this._findResourceByFile(resources, resource, "ImageResource", `${skeletonBase}.png`);
|
|
696
|
+
if (textureJson?.getId?.()) requireIds.push(textureJson.getId());
|
|
697
|
+
if (textureImage?.getId?.()) requireIds.push(textureImage.getId());
|
|
698
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
699
|
+
}
|
|
700
|
+
_findResourceByFile(resources, owner, propertyType, fileName) {
|
|
701
|
+
const expected = fileName.toLowerCase();
|
|
702
|
+
return resources.find((resource) => {
|
|
703
|
+
return resource.propertyType === propertyType && sameVirtualPath(owner, resource) && fileBaseName(resourceFileName(resource)).toLowerCase() === expected;
|
|
704
|
+
}) ?? null;
|
|
705
|
+
}
|
|
706
|
+
_initializePublishedFontDefaults(doc) {
|
|
707
|
+
for (const pkg of doc.getRoot().listPackages()) for (const resource of pkg.listResources()) {
|
|
708
|
+
if (resource.propertyType !== "FontResource") continue;
|
|
709
|
+
const fileName = resourceFileName(resource);
|
|
710
|
+
if (!/\bsdf\b/i.test(fileName)) continue;
|
|
711
|
+
if (!resource.getRenderMode?.()) resource.setRenderMode?.("sdfaa");
|
|
712
|
+
if (!resource.getSamplePointSize?.()) resource.setSamplePointSize?.(60);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
_initializePublishedTextFontResources(doc) {
|
|
716
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
717
|
+
const fontResources = pkg.listResources().filter((resource) => resource.propertyType === "FontResource");
|
|
718
|
+
const fontByFileName = new Map(fontResources.map((resource) => [resourceFileName(resource).toLowerCase(), resource]));
|
|
719
|
+
const fontByDisplayName = new Map(fontResources.map((resource) => [stripExtension(resourceFileName(resource)).toLowerCase(), resource]));
|
|
720
|
+
for (const component of pkg.listComponents()) for (const child of component.listChildren()) {
|
|
721
|
+
const font = child.getFont?.() ?? "";
|
|
722
|
+
if (!font || font.startsWith("ui://")) continue;
|
|
723
|
+
if (!/\bsdf\b/i.test(font)) continue;
|
|
724
|
+
const normalized = font.trim().toLowerCase();
|
|
725
|
+
let resource = fontByDisplayName.get(normalized) ?? fontByFileName.get(`${normalized}.ttf`);
|
|
726
|
+
if (!resource) {
|
|
727
|
+
resource = doc.createFontResource(font.trim());
|
|
728
|
+
resource.setId((0, _openfairygui_core.generateId)()).setPath("/font/").setFileName(`${font.trim()}.ttf`).setExported(false).setRenderMode("sdfaa").setSamplePointSize(60).setTtf(true);
|
|
729
|
+
pkg.addResource(resource);
|
|
730
|
+
fontByDisplayName.set(normalized, resource);
|
|
731
|
+
fontByFileName.set(`${normalized}.ttf`, resource);
|
|
732
|
+
}
|
|
733
|
+
child.setFont?.(`ui://${pkg.getId()}${resource.getId?.() ?? ""}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
_initializePublishedFontTextureIds(doc) {
|
|
738
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
739
|
+
const resources = pkg.listResources();
|
|
740
|
+
for (const resource of resources) {
|
|
741
|
+
if (resource.propertyType !== "FontResource") continue;
|
|
742
|
+
if (resource.getTextureId?.()) continue;
|
|
743
|
+
if (resource.getTtf?.() !== true) continue;
|
|
744
|
+
const expectedFileName = syntheticFontTextureFileName(resource).toLowerCase();
|
|
745
|
+
const texture = resources.find((candidate) => {
|
|
746
|
+
return candidate.propertyType === "ImageResource" && sameVirtualPath(resource, candidate) && fileBaseName(resourceFileName(candidate)).toLowerCase() === expectedFileName;
|
|
747
|
+
});
|
|
748
|
+
if (texture?.getId?.()) resource.setTextureId?.(texture.getId());
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async _restoreAssets(doc, options, warnings) {
|
|
753
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
754
|
+
await this._restoreAtlasImages(pkg, options);
|
|
755
|
+
await this._writeGeneratedResources(pkg, options, warnings);
|
|
756
|
+
await this._copyLooseResources(pkg, options, warnings);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async _restoreAtlasImages(pkg, options) {
|
|
760
|
+
if (!options.cropImage) return;
|
|
761
|
+
for (const atlas of pkg.listAtlases()) {
|
|
762
|
+
const sourceAtlas = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, atlas.getFile()));
|
|
763
|
+
if (!sourceAtlas) throw new Error(`Atlas image not found for package "${pkg.getName()}": ${this._sourceFileCandidates(pkg, atlas.getFile()).join(", ")}`);
|
|
764
|
+
for (const sprite of atlas.listSprites()) {
|
|
765
|
+
const image = findImageResource(pkg, sprite.getItemId());
|
|
766
|
+
if (!image) continue;
|
|
767
|
+
if (sprite.getRectWidth() <= 0 || sprite.getRectHeight() <= 0) continue;
|
|
768
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, image, imageFileName(image));
|
|
769
|
+
const imageWidth = image.getWidth?.() ?? 0;
|
|
770
|
+
const imageHeight = image.getHeight?.() ?? 0;
|
|
771
|
+
const spriteWidth = sprite.getRotated() ? sprite.getRectHeight() : sprite.getRectWidth();
|
|
772
|
+
const spriteHeight = sprite.getRotated() ? sprite.getRectWidth() : sprite.getRectHeight();
|
|
773
|
+
await this._mkdirForFile(outputPath);
|
|
774
|
+
await options.cropImage({
|
|
775
|
+
sourcePath: sourceAtlas,
|
|
776
|
+
outputPath,
|
|
777
|
+
left: sprite.getRectX(),
|
|
778
|
+
top: sprite.getRectY(),
|
|
779
|
+
width: sprite.getRectWidth(),
|
|
780
|
+
height: sprite.getRectHeight(),
|
|
781
|
+
rotated: sprite.getRotated(),
|
|
782
|
+
offsetX: sprite.getOffsetX(),
|
|
783
|
+
offsetY: sprite.getOffsetY(),
|
|
784
|
+
expectedWidth: Math.max(imageWidth, sprite.getOriginalWidth(), spriteWidth),
|
|
785
|
+
expectedHeight: Math.max(imageHeight, sprite.getOriginalHeight(), spriteHeight)
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async _copyLooseResources(pkg, options, warnings) {
|
|
791
|
+
for (const resource of pkg.listResources()) {
|
|
792
|
+
const restoreAsLooseImage = resource.getExtras?.()?._restoreAsLooseImage === true;
|
|
793
|
+
if (![
|
|
794
|
+
"SoundResource",
|
|
795
|
+
"MiscResource",
|
|
796
|
+
"SpineResource",
|
|
797
|
+
"DragonBonesResource"
|
|
798
|
+
].includes(resource.propertyType) && !restoreAsLooseImage) continue;
|
|
799
|
+
const fileName = resourceFileName(resource);
|
|
800
|
+
if (!fileName) continue;
|
|
801
|
+
const sourcePath = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, resourcePublishedFileName(resource), fileName));
|
|
802
|
+
if (!sourcePath) {
|
|
803
|
+
warnings.push(`Loose resource not found for package "${pkg.getName()}": ${fileName}`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
807
|
+
await this._mkdirForFile(outputPath);
|
|
808
|
+
await this._fs.writeFileRaw(outputPath, await this._fs.readFileRaw(sourcePath));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async _writeGeneratedResources(pkg, options, warnings) {
|
|
812
|
+
for (const resource of pkg.listResources()) if (resource.propertyType === "FontResource") await this._writeFontFile(pkg, resource, options.outputProjectPath);
|
|
813
|
+
else if (resource.propertyType === "MovieClipResource") await this._writeMovieClipFile(pkg, resource, options, warnings);
|
|
814
|
+
await this._writeSyntheticFontGlyphImages(pkg, options.outputProjectPath);
|
|
815
|
+
}
|
|
816
|
+
async _writeFontFile(pkg, resource, outputProjectPath) {
|
|
817
|
+
const fileName = resourceFileName(resource);
|
|
818
|
+
if (!/\.fnt$/i.test(fileName)) return;
|
|
819
|
+
const glyphs = resource.listGlyphs?.() ?? [];
|
|
820
|
+
if (glyphs.length === 0) return;
|
|
821
|
+
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
822
|
+
await this._mkdirForFile(outputPath);
|
|
823
|
+
await this._fs.writeFile(outputPath, serializeFont(pkg, resource, glyphs));
|
|
824
|
+
}
|
|
825
|
+
async _writeMovieClipFile(pkg, resource, options, warnings) {
|
|
826
|
+
const fileName = resourceFileName(resource);
|
|
827
|
+
if (!/\.jta$/i.test(fileName)) return;
|
|
828
|
+
const frames = resource.listFrames?.() ?? [];
|
|
829
|
+
if (frames.length === 0) return;
|
|
830
|
+
if (!options.extractImage) {
|
|
831
|
+
warnings.push(`MovieClip file not generated for package "${pkg.getName()}": ${fileName}`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const sprites = await this._buildSpriteLookup(pkg, options);
|
|
835
|
+
const textures = [];
|
|
836
|
+
for (const [index, frame] of frames.entries()) {
|
|
837
|
+
const spriteEntry = sprites.get(frame.getSpriteId());
|
|
838
|
+
if (!spriteEntry) {
|
|
839
|
+
warnings.push(`MovieClip frame sprite not found for package "${pkg.getName()}": ${fileName} frame ${index}`);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const sprite = spriteEntry.sprite;
|
|
843
|
+
if (sprite.getRectWidth() <= 0 || sprite.getRectHeight() <= 0) {
|
|
844
|
+
textures.push(new Uint8Array(0));
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
textures.push(await options.extractImage({
|
|
848
|
+
sourcePath: spriteEntry.sourceAtlas,
|
|
849
|
+
left: sprite.getRectX(),
|
|
850
|
+
top: sprite.getRectY(),
|
|
851
|
+
width: sprite.getRectWidth(),
|
|
852
|
+
height: sprite.getRectHeight(),
|
|
853
|
+
rotated: sprite.getRotated(),
|
|
854
|
+
offsetX: 0,
|
|
855
|
+
offsetY: 0,
|
|
856
|
+
expectedWidth: sprite.getRotated() ? sprite.getRectHeight() : sprite.getRectWidth(),
|
|
857
|
+
expectedHeight: sprite.getRotated() ? sprite.getRectWidth() : sprite.getRectHeight()
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
861
|
+
await this._mkdirForFile(outputPath);
|
|
862
|
+
await this._fs.writeFileRaw(outputPath, serializeMovieClip(resource, frames, textures));
|
|
863
|
+
}
|
|
864
|
+
async _writeSyntheticFontGlyphImages(pkg, outputProjectPath) {
|
|
865
|
+
for (const resource of pkg.listResources()) {
|
|
866
|
+
if (resource.propertyType !== "ImageResource" || !isSyntheticFontGlyphResource(resource)) continue;
|
|
867
|
+
const fileName = resourceFileName(resource) || defaultSyntheticFontGlyphFileName(resource.getId?.() ?? "glyph");
|
|
868
|
+
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
869
|
+
await this._mkdirForFile(outputPath);
|
|
870
|
+
await this._fs.writeFileRaw(outputPath, TRANSPARENT_PNG_1X1);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async _buildSpriteLookup(pkg, options) {
|
|
874
|
+
const sprites = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const atlas of pkg.listAtlases()) {
|
|
876
|
+
const sourceAtlas = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, atlas.getFile()));
|
|
877
|
+
if (!sourceAtlas) throw new Error(`Atlas image not found for package "${pkg.getName()}": ${this._sourceFileCandidates(pkg, atlas.getFile()).join(", ")}`);
|
|
878
|
+
for (const sprite of atlas.listSprites()) sprites.set(sprite.getItemId(), {
|
|
879
|
+
sourceAtlas,
|
|
880
|
+
sprite
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return sprites;
|
|
884
|
+
}
|
|
885
|
+
_sourceFileCandidates(pkg, fileName, outputFileName = fileName) {
|
|
886
|
+
const publishName = pkg.getPublishName() || pkg.getName();
|
|
887
|
+
assertSafeRestoreSegment(publishName, "package publish name");
|
|
888
|
+
assertSafeRestoreSegment(fileName, "published source file name");
|
|
889
|
+
assertSafeRestoreSegment(outputFileName, "published source file name");
|
|
890
|
+
const candidates = Array.from(new Set([
|
|
891
|
+
`${publishName}_${fileName}`,
|
|
892
|
+
fileName,
|
|
893
|
+
`${publishName}_${outputFileName}`,
|
|
894
|
+
outputFileName
|
|
895
|
+
]));
|
|
896
|
+
for (const candidate of candidates) assertSafeRestoreSegment(candidate, "published source file name");
|
|
897
|
+
return candidates;
|
|
898
|
+
}
|
|
899
|
+
async _resolveLooseSourceFile(pkg, sourceDir, outputFileName) {
|
|
900
|
+
const candidates = outputFileName.endsWith(".atlas") ? this._sourceFileCandidates(pkg, `${outputFileName}.txt`, outputFileName) : outputFileName.endsWith(".skel") ? this._sourceFileCandidates(pkg, `${outputFileName}.bytes`, outputFileName) : this._sourceFileCandidates(pkg, outputFileName);
|
|
901
|
+
return this._resolveSourceFile(sourceDir, candidates);
|
|
902
|
+
}
|
|
903
|
+
async _resolveSourceFile(sourceDir, candidates) {
|
|
904
|
+
const resolvedSourceDir = await Promise.resolve(this._fs.resolvePath(sourceDir));
|
|
905
|
+
for (const candidate of candidates) {
|
|
906
|
+
assertSafeRestoreSegment(candidate, "published source file name");
|
|
907
|
+
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
908
|
+
if (!await this._fs.isFile(sourcePath)) continue;
|
|
909
|
+
const resolvedSourcePath = await Promise.resolve(this._fs.resolvePath(sourcePath));
|
|
910
|
+
if (!isPathWithin(resolvedSourceDir, resolvedSourcePath)) throw new Error(`restore: Published source file resolves outside the input directory: ${candidate}.`);
|
|
911
|
+
return resolvedSourcePath;
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
_resourceOutputPath(outputProjectPath, pkg, resource, fileName) {
|
|
916
|
+
const basePath = this._fs.dirname(outputProjectPath);
|
|
917
|
+
const branch = resource.getBranch?.() ?? "";
|
|
918
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
919
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
920
|
+
assertSafeRestoreSegment(fileName, "resource file name");
|
|
921
|
+
const assetsDir = branch ? `assets_${branch}` : "assets";
|
|
922
|
+
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
923
|
+
const pkgDir = this._fs.join(basePath, assetsDir, pkg.getName());
|
|
924
|
+
return virtualPath ? this._fs.join(pkgDir, virtualPath, fileName) : this._fs.join(pkgDir, fileName);
|
|
925
|
+
}
|
|
926
|
+
async _mkdirForFile(filePath) {
|
|
927
|
+
await this._fs.mkdir(this._fs.dirname(filePath));
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
//#endregion
|
|
931
|
+
Object.defineProperty(exports, "restore", {
|
|
932
|
+
enumerable: true,
|
|
933
|
+
get: function() {
|
|
934
|
+
return restore;
|
|
935
|
+
}
|
|
936
|
+
});
|