@openfairygui/functions 0.2.0-alpha.19 → 0.2.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +7 -2
  2. package/dist/index.cjs +3 -920
  3. package/dist/index.d.cts +2 -46
  4. package/dist/index.d.ts +3 -47
  5. package/dist/index.js +2 -919
  6. package/dist/node.cjs +138 -10
  7. package/dist/node.d.cts +7 -2
  8. package/dist/node.d.ts +8 -3
  9. package/dist/node.js +138 -11
  10. package/dist/{publish-DsPK0SJ1.js → publish-BJ_eelME.js} +1973 -2070
  11. package/dist/{publish-D7268_u4.cjs → publish-xFWT9Slz.cjs} +1973 -2070
  12. package/dist/restore-BW2xacB3.cjs +936 -0
  13. package/dist/{codegen-B8ZM1F4j.d.cts → restore-BeWaJNjR.d.cts} +49 -3
  14. package/dist/restore-Clk62n0O.js +931 -0
  15. package/dist/{codegen-CfbDHuFt.d.ts → restore-Dh0-Nvms.d.ts} +50 -4
  16. package/dist/web.cjs +4 -2
  17. package/dist/web.d.ts +1 -1
  18. package/dist/web.js +4 -2
  19. package/package.json +5 -2
  20. package/src/adapters/node/restore.ts +187 -0
  21. package/src/adapters/web/publish.ts +1 -247
  22. package/src/adapters/web/raster.ts +251 -0
  23. package/src/atlas/font.ts +95 -0
  24. package/src/atlas/inputs.ts +515 -0
  25. package/src/atlas/jta.ts +211 -0
  26. package/src/atlas/packing.ts +767 -0
  27. package/src/atlas.ts +23 -1639
  28. package/src/node.ts +4 -0
  29. package/src/publish/external-resources.ts +117 -0
  30. package/src/publish/options.ts +180 -0
  31. package/src/publish/package-context.ts +608 -0
  32. package/src/publish/resource-references.ts +210 -0
  33. package/src/publish.ts +21 -1130
  34. package/src/restore-internals/font.ts +100 -0
  35. package/src/restore-internals/movie-clip.ts +104 -0
  36. package/src/restore-internals/output-transaction.ts +164 -0
  37. package/src/restore.ts +14 -329
  38. /package/dist/{atlas-CDn6TirX.d.ts → atlas-C6tbl7nn.d.ts} +0 -0
package/src/restore.ts CHANGED
@@ -7,6 +7,18 @@ import {
7
7
  ProjectType,
8
8
  ProjectWriter,
9
9
  } from '@openfairygui/core';
10
+ import {
11
+ assertRestoreOutputDir,
12
+ basename,
13
+ commitRestoreOutput,
14
+ createRestoreStagingDir,
15
+ isPathWithin,
16
+ normalizeRestoreOutputDir,
17
+ resolveOutputProjectPath,
18
+ trimTrailingSlashes,
19
+ } from './restore-internals/output-transaction.js';
20
+ import { serializeFont, type RestorableFontGlyph } from './restore-internals/font.js';
21
+ import { serializeMovieClip, type RestorableMovieFrame } from './restore-internals/movie-clip.js';
10
22
 
11
23
  export interface RestoreImageCropInput {
12
24
  sourcePath: string;
@@ -120,29 +132,6 @@ interface RestorableSprite {
120
132
  getOriginalHeight(): number;
121
133
  }
122
134
 
123
- interface RestorableMovieFrame {
124
- getRectX(): number;
125
- getRectY(): number;
126
- getRectWidth(): number;
127
- getRectHeight(): number;
128
- getAddDelay(): number;
129
- getSpriteId(): string;
130
- }
131
-
132
- interface RestorableFontGlyph {
133
- getAdvance(): number;
134
- getChannel(): number;
135
- getChar(): string;
136
- getCharId(): number;
137
- getHeight(): number;
138
- getImg(): string;
139
- getWidth(): number;
140
- getX(): number;
141
- getXOffset(): number;
142
- getY(): number;
143
- getYOffset(): number;
144
- }
145
-
146
135
  type RestorableFontResource = RestorableResource & {
147
136
  listGlyphs(): RestorableFontGlyph[];
148
137
  getBranch?(): string;
@@ -164,9 +153,6 @@ interface RestorableDisplayObject {
164
153
  setFont?(font: string): unknown;
165
154
  }
166
155
 
167
- const JTA_FILE_MARK = 'yytou';
168
- const JTA_VERSION = 102;
169
- const JTA_DEFAULT_FPS = 24;
170
156
  const TRANSPARENT_PNG_1X1 = Uint8Array.from([
171
157
  137, 80, 78, 71, 13, 10, 26, 10,
172
158
  0, 0, 0, 13, 73, 72, 68, 82,
@@ -352,59 +338,6 @@ function findImageResource(pkg: Package, itemId: string): RestorableResource | n
352
338
  }) ?? null;
353
339
  }
354
340
 
355
- function fontGlyphCharId(glyph: RestorableFontGlyph): number {
356
- const charId = glyph.getCharId();
357
- if (charId > 0) return charId;
358
- const char = glyph.getChar();
359
- return char ? (char.codePointAt(0) ?? 0) : 0;
360
- }
361
-
362
- function scaledFrameDelay(milliseconds: number): number {
363
- return milliseconds <= 0 ? 0 : Math.max(1, Math.round(milliseconds / (1000 / JTA_DEFAULT_FPS)));
364
- }
365
-
366
- function jtaSpeed(interval: number): number {
367
- return interval <= 0 ? 1 : Math.max(1, Math.round(interval / (1000 / JTA_DEFAULT_FPS)));
368
- }
369
-
370
- function writeInt16(value: number): Uint8Array {
371
- const data = new Uint8Array(2);
372
- new DataView(data.buffer).setInt16(0, value);
373
- return data;
374
- }
375
-
376
- function writeUint16(value: number): Uint8Array {
377
- const data = new Uint8Array(2);
378
- new DataView(data.buffer).setUint16(0, value);
379
- return data;
380
- }
381
-
382
- function writeInt32(value: number): Uint8Array {
383
- const data = new Uint8Array(4);
384
- new DataView(data.buffer).setInt32(0, value);
385
- return data;
386
- }
387
-
388
- function writeByte(value: number): Uint8Array {
389
- return new Uint8Array([value & 0xff]);
390
- }
391
-
392
- function concatBytes(chunks: Uint8Array[]): Uint8Array {
393
- const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
394
- const data = new Uint8Array(length);
395
- let offset = 0;
396
- for (const chunk of chunks) {
397
- data.set(chunk, offset);
398
- offset += chunk.byteLength;
399
- }
400
- return data;
401
- }
402
-
403
- function encodeJtaUtf(value: string): Uint8Array {
404
- const bytes = new TextEncoder().encode(value);
405
- return concatBytes([writeUint16(bytes.byteLength), bytes]);
406
- }
407
-
408
341
  function isPublishedBinaryFile(fileName: string): boolean {
409
342
  return /_fui\.bytes$/i.test(fileName) || /\.fui$/i.test(fileName) || /\.bin$/i.test(fileName);
410
343
  }
@@ -415,168 +348,6 @@ function inferPackageName(fileName: string): string {
415
348
  return fileName.replace(/\.bin$/i, '');
416
349
  }
417
350
 
418
- function trimTrailingSlashes(value: string): string {
419
- return value.replace(/[/\\]+$/, '');
420
- }
421
-
422
- function normalizeComparablePath(value: string): string {
423
- const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
424
- const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
425
- const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
426
- const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
427
- const hasRoot = driveMatch ? true : remainder.startsWith('/');
428
- const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
429
- const segments: string[] = [];
430
-
431
- for (const segment of rawSegments) {
432
- if (segment === '.') continue;
433
- if (segment === '..') {
434
- if (segments.length > 0 && segments[segments.length - 1] !== '..') {
435
- segments.pop();
436
- } else if (!hasRoot) {
437
- segments.push('..');
438
- }
439
- continue;
440
- }
441
- segments.push(segment);
442
- }
443
-
444
- const joined = segments.join('/');
445
- const comparable = drivePrefix
446
- ? `${drivePrefix}/${joined}`.replace(/\/$/, '')
447
- : hasRoot
448
- ? `/${joined}`.replace(/\/$/, '')
449
- : joined || '.';
450
- // Restore prefers a conservative same-directory guard: false positives are safer than
451
- // missing a Windows-style case-only path alias and deleting the source publish dir.
452
- return comparable.toLowerCase();
453
- }
454
-
455
- function isPathWithin(root: string, candidate: string): boolean {
456
- const normalizedRoot = normalizeComparablePath(root);
457
- const normalizedCandidate = normalizeComparablePath(candidate);
458
- return normalizedCandidate.startsWith(`${normalizedRoot}/`);
459
- }
460
-
461
- function basename(filePath: string): string {
462
- const trimmed = trimTrailingSlashes(filePath);
463
- const match = trimmed.match(/([^/\\]+)$/);
464
- return match?.[1] ?? '';
465
- }
466
-
467
- function normalizeRestoreOutputDir(output: string): string {
468
- const normalized = trimTrailingSlashes(output);
469
- const name = basename(normalized);
470
- if (!normalized || /\.fairy$/i.test(normalized) || !name || name === '.' || name === '..' || /^[a-z]:$/iu.test(name)) {
471
- throw new Error('restore: Output must be a non-root project directory, not a .fairy file.');
472
- }
473
- return normalized;
474
- }
475
-
476
- function resolveOutputProjectPath(outputDir: string, fs: Pick<RestoreFileSystem, 'join'>): string {
477
- return fs.join(outputDir, `${basename(outputDir)}.fairy`);
478
- }
479
-
480
- async function resolvePathForContainment(filePath: string, fs: RestoreFileSystem): Promise<string> {
481
- const missingSegments: string[] = [];
482
- let existingPath = filePath;
483
- while (!(await fs.exists(existingPath))) {
484
- const parentPath = fs.dirname(existingPath);
485
- if (!parentPath || parentPath === existingPath) {
486
- return Promise.resolve(fs.resolvePath(filePath));
487
- }
488
- missingSegments.unshift(basename(existingPath));
489
- existingPath = parentPath;
490
- }
491
-
492
- const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
493
- return missingSegments.reduce((resolvedPath, segment) => fs.join(resolvedPath, segment), resolvedExistingPath);
494
- }
495
-
496
- async function assertRestoreOutputDir(
497
- inputDir: string,
498
- outputDir: string,
499
- fs: RestoreFileSystem,
500
- force: boolean,
501
- ): Promise<void> {
502
- const [resolvedInputDir, resolvedOutputDir] = await Promise.all([
503
- resolvePathForContainment(inputDir, fs),
504
- resolvePathForContainment(outputDir, fs),
505
- ]);
506
- const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
507
- const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
508
- if (
509
- normalizedInputDir === normalizedOutputDir ||
510
- isPathWithin(normalizedInputDir, normalizedOutputDir) ||
511
- isPathWithin(normalizedOutputDir, normalizedInputDir)
512
- ) {
513
- throw new Error('Restore output directory must be independent from the published input directory.');
514
- }
515
-
516
- if (!(await fs.exists(outputDir))) return;
517
-
518
- let entries: string[];
519
- try {
520
- entries = await fs.readdir(outputDir);
521
- } catch {
522
- throw new Error(`Restore output path is not a directory: ${outputDir}`);
523
- }
524
-
525
- if (entries.length === 0) return;
526
- if (!force) {
527
- throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
528
- }
529
- }
530
-
531
- async function createRestoreStagingDir(outputDir: string, fs: RestoreFileSystem): Promise<string> {
532
- const parentDir = fs.dirname(outputDir) || '.';
533
- await fs.mkdir(parentDir);
534
- for (let attempt = 0; attempt < 8; attempt += 1) {
535
- const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${generateId()}`);
536
- if (await fs.exists(stagingDir)) continue;
537
- await fs.mkdir(stagingDir);
538
- return stagingDir;
539
- }
540
- throw new Error(`restore: Could not allocate a staging directory beside ${outputDir}.`);
541
- }
542
-
543
- async function commitRestoreOutput(
544
- stagingDir: string,
545
- outputDir: string,
546
- fs: RestoreFileSystem,
547
- ): Promise<string | null> {
548
- if (!(await fs.exists(outputDir))) {
549
- await fs.rename(stagingDir, outputDir);
550
- return null;
551
- }
552
-
553
- const parentDir = fs.dirname(outputDir) || '.';
554
- let backupDir = '';
555
- for (let attempt = 0; attempt < 8; attempt += 1) {
556
- const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${generateId()}`);
557
- if (!(await fs.exists(candidate))) {
558
- backupDir = candidate;
559
- break;
560
- }
561
- }
562
- if (!backupDir) throw new Error(`restore: Could not allocate a backup directory beside ${outputDir}.`);
563
-
564
- // ponytail: two-step rename preserves rollback; use a platform directory-exchange primitive if zero reader gap matters.
565
- await fs.rename(outputDir, backupDir);
566
- try {
567
- await fs.rename(stagingDir, outputDir);
568
- } catch (error) {
569
- await fs.rename(backupDir, outputDir);
570
- throw error;
571
- }
572
- try {
573
- await fs.rm(backupDir, { recursive: true, force: true });
574
- return null;
575
- } catch {
576
- return `restore: Previous output retained at ${backupDir}; remove it after checking the restored project.`;
577
- }
578
- }
579
-
580
351
  export async function restore(options: RestoreOptions): Promise<RestoreResult> {
581
352
  const sourceDir = trimTrailingSlashes(options.inputDir);
582
353
  const outputDir = normalizeRestoreOutputDir(options.output);
@@ -1166,52 +937,7 @@ class RestoreWorkflow {
1166
937
 
1167
938
  const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
1168
939
  await this._mkdirForFile(outputPath);
1169
- await this._fs.writeFile(outputPath, this._serializeFont(pkg, resource, glyphs));
1170
- }
1171
-
1172
- private _serializeFont(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string {
1173
- const isTtf = resource.getTtf?.() === true;
1174
- const lines = isTtf
1175
- ? this._serializeTtfFontHeader(pkg, resource, glyphs)
1176
- : [
1177
- 'info creator=UIBuilder',
1178
- `common lineHeight=${resource.getLineHeight?.() ?? 0}`,
1179
- ];
1180
-
1181
- for (const glyph of glyphs) {
1182
- const charId = fontGlyphCharId(glyph);
1183
- if (isTtf) {
1184
- lines.push(
1185
- `char id=${charId} x=${glyph.getX()} y=${glyph.getY()} width=${glyph.getWidth()} height=${glyph.getHeight()} `
1186
- + `xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()} page=0 chnl=${glyph.getChannel()}`,
1187
- );
1188
- } else {
1189
- lines.push(
1190
- `char id=${charId} img=${glyph.getImg()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()}`,
1191
- );
1192
- }
1193
- }
1194
-
1195
- return `${lines.join('\n')}\n`;
1196
- }
1197
-
1198
- private _serializeTtfFontHeader(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string[] {
1199
- const fileName = resourceFileName(resource);
1200
- const face = stripExtension(fileName) || resource.getName?.() || 'Font';
1201
- const lineHeight = resource.getLineHeight?.() ?? 0;
1202
- const fontSize = resource.getFontSize?.() ?? lineHeight;
1203
- const textureId = resource.getTextureId?.() ?? '';
1204
- const textureResource = textureId ? pkg.getResourceById(textureId) as RestorableResource | null : null;
1205
- const textureName = textureResource ? resourceFileName(textureResource) : `${face}_atlas.png`;
1206
- const scaleW = textureResource?.getWidth?.() ?? 256;
1207
- const scaleH = textureResource?.getHeight?.() ?? 256;
1208
- const base = Math.max(Math.min(fontSize, lineHeight) - 6, 0);
1209
- return [
1210
- `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`,
1211
- `common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=1 packed=0 alphaChnl=${resource.getTint?.() ? 1 : 0} redChnl=0 greenChnl=0 blueChnl=0`,
1212
- `page id=0 file="${textureName}"`,
1213
- `chars count=${glyphs.length}`,
1214
- ];
940
+ await this._fs.writeFile(outputPath, serializeFont(pkg, resource, glyphs));
1215
941
  }
1216
942
 
1217
943
  private async _writeMovieClipFile(
@@ -1258,7 +984,7 @@ class RestoreWorkflow {
1258
984
 
1259
985
  const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
1260
986
  await this._mkdirForFile(outputPath);
1261
- await this._fs.writeFileRaw(outputPath, this._serializeMovieClip(resource, frames, textures));
987
+ await this._fs.writeFileRaw(outputPath, serializeMovieClip(resource, frames, textures));
1262
988
  }
1263
989
 
1264
990
  private async _writeSyntheticFontGlyphImages(pkg: Package, outputProjectPath: string): Promise<void> {
@@ -1288,47 +1014,6 @@ class RestoreWorkflow {
1288
1014
  return sprites;
1289
1015
  }
1290
1016
 
1291
- private _serializeMovieClip(
1292
- resource: RestorableResource,
1293
- frames: RestorableMovieFrame[],
1294
- textures: Uint8Array[],
1295
- ): Uint8Array {
1296
- const chunks: Uint8Array[] = [
1297
- encodeJtaUtf(JTA_FILE_MARK),
1298
- writeInt32(JTA_VERSION),
1299
- writeByte(0),
1300
- writeByte(0),
1301
- writeByte(0),
1302
- writeByte(0),
1303
- writeUint16(0),
1304
- writeUint16(0),
1305
- writeUint16(resource.getWidth?.() ?? 0),
1306
- writeUint16(resource.getHeight?.() ?? 0),
1307
- writeByte(jtaSpeed(resource.getInterval?.() ?? 0)),
1308
- writeByte(scaledFrameDelay(resource.getRepeatDelay?.() ?? 0)),
1309
- writeByte(resource.getSwing?.() ? 1 : 0),
1310
- writeInt16(frames.length),
1311
- ];
1312
-
1313
- for (const [index, frame] of frames.entries()) {
1314
- chunks.push(
1315
- writeInt16(scaledFrameDelay(frame.getAddDelay())),
1316
- writeInt16(frame.getRectX()),
1317
- writeInt16(frame.getRectY()),
1318
- writeInt16(frame.getRectWidth()),
1319
- writeInt16(frame.getRectHeight()),
1320
- writeInt16(textures[index]?.byteLength === 0 ? -1 : index),
1321
- );
1322
- }
1323
-
1324
- chunks.push(writeInt16(textures.length));
1325
- for (const texture of textures) {
1326
- chunks.push(writeInt32(texture.byteLength), texture);
1327
- }
1328
-
1329
- return concatBytes(chunks);
1330
- }
1331
-
1332
1017
  private _sourceFileCandidates(pkg: Package, fileName: string, outputFileName = fileName): string[] {
1333
1018
  const publishName = pkg.getPublishName() || pkg.getName();
1334
1019
  assertSafeRestoreSegment(publishName, 'package publish name');