@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,100 @@
|
|
|
1
|
+
import type { Package } from '@openfairygui/core';
|
|
2
|
+
|
|
3
|
+
export interface RestorableFontGlyph {
|
|
4
|
+
getAdvance(): number;
|
|
5
|
+
getChannel(): number;
|
|
6
|
+
getChar(): string;
|
|
7
|
+
getCharId(): number;
|
|
8
|
+
getHeight(): number;
|
|
9
|
+
getImg(): string;
|
|
10
|
+
getWidth(): number;
|
|
11
|
+
getX(): number;
|
|
12
|
+
getXOffset(): number;
|
|
13
|
+
getY(): number;
|
|
14
|
+
getYOffset(): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RestorableFontResource {
|
|
18
|
+
getFile?(): string;
|
|
19
|
+
getFileName?(): string;
|
|
20
|
+
getFontSize?(): number;
|
|
21
|
+
getLineHeight?(): number;
|
|
22
|
+
getName?(): string;
|
|
23
|
+
getTextureId?(): string;
|
|
24
|
+
getTtf?(): boolean;
|
|
25
|
+
getTint?(): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RestorableTextureResource {
|
|
29
|
+
getFile?(): string;
|
|
30
|
+
getFileName?(): string;
|
|
31
|
+
getHeight?(): number;
|
|
32
|
+
getName?(): string;
|
|
33
|
+
getWidth?(): number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resourceFileName(resource: RestorableFontResource | RestorableTextureResource): string {
|
|
37
|
+
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stripExtension(fileName: string): string {
|
|
41
|
+
return fileName.split(/[\\/]/).pop()?.replace(/\.[^.]+$/u, '') ?? '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fontGlyphCharId(glyph: RestorableFontGlyph): number {
|
|
45
|
+
const charId = glyph.getCharId();
|
|
46
|
+
if (charId > 0) return charId;
|
|
47
|
+
const char = glyph.getChar();
|
|
48
|
+
return char ? (char.codePointAt(0) ?? 0) : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serializeTtfFontHeader(
|
|
52
|
+
pkg: Package,
|
|
53
|
+
resource: RestorableFontResource,
|
|
54
|
+
glyphs: RestorableFontGlyph[],
|
|
55
|
+
): string[] {
|
|
56
|
+
const fileName = resourceFileName(resource);
|
|
57
|
+
const face = stripExtension(fileName) || resource.getName?.() || 'Font';
|
|
58
|
+
const lineHeight = resource.getLineHeight?.() ?? 0;
|
|
59
|
+
const fontSize = resource.getFontSize?.() ?? lineHeight;
|
|
60
|
+
const textureId = resource.getTextureId?.() ?? '';
|
|
61
|
+
const textureResource = textureId
|
|
62
|
+
? (pkg.getResourceById(textureId) as RestorableTextureResource | null)
|
|
63
|
+
: null;
|
|
64
|
+
const textureName = textureResource ? resourceFileName(textureResource) : `${face}_atlas.png`;
|
|
65
|
+
const scaleW = textureResource?.getWidth?.() ?? 256;
|
|
66
|
+
const scaleH = textureResource?.getHeight?.() ?? 256;
|
|
67
|
+
const base = Math.max(Math.min(fontSize, lineHeight) - 6, 0);
|
|
68
|
+
return [
|
|
69
|
+
`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`,
|
|
70
|
+
`common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=1 packed=0 alphaChnl=${resource.getTint?.() ? 1 : 0} redChnl=0 greenChnl=0 blueChnl=0`,
|
|
71
|
+
`page id=0 file="${textureName}"`,
|
|
72
|
+
`chars count=${glyphs.length}`,
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function serializeFont(
|
|
77
|
+
pkg: Package,
|
|
78
|
+
resource: RestorableFontResource,
|
|
79
|
+
glyphs: RestorableFontGlyph[],
|
|
80
|
+
): string {
|
|
81
|
+
const isTtf = resource.getTtf?.() === true;
|
|
82
|
+
const lines = isTtf
|
|
83
|
+
? serializeTtfFontHeader(pkg, resource, glyphs)
|
|
84
|
+
: ['info creator=UIBuilder', `common lineHeight=${resource.getLineHeight?.() ?? 0}`];
|
|
85
|
+
|
|
86
|
+
for (const glyph of glyphs) {
|
|
87
|
+
const charId = fontGlyphCharId(glyph);
|
|
88
|
+
if (isTtf) {
|
|
89
|
+
lines.push(
|
|
90
|
+
`char id=${charId} x=${glyph.getX()} y=${glyph.getY()} width=${glyph.getWidth()} height=${glyph.getHeight()} `
|
|
91
|
+
+ `xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()} page=0 chnl=${glyph.getChannel()}`,
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(
|
|
95
|
+
`char id=${charId} img=${glyph.getImg()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join('\n')}\n`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const JTA_FILE_MARK = 'yytou';
|
|
2
|
+
const JTA_VERSION = 102;
|
|
3
|
+
const JTA_DEFAULT_FPS = 24;
|
|
4
|
+
|
|
5
|
+
export interface RestorableMovieFrame {
|
|
6
|
+
getRectX(): number;
|
|
7
|
+
getRectY(): number;
|
|
8
|
+
getRectWidth(): number;
|
|
9
|
+
getRectHeight(): number;
|
|
10
|
+
getAddDelay(): number;
|
|
11
|
+
getSpriteId(): string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RestorableMovieClipResource {
|
|
15
|
+
getHeight?(): number;
|
|
16
|
+
getInterval?(): number;
|
|
17
|
+
getRepeatDelay?(): number;
|
|
18
|
+
getSwing?(): boolean;
|
|
19
|
+
getWidth?(): number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scaledFrameDelay(milliseconds: number): number {
|
|
23
|
+
return milliseconds <= 0 ? 0 : Math.max(1, Math.round(milliseconds / (1000 / JTA_DEFAULT_FPS)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function jtaSpeed(interval: number): number {
|
|
27
|
+
return interval <= 0 ? 1 : Math.max(1, Math.round(interval / (1000 / JTA_DEFAULT_FPS)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeInt16(value: number): Uint8Array {
|
|
31
|
+
const data = new Uint8Array(2);
|
|
32
|
+
new DataView(data.buffer).setInt16(0, value);
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeUint16(value: number): Uint8Array {
|
|
37
|
+
const data = new Uint8Array(2);
|
|
38
|
+
new DataView(data.buffer).setUint16(0, value);
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeInt32(value: number): Uint8Array {
|
|
43
|
+
const data = new Uint8Array(4);
|
|
44
|
+
new DataView(data.buffer).setInt32(0, value);
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeByte(value: number): Uint8Array {
|
|
49
|
+
return new Uint8Array([value & 0xff]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
53
|
+
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
54
|
+
const data = new Uint8Array(length);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
data.set(chunk, offset);
|
|
58
|
+
offset += chunk.byteLength;
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function encodeJtaUtf(value: string): Uint8Array {
|
|
64
|
+
const bytes = new TextEncoder().encode(value);
|
|
65
|
+
return concatBytes([writeUint16(bytes.byteLength), bytes]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function serializeMovieClip(
|
|
69
|
+
resource: RestorableMovieClipResource,
|
|
70
|
+
frames: RestorableMovieFrame[],
|
|
71
|
+
textures: Uint8Array[],
|
|
72
|
+
): Uint8Array {
|
|
73
|
+
const chunks: Uint8Array[] = [
|
|
74
|
+
encodeJtaUtf(JTA_FILE_MARK),
|
|
75
|
+
writeInt32(JTA_VERSION),
|
|
76
|
+
writeByte(0),
|
|
77
|
+
writeByte(0),
|
|
78
|
+
writeByte(0),
|
|
79
|
+
writeByte(0),
|
|
80
|
+
writeUint16(0),
|
|
81
|
+
writeUint16(0),
|
|
82
|
+
writeUint16(resource.getWidth?.() ?? 0),
|
|
83
|
+
writeUint16(resource.getHeight?.() ?? 0),
|
|
84
|
+
writeByte(jtaSpeed(resource.getInterval?.() ?? 0)),
|
|
85
|
+
writeByte(scaledFrameDelay(resource.getRepeatDelay?.() ?? 0)),
|
|
86
|
+
writeByte(resource.getSwing?.() ? 1 : 0),
|
|
87
|
+
writeInt16(frames.length),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const [index, frame] of frames.entries()) {
|
|
91
|
+
chunks.push(
|
|
92
|
+
writeInt16(scaledFrameDelay(frame.getAddDelay())),
|
|
93
|
+
writeInt16(frame.getRectX()),
|
|
94
|
+
writeInt16(frame.getRectY()),
|
|
95
|
+
writeInt16(frame.getRectWidth()),
|
|
96
|
+
writeInt16(frame.getRectHeight()),
|
|
97
|
+
writeInt16(textures[index]?.byteLength === 0 ? -1 : index),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
chunks.push(writeInt16(textures.length));
|
|
102
|
+
for (const texture of textures) chunks.push(writeInt32(texture.byteLength), texture);
|
|
103
|
+
return concatBytes(chunks);
|
|
104
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { generateId } from '@openfairygui/core';
|
|
2
|
+
import type { RestoreFileSystem } from '../restore.js';
|
|
3
|
+
|
|
4
|
+
export function trimTrailingSlashes(value: string): string {
|
|
5
|
+
return value.replace(/[/\\]+$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeComparablePath(value: string): string {
|
|
9
|
+
const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
|
|
10
|
+
const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
|
|
11
|
+
const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
|
|
12
|
+
const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
|
|
13
|
+
const hasRoot = driveMatch ? true : remainder.startsWith('/');
|
|
14
|
+
const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
|
|
15
|
+
const segments: string[] = [];
|
|
16
|
+
|
|
17
|
+
for (const segment of rawSegments) {
|
|
18
|
+
if (segment === '.') continue;
|
|
19
|
+
if (segment === '..') {
|
|
20
|
+
if (segments.length > 0 && segments[segments.length - 1] !== '..') {
|
|
21
|
+
segments.pop();
|
|
22
|
+
} else if (!hasRoot) {
|
|
23
|
+
segments.push('..');
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
segments.push(segment);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const joined = segments.join('/');
|
|
31
|
+
const comparable = drivePrefix
|
|
32
|
+
? `${drivePrefix}/${joined}`.replace(/\/$/, '')
|
|
33
|
+
: hasRoot
|
|
34
|
+
? `/${joined}`.replace(/\/$/, '')
|
|
35
|
+
: joined || '.';
|
|
36
|
+
// Restore prefers a conservative same-directory guard: false positives are safer than
|
|
37
|
+
// missing a Windows-style case-only path alias and deleting the source publish dir.
|
|
38
|
+
return comparable.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isPathWithin(root: string, candidate: string): boolean {
|
|
42
|
+
const normalizedRoot = normalizeComparablePath(root);
|
|
43
|
+
const normalizedCandidate = normalizeComparablePath(candidate);
|
|
44
|
+
return normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function basename(filePath: string): string {
|
|
48
|
+
const trimmed = trimTrailingSlashes(filePath);
|
|
49
|
+
const match = trimmed.match(/([^/\\]+)$/);
|
|
50
|
+
return match?.[1] ?? '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeRestoreOutputDir(output: string): string {
|
|
54
|
+
const normalized = trimTrailingSlashes(output);
|
|
55
|
+
const name = basename(normalized);
|
|
56
|
+
if (!normalized || /\.fairy$/i.test(normalized) || !name || name === '.' || name === '..' || /^[a-z]:$/iu.test(name)) {
|
|
57
|
+
throw new Error('restore: Output must be a non-root project directory, not a .fairy file.');
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveOutputProjectPath(outputDir: string, fs: Pick<RestoreFileSystem, 'join'>): string {
|
|
63
|
+
return fs.join(outputDir, `${basename(outputDir)}.fairy`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolvePathForContainment(filePath: string, fs: RestoreFileSystem): Promise<string> {
|
|
67
|
+
const missingSegments: string[] = [];
|
|
68
|
+
let existingPath = filePath;
|
|
69
|
+
while (!(await fs.exists(existingPath))) {
|
|
70
|
+
const parentPath = fs.dirname(existingPath);
|
|
71
|
+
if (!parentPath || parentPath === existingPath) {
|
|
72
|
+
return Promise.resolve(fs.resolvePath(filePath));
|
|
73
|
+
}
|
|
74
|
+
missingSegments.unshift(basename(existingPath));
|
|
75
|
+
existingPath = parentPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
|
|
79
|
+
return missingSegments.reduce((resolvedPath, segment) => fs.join(resolvedPath, segment), resolvedExistingPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function assertRestoreOutputDir(
|
|
83
|
+
inputDir: string,
|
|
84
|
+
outputDir: string,
|
|
85
|
+
fs: RestoreFileSystem,
|
|
86
|
+
force: boolean,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([
|
|
89
|
+
resolvePathForContainment(inputDir, fs),
|
|
90
|
+
resolvePathForContainment(outputDir, fs),
|
|
91
|
+
]);
|
|
92
|
+
const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
|
|
93
|
+
const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
|
|
94
|
+
if (
|
|
95
|
+
normalizedInputDir === normalizedOutputDir ||
|
|
96
|
+
isPathWithin(normalizedInputDir, normalizedOutputDir) ||
|
|
97
|
+
isPathWithin(normalizedOutputDir, normalizedInputDir)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error('Restore output directory must be independent from the published input directory.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!(await fs.exists(outputDir))) return;
|
|
103
|
+
|
|
104
|
+
let entries: string[];
|
|
105
|
+
try {
|
|
106
|
+
entries = await fs.readdir(outputDir);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (entries.length === 0) return;
|
|
112
|
+
if (!force) {
|
|
113
|
+
throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function createRestoreStagingDir(outputDir: string, fs: RestoreFileSystem): Promise<string> {
|
|
118
|
+
const parentDir = fs.dirname(outputDir) || '.';
|
|
119
|
+
await fs.mkdir(parentDir);
|
|
120
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
121
|
+
const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${generateId()}`);
|
|
122
|
+
if (await fs.exists(stagingDir)) continue;
|
|
123
|
+
await fs.mkdir(stagingDir);
|
|
124
|
+
return stagingDir;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`restore: Could not allocate a staging directory beside ${outputDir}.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function commitRestoreOutput(
|
|
130
|
+
stagingDir: string,
|
|
131
|
+
outputDir: string,
|
|
132
|
+
fs: RestoreFileSystem,
|
|
133
|
+
): Promise<string | null> {
|
|
134
|
+
if (!(await fs.exists(outputDir))) {
|
|
135
|
+
await fs.rename(stagingDir, outputDir);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const parentDir = fs.dirname(outputDir) || '.';
|
|
140
|
+
let backupDir = '';
|
|
141
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
142
|
+
const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${generateId()}`);
|
|
143
|
+
if (!(await fs.exists(candidate))) {
|
|
144
|
+
backupDir = candidate;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!backupDir) throw new Error(`restore: Could not allocate a backup directory beside ${outputDir}.`);
|
|
149
|
+
|
|
150
|
+
// ponytail: two-step rename preserves rollback; use a platform directory-exchange primitive if zero reader gap matters.
|
|
151
|
+
await fs.rename(outputDir, backupDir);
|
|
152
|
+
try {
|
|
153
|
+
await fs.rename(stagingDir, outputDir);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
await fs.rename(backupDir, outputDir);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
160
|
+
return null;
|
|
161
|
+
} catch {
|
|
162
|
+
return `restore: Previous output retained at ${backupDir}; remove it after checking the restored project.`;
|
|
163
|
+
}
|
|
164
|
+
}
|