@series-inc/stowkit-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/app/blob-store.d.ts +9 -0
  2. package/dist/app/blob-store.js +42 -0
  3. package/dist/app/disk-project.d.ts +84 -0
  4. package/dist/app/disk-project.js +70 -0
  5. package/dist/app/process-cache.d.ts +10 -0
  6. package/dist/app/process-cache.js +126 -0
  7. package/dist/app/state.d.ts +38 -0
  8. package/dist/app/state.js +16 -0
  9. package/dist/app/stowmat-io.d.ts +6 -0
  10. package/dist/app/stowmat-io.js +48 -0
  11. package/dist/app/stowmeta-io.d.ts +14 -0
  12. package/dist/app/stowmeta-io.js +207 -0
  13. package/dist/cleanup.d.ts +3 -0
  14. package/dist/cleanup.js +72 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +148 -0
  17. package/dist/core/binary.d.ts +41 -0
  18. package/dist/core/binary.js +118 -0
  19. package/dist/core/constants.d.ts +64 -0
  20. package/dist/core/constants.js +65 -0
  21. package/dist/core/path.d.ts +3 -0
  22. package/dist/core/path.js +27 -0
  23. package/dist/core/types.d.ts +204 -0
  24. package/dist/core/types.js +76 -0
  25. package/dist/encoders/aac-encoder.d.ts +12 -0
  26. package/dist/encoders/aac-encoder.js +179 -0
  27. package/dist/encoders/basis-encoder.d.ts +15 -0
  28. package/dist/encoders/basis-encoder.js +116 -0
  29. package/dist/encoders/draco-encoder.d.ts +11 -0
  30. package/dist/encoders/draco-encoder.js +155 -0
  31. package/dist/encoders/fbx-loader.d.ts +4 -0
  32. package/dist/encoders/fbx-loader.js +540 -0
  33. package/dist/encoders/image-decoder.d.ts +13 -0
  34. package/dist/encoders/image-decoder.js +33 -0
  35. package/dist/encoders/interfaces.d.ts +105 -0
  36. package/dist/encoders/interfaces.js +1 -0
  37. package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
  38. package/dist/encoders/skinned-mesh-builder.js +135 -0
  39. package/dist/format/metadata.d.ts +18 -0
  40. package/dist/format/metadata.js +381 -0
  41. package/dist/format/packer.d.ts +8 -0
  42. package/dist/format/packer.js +87 -0
  43. package/dist/index.d.ts +28 -0
  44. package/dist/index.js +35 -0
  45. package/dist/init.d.ts +1 -0
  46. package/dist/init.js +73 -0
  47. package/dist/node-fs.d.ts +22 -0
  48. package/dist/node-fs.js +148 -0
  49. package/dist/orchestrator.d.ts +20 -0
  50. package/dist/orchestrator.js +301 -0
  51. package/dist/pipeline.d.ts +23 -0
  52. package/dist/pipeline.js +354 -0
  53. package/dist/server.d.ts +9 -0
  54. package/dist/server.js +859 -0
  55. package/package.json +35 -0
  56. package/skill.md +211 -0
@@ -0,0 +1,204 @@
1
+ export declare enum AssetType {
2
+ Unknown = 0,
3
+ StaticMesh = 1,
4
+ Texture2D = 2,
5
+ Audio = 3,
6
+ MaterialSchema = 4,
7
+ SkinnedMesh = 5,
8
+ AnimationClip = 6
9
+ }
10
+ export declare enum KTX2Quality {
11
+ Fastest = 0,
12
+ Fast = 1,
13
+ Normal = 2,
14
+ High = 3,
15
+ Best = 4
16
+ }
17
+ export declare enum TextureChannelFormat {
18
+ Auto = 0,
19
+ RGB = 1,
20
+ RGBA = 2
21
+ }
22
+ export declare enum TextureResize {
23
+ Full = 0,
24
+ Half = 1,
25
+ Quarter = 2,
26
+ Eighth = 3
27
+ }
28
+ export declare enum MaterialFieldType {
29
+ Texture = 0,
30
+ Color = 1,
31
+ Float = 2,
32
+ Vec2 = 3,
33
+ Vec3 = 4,
34
+ Vec4 = 5,
35
+ Int = 6
36
+ }
37
+ export declare enum PreviewPropertyFlag {
38
+ None = 0,
39
+ MainTex = 1,
40
+ Tint = 2,
41
+ AlphaTest = 3
42
+ }
43
+ export declare enum AacQuality {
44
+ Lowest = 0,
45
+ Low = 1,
46
+ Medium = 2,
47
+ High = 3,
48
+ Best = 4
49
+ }
50
+ export declare enum AudioSampleRate {
51
+ Auto = 0,
52
+ Hz48000 = 48000,
53
+ Hz44100 = 44100,
54
+ Hz22050 = 22050,
55
+ Hz11025 = 11025
56
+ }
57
+ export declare enum DracoQualityPreset {
58
+ Fast = 0,
59
+ Balanced = 1,
60
+ HighQuality = 2,
61
+ MaximumQuality = 3
62
+ }
63
+ export interface FileHeader {
64
+ magic: number;
65
+ version: number;
66
+ assetCount: number;
67
+ directoryOffset: number;
68
+ }
69
+ export interface AssetDirectoryEntry {
70
+ assetUID: bigint;
71
+ assetType: AssetType;
72
+ dataOffset: number;
73
+ dataSize: number;
74
+ metadataOffset: number;
75
+ metadataSize: number;
76
+ }
77
+ export interface TextureMetadata {
78
+ width: number;
79
+ height: number;
80
+ channels: number;
81
+ channelFormat: TextureChannelFormat;
82
+ stringId: string;
83
+ }
84
+ export interface AudioMetadata {
85
+ stringId: string;
86
+ sampleRate: number;
87
+ channels: number;
88
+ durationMs: number;
89
+ }
90
+ export interface MeshGeometryInfo {
91
+ vertexCount: number;
92
+ indexCount: number;
93
+ hasNormals: number;
94
+ hasUvs: number;
95
+ compressedBufferOffset: number;
96
+ compressedBufferSize: number;
97
+ materialIndex: number;
98
+ positionMin?: [number, number, number];
99
+ positionMax?: [number, number, number];
100
+ }
101
+ export interface SceneNode {
102
+ name: string;
103
+ parentIndex: number;
104
+ position: [number, number, number];
105
+ rotation: [number, number, number, number];
106
+ scale: [number, number, number];
107
+ meshCount: number;
108
+ firstMeshIndex: number;
109
+ }
110
+ export interface MaterialPropertyValue {
111
+ fieldName: string;
112
+ value: [number, number, number, number];
113
+ textureId: string;
114
+ }
115
+ export interface MaterialData {
116
+ name: string;
117
+ schemaId: string;
118
+ propertyCount: number;
119
+ properties: MaterialPropertyValue[];
120
+ }
121
+ export interface MaterialSchemaField {
122
+ name: string;
123
+ fieldType: MaterialFieldType;
124
+ previewFlags: PreviewPropertyFlag;
125
+ defaultValue: [number, number, number, number];
126
+ defaultTextureId: string;
127
+ }
128
+ export interface MaterialSchemaMetadata {
129
+ stringId: string;
130
+ schemaName: string;
131
+ fieldCount: number;
132
+ fields: MaterialSchemaField[];
133
+ }
134
+ export interface MeshMetadata {
135
+ meshGeometryCount: number;
136
+ materialCount: number;
137
+ nodeCount: number;
138
+ stringId: string;
139
+ geometries: MeshGeometryInfo[];
140
+ materials: MaterialData[];
141
+ nodes: SceneNode[];
142
+ meshIndices: number[];
143
+ }
144
+ export interface SkinnedMeshGeometryInfo {
145
+ vertexCount: number;
146
+ indexCount: number;
147
+ hasNormals: number;
148
+ hasUvs: number;
149
+ vertexBufferOffset: number;
150
+ vertexBufferSize: number;
151
+ indexBufferOffset: number;
152
+ indexBufferSize: number;
153
+ weightsOffset: number;
154
+ weightsSize: number;
155
+ materialIndex: number;
156
+ _padding: number;
157
+ }
158
+ export interface Bone {
159
+ name: string;
160
+ parentIndex: number;
161
+ offsetMatrix: number[];
162
+ }
163
+ export interface VertexWeights {
164
+ boneIndices: [number, number, number, number];
165
+ weights: [number, number, number, number];
166
+ }
167
+ export interface SkinnedMeshMetadata {
168
+ meshGeometryCount: number;
169
+ materialCount: number;
170
+ nodeCount: number;
171
+ boneCount: number;
172
+ stringId: string;
173
+ geometries: SkinnedMeshGeometryInfo[];
174
+ materials: MaterialData[];
175
+ nodes: SceneNode[];
176
+ meshIndices: number[];
177
+ bones: Bone[];
178
+ }
179
+ export interface AnimationTrackDescriptor {
180
+ trackName: string;
181
+ keyframeCount: number;
182
+ valuesPerKey: number;
183
+ timesOffset: number;
184
+ valuesOffset: number;
185
+ }
186
+ export interface AnimationClipMetadata {
187
+ stringId: string;
188
+ targetMeshId: string;
189
+ metadataVersion: number;
190
+ duration: number;
191
+ trackCount: number;
192
+ tracks: AnimationTrackDescriptor[];
193
+ }
194
+ export interface AssetMetadataHeader {
195
+ tagCsvLength: number;
196
+ }
197
+ export interface PackerAsset {
198
+ canonicalPath: string;
199
+ type: AssetType;
200
+ uid: bigint;
201
+ data: Uint8Array;
202
+ metadata: Uint8Array | null;
203
+ tags: string[];
204
+ }
@@ -0,0 +1,76 @@
1
+ // ─── Asset Type Enum ────────────────────────────────────────────────────────
2
+ export var AssetType;
3
+ (function (AssetType) {
4
+ AssetType[AssetType["Unknown"] = 0] = "Unknown";
5
+ AssetType[AssetType["StaticMesh"] = 1] = "StaticMesh";
6
+ AssetType[AssetType["Texture2D"] = 2] = "Texture2D";
7
+ AssetType[AssetType["Audio"] = 3] = "Audio";
8
+ AssetType[AssetType["MaterialSchema"] = 4] = "MaterialSchema";
9
+ AssetType[AssetType["SkinnedMesh"] = 5] = "SkinnedMesh";
10
+ AssetType[AssetType["AnimationClip"] = 6] = "AnimationClip";
11
+ })(AssetType || (AssetType = {}));
12
+ // ─── Texture Enums ──────────────────────────────────────────────────────────
13
+ export var KTX2Quality;
14
+ (function (KTX2Quality) {
15
+ KTX2Quality[KTX2Quality["Fastest"] = 0] = "Fastest";
16
+ KTX2Quality[KTX2Quality["Fast"] = 1] = "Fast";
17
+ KTX2Quality[KTX2Quality["Normal"] = 2] = "Normal";
18
+ KTX2Quality[KTX2Quality["High"] = 3] = "High";
19
+ KTX2Quality[KTX2Quality["Best"] = 4] = "Best";
20
+ })(KTX2Quality || (KTX2Quality = {}));
21
+ export var TextureChannelFormat;
22
+ (function (TextureChannelFormat) {
23
+ TextureChannelFormat[TextureChannelFormat["Auto"] = 0] = "Auto";
24
+ TextureChannelFormat[TextureChannelFormat["RGB"] = 1] = "RGB";
25
+ TextureChannelFormat[TextureChannelFormat["RGBA"] = 2] = "RGBA";
26
+ })(TextureChannelFormat || (TextureChannelFormat = {}));
27
+ export var TextureResize;
28
+ (function (TextureResize) {
29
+ TextureResize[TextureResize["Full"] = 0] = "Full";
30
+ TextureResize[TextureResize["Half"] = 1] = "Half";
31
+ TextureResize[TextureResize["Quarter"] = 2] = "Quarter";
32
+ TextureResize[TextureResize["Eighth"] = 3] = "Eighth";
33
+ })(TextureResize || (TextureResize = {}));
34
+ // ─── Material Enums ─────────────────────────────────────────────────────────
35
+ export var MaterialFieldType;
36
+ (function (MaterialFieldType) {
37
+ MaterialFieldType[MaterialFieldType["Texture"] = 0] = "Texture";
38
+ MaterialFieldType[MaterialFieldType["Color"] = 1] = "Color";
39
+ MaterialFieldType[MaterialFieldType["Float"] = 2] = "Float";
40
+ MaterialFieldType[MaterialFieldType["Vec2"] = 3] = "Vec2";
41
+ MaterialFieldType[MaterialFieldType["Vec3"] = 4] = "Vec3";
42
+ MaterialFieldType[MaterialFieldType["Vec4"] = 5] = "Vec4";
43
+ MaterialFieldType[MaterialFieldType["Int"] = 6] = "Int";
44
+ })(MaterialFieldType || (MaterialFieldType = {}));
45
+ export var PreviewPropertyFlag;
46
+ (function (PreviewPropertyFlag) {
47
+ PreviewPropertyFlag[PreviewPropertyFlag["None"] = 0] = "None";
48
+ PreviewPropertyFlag[PreviewPropertyFlag["MainTex"] = 1] = "MainTex";
49
+ PreviewPropertyFlag[PreviewPropertyFlag["Tint"] = 2] = "Tint";
50
+ PreviewPropertyFlag[PreviewPropertyFlag["AlphaTest"] = 3] = "AlphaTest";
51
+ })(PreviewPropertyFlag || (PreviewPropertyFlag = {}));
52
+ // ─── Audio Enums ────────────────────────────────────────────────────────
53
+ export var AacQuality;
54
+ (function (AacQuality) {
55
+ AacQuality[AacQuality["Lowest"] = 0] = "Lowest";
56
+ AacQuality[AacQuality["Low"] = 1] = "Low";
57
+ AacQuality[AacQuality["Medium"] = 2] = "Medium";
58
+ AacQuality[AacQuality["High"] = 3] = "High";
59
+ AacQuality[AacQuality["Best"] = 4] = "Best";
60
+ })(AacQuality || (AacQuality = {}));
61
+ export var AudioSampleRate;
62
+ (function (AudioSampleRate) {
63
+ AudioSampleRate[AudioSampleRate["Auto"] = 0] = "Auto";
64
+ AudioSampleRate[AudioSampleRate["Hz48000"] = 48000] = "Hz48000";
65
+ AudioSampleRate[AudioSampleRate["Hz44100"] = 44100] = "Hz44100";
66
+ AudioSampleRate[AudioSampleRate["Hz22050"] = 22050] = "Hz22050";
67
+ AudioSampleRate[AudioSampleRate["Hz11025"] = 11025] = "Hz11025";
68
+ })(AudioSampleRate || (AudioSampleRate = {}));
69
+ // ─── Mesh Enums ─────────────────────────────────────────────────────────────
70
+ export var DracoQualityPreset;
71
+ (function (DracoQualityPreset) {
72
+ DracoQualityPreset[DracoQualityPreset["Fast"] = 0] = "Fast";
73
+ DracoQualityPreset[DracoQualityPreset["Balanced"] = 1] = "Balanced";
74
+ DracoQualityPreset[DracoQualityPreset["HighQuality"] = 2] = "HighQuality";
75
+ DracoQualityPreset[DracoQualityPreset["MaximumQuality"] = 3] = "MaximumQuality";
76
+ })(DracoQualityPreset || (DracoQualityPreset = {}));
@@ -0,0 +1,12 @@
1
+ import { AacQuality } from '../core/types.js';
2
+ import type { IAacEncoder, IAudioDecoder, DecodedPcm } from './interfaces.js';
3
+ export declare class NodeAacEncoder implements IAacEncoder {
4
+ private ffmpegPath;
5
+ initialize(): Promise<void>;
6
+ encode(channels: Float32Array[], sampleRate: number, quality: AacQuality): Promise<Uint8Array>;
7
+ }
8
+ export declare class NodeAudioDecoder implements IAudioDecoder {
9
+ private ffmpegPath;
10
+ initialize(): Promise<void>;
11
+ decodeToPcm(audioData: Uint8Array, _fileName: string): Promise<DecodedPcm>;
12
+ }
@@ -0,0 +1,179 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { AacQuality } from '../core/types.js';
7
+ const execFileAsync = promisify(execFile);
8
+ // ─── Quality mapping ────────────────────────────────────────────────────────
9
+ const QUALITY_TO_VBR = {
10
+ [AacQuality.Lowest]: '0.1',
11
+ [AacQuality.Low]: '0.25',
12
+ [AacQuality.Medium]: '0.4',
13
+ [AacQuality.High]: '0.8',
14
+ [AacQuality.Best]: '1.3',
15
+ };
16
+ // ─── WAV builder ─────────────────────────────────────────────────────────────
17
+ function createWavBuffer(channels, sampleRate) {
18
+ const numChannels = channels.length;
19
+ const numSamples = channels[0].length;
20
+ const bytesPerSample = 4;
21
+ const dataSize = numSamples * numChannels * bytesPerSample;
22
+ const headerSize = 44;
23
+ const buffer = new ArrayBuffer(headerSize + dataSize);
24
+ const view = new DataView(buffer);
25
+ view.setUint32(0, 0x52494646, false); // "RIFF"
26
+ view.setUint32(4, 36 + dataSize, true);
27
+ view.setUint32(8, 0x57415645, false); // "WAVE"
28
+ view.setUint32(12, 0x666D7420, false); // "fmt "
29
+ view.setUint32(16, 16, true);
30
+ view.setUint16(20, 3, true); // IEEE float
31
+ view.setUint16(22, numChannels, true);
32
+ view.setUint32(24, sampleRate, true);
33
+ view.setUint32(28, sampleRate * numChannels * bytesPerSample, true);
34
+ view.setUint16(32, numChannels * bytesPerSample, true);
35
+ view.setUint16(34, 32, true);
36
+ view.setUint32(36, 0x64617461, false); // "data"
37
+ view.setUint32(40, dataSize, true);
38
+ let offset = headerSize;
39
+ for (let i = 0; i < numSamples; i++) {
40
+ for (let ch = 0; ch < numChannels; ch++) {
41
+ view.setFloat32(offset, channels[ch][i], true);
42
+ offset += 4;
43
+ }
44
+ }
45
+ return new Uint8Array(buffer);
46
+ }
47
+ // ─── Resolve ffmpeg binary ──────────────────────────────────────────────────
48
+ async function getFfmpegPath() {
49
+ try {
50
+ // Try ffmpeg-static first
51
+ const mod = await import('ffmpeg-static');
52
+ const p = (mod.default ?? mod);
53
+ if (p)
54
+ return p;
55
+ }
56
+ catch {
57
+ // Fallback to PATH
58
+ }
59
+ return 'ffmpeg';
60
+ }
61
+ // ─── NodeAacEncoder ─────────────────────────────────────────────────────────
62
+ export class NodeAacEncoder {
63
+ ffmpegPath = null;
64
+ async initialize() {
65
+ this.ffmpegPath = await getFfmpegPath();
66
+ }
67
+ async encode(channels, sampleRate, quality) {
68
+ if (!this.ffmpegPath)
69
+ await this.initialize();
70
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'stowkit-'));
71
+ const inFile = path.join(tmpDir, 'in.wav');
72
+ const outFile = path.join(tmpDir, 'out.m4a');
73
+ try {
74
+ const wavBytes = createWavBuffer(channels, sampleRate);
75
+ await fs.writeFile(inFile, wavBytes);
76
+ const vbr = QUALITY_TO_VBR[quality];
77
+ await execFileAsync(this.ffmpegPath, [
78
+ '-y',
79
+ '-i', inFile,
80
+ '-c:a', 'aac',
81
+ '-q:a', vbr,
82
+ outFile,
83
+ ]);
84
+ const result = await fs.readFile(outFile);
85
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
86
+ }
87
+ finally {
88
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
89
+ }
90
+ }
91
+ }
92
+ // ─── NodeAudioDecoder ───────────────────────────────────────────────────────
93
+ export class NodeAudioDecoder {
94
+ ffmpegPath = null;
95
+ async initialize() {
96
+ this.ffmpegPath = await getFfmpegPath();
97
+ }
98
+ async decodeToPcm(audioData, _fileName) {
99
+ if (!this.ffmpegPath)
100
+ await this.initialize();
101
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'stowkit-'));
102
+ const inFile = path.join(tmpDir, 'input');
103
+ const outFile = path.join(tmpDir, 'output.raw');
104
+ try {
105
+ await fs.writeFile(inFile, audioData);
106
+ // First, get audio info
107
+ const { stdout } = await execFileAsync(this.ffmpegPath, [
108
+ '-i', inFile,
109
+ '-hide_banner',
110
+ ], { encoding: 'utf-8' }).catch(e => {
111
+ // ffmpeg exits non-zero when only probing, but prints info to stderr
112
+ return { stdout: '', stderr: e.stderr ?? '' };
113
+ });
114
+ const infoText = stdout || '';
115
+ // Parse sample rate and channels from ffmpeg output
116
+ const sampleRateMatch = infoText.match(/(\d+) Hz/);
117
+ const channelsMatch = infoText.match(/(\d+) channels/) || infoText.match(/(mono|stereo)/i);
118
+ // Use ffprobe-style approach: decode to raw PCM f32le
119
+ await execFileAsync(this.ffmpegPath, [
120
+ '-y',
121
+ '-i', inFile,
122
+ '-f', 'f32le',
123
+ '-acodec', 'pcm_f32le',
124
+ outFile,
125
+ ]);
126
+ // Get the actual sample rate and channel count via a second pass
127
+ const probeResult = await execFileAsync(this.ffmpegPath, [
128
+ '-i', inFile,
129
+ '-show_entries', 'stream=sample_rate,channels',
130
+ '-of', 'json',
131
+ '-v', 'quiet',
132
+ ]).catch(() => null);
133
+ let sampleRate = 44100;
134
+ let numChannels = 2;
135
+ if (probeResult) {
136
+ try {
137
+ const info = JSON.parse(probeResult.stdout);
138
+ if (info.streams?.[0]) {
139
+ sampleRate = parseInt(info.streams[0].sample_rate) || 44100;
140
+ numChannels = info.streams[0].channels || 2;
141
+ }
142
+ }
143
+ catch {
144
+ // Use ffmpeg stderr parsing fallback
145
+ if (sampleRateMatch)
146
+ sampleRate = parseInt(sampleRateMatch[1]);
147
+ if (channelsMatch) {
148
+ if (channelsMatch[1] === 'mono')
149
+ numChannels = 1;
150
+ else if (channelsMatch[1] === 'stereo')
151
+ numChannels = 2;
152
+ else
153
+ numChannels = parseInt(channelsMatch[1]) || 2;
154
+ }
155
+ }
156
+ }
157
+ const rawBuf = await fs.readFile(outFile);
158
+ const rawF32 = new Float32Array(rawBuf.buffer, rawBuf.byteOffset, rawBuf.byteLength / 4);
159
+ const numSamples = Math.floor(rawF32.length / numChannels);
160
+ // Deinterleave
161
+ const channels = [];
162
+ for (let ch = 0; ch < numChannels; ch++) {
163
+ const channelData = new Float32Array(numSamples);
164
+ for (let i = 0; i < numSamples; i++) {
165
+ channelData[i] = rawF32[i * numChannels + ch];
166
+ }
167
+ channels.push(channelData);
168
+ }
169
+ return {
170
+ channels,
171
+ sampleRate,
172
+ durationMs: Math.round((numSamples / sampleRate) * 1000),
173
+ };
174
+ }
175
+ finally {
176
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,15 @@
1
+ import type { ITextureEncoder, TextureEncodeResult } from './interfaces.js';
2
+ import { KTX2Quality, TextureChannelFormat } from '../core/types.js';
3
+ /**
4
+ * Node.js Basis Universal texture encoder.
5
+ * Loads the Basis WASM encoder from the stowkit-packer-gui/public/wasm/ directory.
6
+ */
7
+ export declare class NodeBasisEncoder implements ITextureEncoder {
8
+ private basis;
9
+ private ready;
10
+ private wasmDir;
11
+ constructor(wasmDir?: string);
12
+ isReady(): boolean;
13
+ initialize(): Promise<void>;
14
+ encode(pixels: Uint8Array, width: number, height: number, channels: number, quality: KTX2Quality, channelFormat: TextureChannelFormat, generateMipmaps?: boolean): Promise<TextureEncodeResult>;
15
+ }
@@ -0,0 +1,116 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createRequire } from 'node:module';
5
+ import { KTX2Quality, TextureChannelFormat } from '../core/types.js';
6
+ const QUALITY_TO_LEVEL = {
7
+ [KTX2Quality.Fastest]: 1,
8
+ [KTX2Quality.Fast]: 64,
9
+ [KTX2Quality.Normal]: 128,
10
+ [KTX2Quality.High]: 192,
11
+ [KTX2Quality.Best]: 255,
12
+ };
13
+ function hasAlpha(rgba, pixelCount) {
14
+ for (let i = 0; i < pixelCount; i++) {
15
+ if (rgba[i * 4 + 3] < 255)
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ function rgbToRgba(rgb, pixelCount) {
21
+ const rgba = new Uint8Array(pixelCount * 4);
22
+ for (let i = 0; i < pixelCount; i++) {
23
+ rgba[i * 4] = rgb[i * 3];
24
+ rgba[i * 4 + 1] = rgb[i * 3 + 1];
25
+ rgba[i * 4 + 2] = rgb[i * 3 + 2];
26
+ rgba[i * 4 + 3] = 255;
27
+ }
28
+ return rgba;
29
+ }
30
+ /**
31
+ * Node.js Basis Universal texture encoder.
32
+ * Loads the Basis WASM encoder from the stowkit-packer-gui/public/wasm/ directory.
33
+ */
34
+ export class NodeBasisEncoder {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ basis = null;
37
+ ready = false;
38
+ wasmDir;
39
+ constructor(wasmDir) {
40
+ // Default: look for wasm files in stowkit-packer-gui/public/wasm/ relative to monorepo root
41
+ this.wasmDir = wasmDir ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../stowkit-packer-gui/public/wasm');
42
+ }
43
+ isReady() {
44
+ return this.ready;
45
+ }
46
+ async initialize() {
47
+ const jsPath = path.join(this.wasmDir, 'basis_encoder.js');
48
+ const jsSource = fs.readFileSync(jsPath, 'utf-8');
49
+ // The basis_encoder.js glue expects CJS globals. Provide them in our ESM context.
50
+ const nodeRequire = createRequire(jsPath);
51
+ const fn = new Function('require', '__dirname', '__filename', 'module', 'exports', jsSource + '\nreturn typeof BASIS !== "undefined" ? BASIS : globalThis.BASIS;');
52
+ const moduleShim = { exports: {} };
53
+ const BASIS = fn(nodeRequire, this.wasmDir, jsPath, moduleShim, moduleShim.exports);
54
+ if (!BASIS || typeof BASIS !== 'function') {
55
+ throw new Error('BASIS factory not found in basis_encoder.js');
56
+ }
57
+ this.basis = await BASIS({
58
+ locateFile: (f) => path.join(this.wasmDir, f),
59
+ });
60
+ this.basis.initializeBasis();
61
+ this.ready = true;
62
+ }
63
+ async encode(pixels, width, height, channels, quality, channelFormat, generateMipmaps = false) {
64
+ if (!this.basis || !this.ready) {
65
+ throw new Error('Basis encoder not initialized');
66
+ }
67
+ const pixelCount = width * height;
68
+ const rgba = channels === 3 ? rgbToRgba(pixels, pixelCount) :
69
+ channels === 4 ? pixels :
70
+ (() => { throw new Error(`Unsupported channel count: ${channels}`); })();
71
+ const useUastc = channelFormat === TextureChannelFormat.RGBA ||
72
+ (channelFormat === TextureChannelFormat.Auto &&
73
+ channels === 4 &&
74
+ hasAlpha(rgba, pixelCount));
75
+ const enc = new this.basis.BasisEncoder();
76
+ try {
77
+ enc.setSliceSourceImage(0, rgba, width, height, false);
78
+ enc.setCreateKTX2File(true);
79
+ enc.setPerceptual(true);
80
+ enc.setMipGen(generateMipmaps);
81
+ enc.setUASTC(useUastc);
82
+ enc.setCheckForAlpha(useUastc);
83
+ if (useUastc) {
84
+ if (typeof enc.setPackUASTCFlags === 'function') {
85
+ enc.setPackUASTCFlags(quality);
86
+ }
87
+ enc.setKTX2UASTCSupercompression(true);
88
+ }
89
+ else {
90
+ enc.setQualityLevel(QUALITY_TO_LEVEL[quality]);
91
+ enc.setKTX2UASTCSupercompression(false);
92
+ if (typeof enc.setNoSelectorRDO === 'function')
93
+ enc.setNoSelectorRDO(true);
94
+ if (typeof enc.setNoEndpointRDO === 'function')
95
+ enc.setNoEndpointRDO(true);
96
+ }
97
+ const maxSize = width * height * 4 + 1048576;
98
+ const dst = new Uint8Array(maxSize);
99
+ const n = enc.encode(dst);
100
+ if (n === 0)
101
+ throw new Error('Basis encoding returned 0 bytes');
102
+ const ktx2Data = dst.slice(0, n);
103
+ const metadata = {
104
+ width,
105
+ height,
106
+ channels: useUastc ? 4 : 3,
107
+ channelFormat: useUastc ? TextureChannelFormat.RGBA : TextureChannelFormat.RGB,
108
+ stringId: '',
109
+ };
110
+ return { data: ktx2Data, metadata };
111
+ }
112
+ finally {
113
+ enc.delete();
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,11 @@
1
+ import type { IMeshEncoder, MeshEncodeSettings, MeshEncodeResult, ImportedMesh } from './interfaces.js';
2
+ import { DracoQualityPreset } from '../core/types.js';
3
+ export declare function dracoPresetToSettings(preset: DracoQualityPreset): MeshEncodeSettings;
4
+ export declare class NodeDracoEncoder implements IMeshEncoder {
5
+ private draco;
6
+ private ready;
7
+ isReady(): boolean;
8
+ initialize(): Promise<void>;
9
+ encode(mesh: ImportedMesh, settings: MeshEncodeSettings): Promise<MeshEncodeResult>;
10
+ private encodeSubMesh;
11
+ }