@needle-tools/gltf-build-pipeline 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 (69) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/clear-caches.mjs +4 -0
  3. package/defaults.mjs +12 -0
  4. package/dist/cache/cache.d.ts +5 -0
  5. package/dist/cache/cache.js +82 -0
  6. package/dist/cache/index.d.ts +1 -0
  7. package/dist/cache/index.js +1 -0
  8. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -0
  9. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -0
  10. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -0
  11. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -0
  12. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +23 -0
  13. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +43 -0
  14. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -0
  15. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -0
  16. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +20 -0
  17. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +699 -0
  18. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -0
  19. package/dist/extensions/NEEDLE_opaque/index.js +18 -0
  20. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +23 -0
  21. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +53 -0
  22. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -0
  23. package/dist/extensions/NEEDLE_progressive/index.js +1 -0
  24. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +21 -0
  25. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +39 -0
  26. package/dist/extensions/index.d.ts +6 -0
  27. package/dist/extensions/index.js +16 -0
  28. package/dist/extensions/utils.d.ts +6 -0
  29. package/dist/extensions/utils.js +72 -0
  30. package/dist/functions/list-texture-slots.d.ts +1 -0
  31. package/dist/functions/list-texture-slots.js +35 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +3 -0
  34. package/dist/transforms/index.d.ts +9 -0
  35. package/dist/transforms/index.js +9 -0
  36. package/dist/transforms/needle_animation_transform.d.ts +2 -0
  37. package/dist/transforms/needle_animation_transform.js +5 -0
  38. package/dist/transforms/needle_asset.d.ts +2 -0
  39. package/dist/transforms/needle_asset.js +28 -0
  40. package/dist/transforms/needle_common.d.ts +11 -0
  41. package/dist/transforms/needle_common.js +1 -0
  42. package/dist/transforms/needle_mesh_transform.d.ts +3 -0
  43. package/dist/transforms/needle_mesh_transform.js +83 -0
  44. package/dist/transforms/needle_progressive.d.ts +34 -0
  45. package/dist/transforms/needle_progressive.js +264 -0
  46. package/dist/transforms/needle_simplify.d.ts +3 -0
  47. package/dist/transforms/needle_simplify.js +76 -0
  48. package/dist/transforms/needle_texture_transform.d.ts +5 -0
  49. package/dist/transforms/needle_texture_transform.js +123 -0
  50. package/dist/transforms/needle_toktx.d.ts +24 -0
  51. package/dist/transforms/needle_toktx.js +144 -0
  52. package/dist/transforms/needle_webp.d.ts +13 -0
  53. package/dist/transforms/needle_webp.js +53 -0
  54. package/dist/transforms/toktx.d.ts +77 -0
  55. package/dist/transforms/toktx.js +230 -0
  56. package/dist/transforms/types/squoosh.d.ts +31 -0
  57. package/dist/transforms/types/squoosh.js +33 -0
  58. package/dist/transforms/util.d.ts +21 -0
  59. package/dist/transforms/util.js +90 -0
  60. package/dist/utils/fileutils.d.ts +2 -0
  61. package/dist/utils/fileutils.js +95 -0
  62. package/dist/utils/index.d.ts +1 -0
  63. package/dist/utils/index.js +1 -0
  64. package/fileutils.mjs +6 -0
  65. package/index.mjs +33 -0
  66. package/make-progressive.mjs +11 -0
  67. package/pack-gltf.mjs +119 -0
  68. package/package.json +60 -0
  69. package/tsconfig.json +15 -0
@@ -0,0 +1,53 @@
1
+ import { WebPEncoderOptions } from "./types/squoosh.js";
2
+ import sharp from 'sharp';
3
+ import { BufferUtils, HTTPUtils } from '@gltf-transform/core';
4
+ var Codec;
5
+ (function (Codec) {
6
+ Codec["OXIPNG"] = "oxipng";
7
+ Codec["MOZJPEG"] = "mozjpeg";
8
+ Codec["WEBP"] = "webp";
9
+ })(Codec || (Codec = {}));
10
+ const CODEC_TO_MIME_TYPE = {
11
+ [Codec.OXIPNG]: 'image/png',
12
+ [Codec.MOZJPEG]: 'image/jpeg',
13
+ [Codec.WEBP]: 'image/webp',
14
+ };
15
+ const defaultWebpCodecOptions = new WebPEncoderOptions();
16
+ defaultWebpCodecOptions.quality = 90;
17
+ defaultWebpCodecOptions.filter_type = 0; // turn strong filter off
18
+ defaultWebpCodecOptions.use_sharp_yuv = 1; // use sharp rgb -> YUV conversion
19
+ export class NeedleWebPContext {
20
+ options;
21
+ logger;
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ async prepare(document) {
26
+ this.logger = document.getLogger();
27
+ }
28
+ async process(index, texture, settings) {
29
+ const options = {
30
+ quality: settings.quality,
31
+ // effort: remap(options.effort, 100, 6),
32
+ // lossless: options.lossless,
33
+ // nearLossless: options.nearLossless,
34
+ };
35
+ if (typeof options !== "string" && options.quality >= 0) {
36
+ // the quality range is 0-1 so we remap to 0-100
37
+ options.quality = settings.quality * 100;
38
+ }
39
+ if (options.quality <= 0) {
40
+ options.quality = 1;
41
+ }
42
+ // console.log(options);
43
+ console.log(`> Will compress texture ${texture.getName()} to webp with quality ${options.quality}`);
44
+ const buffer = await sharp(texture.getImage()).toFormat('webp', options).toBuffer();
45
+ texture
46
+ .setMimeType('image/webp')
47
+ .setImage(BufferUtils.toView(buffer))
48
+ .setURI(HTTPUtils.basename(texture.getURI()) + '.webp');
49
+ return true;
50
+ }
51
+ async finalize(document) {
52
+ }
53
+ }
@@ -0,0 +1,77 @@
1
+ import { Document, ILogger, vec2, Texture } from '@gltf-transform/core';
2
+ /**********************************************************************************************
3
+ * Interfaces.
4
+ */
5
+ export declare const Mode: {
6
+ ETC1S: string;
7
+ UASTC: string;
8
+ };
9
+ export declare const Filter: {
10
+ BOX: string;
11
+ TENT: string;
12
+ BELL: string;
13
+ BSPLINE: string;
14
+ MITCHELL: string;
15
+ LANCZOS3: string;
16
+ LANCZOS4: string;
17
+ LANCZOS6: string;
18
+ LANCZOS12: string;
19
+ BLACKMAN: string;
20
+ KAISER: string;
21
+ GAUSSIAN: string;
22
+ CATMULLROM: string;
23
+ QUADRATIC_INTERP: string;
24
+ QUADRATIC_APPROX: string;
25
+ QUADRATIC_MIX: string;
26
+ };
27
+ export type TextureModeArgs = {
28
+ doc: Document;
29
+ texture: Texture;
30
+ index: number;
31
+ options: ETC1SOptions | UASTCOptions;
32
+ };
33
+ export type TextureParamsArgs = {
34
+ doc: Document;
35
+ texture: Texture;
36
+ index: number;
37
+ params: Array<string | number>;
38
+ };
39
+ export interface ToktxOptions {
40
+ toktxPath?: string;
41
+ /** return false if texture should not be compressed */
42
+ modifyOptions?: (args: TextureModeArgs) => void | boolean;
43
+ modifyParams?: (args: TextureParamsArgs) => void;
44
+ }
45
+ export interface GlobalOptions {
46
+ mode: string;
47
+ slots?: string;
48
+ filter?: string;
49
+ filterScale?: number;
50
+ powerOfTwo?: boolean;
51
+ jobs?: number;
52
+ }
53
+ export interface ETC1SOptions extends GlobalOptions {
54
+ quality?: number;
55
+ compression?: number;
56
+ maxEndpoints?: number;
57
+ maxSelectors?: number;
58
+ rdoOff?: boolean;
59
+ rdoThreshold?: number;
60
+ }
61
+ export interface UASTCOptions extends GlobalOptions {
62
+ level?: number;
63
+ rdo?: number;
64
+ rdoDictionarySize?: number;
65
+ rdoBlockScale?: number;
66
+ rdoStdDev?: number;
67
+ rdoMultithreading?: boolean;
68
+ zstd?: number;
69
+ }
70
+ export declare const ETC1S_DEFAULTS: Omit<ETC1SOptions, 'mode'>;
71
+ export declare const UASTC_DEFAULTS: Omit<UASTCOptions, 'mode'>;
72
+ /**********************************************************************************************
73
+ * Utilities.
74
+ */
75
+ /** Create CLI parameters from the given options. Attempts to write only non-default options. */
76
+ export declare function createParams(slots: string[], channels: number, size: vec2, logger: ILogger, numTextures: number, options: ETC1SOptions | UASTCOptions, version: string): (string | number)[];
77
+ export declare function checkKTXSoftware(logger: ILogger, env?: any): Promise<string>;
@@ -0,0 +1,230 @@
1
+ import micromatch from 'micromatch';
2
+ import os from 'os';
3
+ import semver from 'semver';
4
+ import tmp from 'tmp';
5
+ import { TextureChannel } from '@gltf-transform/core';
6
+ // import { TextureBasisu } from '@gltf-transform/extensions';
7
+ // import { getTextureChannelMask, listTextureSlots } from '@gltf-transform/functions';
8
+ import { execSync } from 'child_process';
9
+ import { spawn, commandExists, waitExit, MICROMATCH_OPTIONS } from './util.js';
10
+ // import { existsSync, mkdirSync } from 'fs';
11
+ // import { tryGetFromCache, getKey, addToCache } from '../cache';
12
+ // import { resizeTextureIfNecessary } from "./needle_texture_transform"
13
+ tmp.setGracefulCleanup();
14
+ const NUM_CPUS = os.cpus().length || 1; // microsoft/vscode#112122
15
+ const KTX_SOFTWARE_VERSION_MIN = '4.0.0-rc1';
16
+ const KTX_SOFTWARE_VERSION_ACTIVE = '4.1.0-rc1';
17
+ const { R, G } = TextureChannel;
18
+ /**********************************************************************************************
19
+ * Interfaces.
20
+ */
21
+ export const Mode = {
22
+ ETC1S: 'etc1s',
23
+ UASTC: 'uastc',
24
+ };
25
+ export const Filter = {
26
+ BOX: 'box',
27
+ TENT: 'tent',
28
+ BELL: 'bell',
29
+ BSPLINE: 'b-spline',
30
+ MITCHELL: 'mitchell',
31
+ LANCZOS3: 'lanczos3',
32
+ LANCZOS4: 'lanczos4',
33
+ LANCZOS6: 'lanczos6',
34
+ LANCZOS12: 'lanczos12',
35
+ BLACKMAN: 'blackman',
36
+ KAISER: 'kaiser',
37
+ GAUSSIAN: 'gaussian',
38
+ CATMULLROM: 'catmullrom',
39
+ QUADRATIC_INTERP: 'quadratic_interp',
40
+ QUADRATIC_APPROX: 'quadratic_approx',
41
+ QUADRATIC_MIX: 'quadratic_mix',
42
+ };
43
+ const GLOBAL_DEFAULTS = {
44
+ filter: Filter.LANCZOS4,
45
+ filterScale: 1,
46
+ powerOfTwo: false,
47
+ slots: '*',
48
+ // See: https://github.com/donmccurdy/glTF-Transform/pull/389#issuecomment-1089842185
49
+ jobs: 2 * NUM_CPUS,
50
+ };
51
+ export const ETC1S_DEFAULTS = {
52
+ quality: 128,
53
+ compression: 1,
54
+ ...GLOBAL_DEFAULTS,
55
+ };
56
+ export const UASTC_DEFAULTS = {
57
+ level: 2,
58
+ rdo: 0,
59
+ rdoDictionarySize: 32768,
60
+ rdoBlockScale: 10.0,
61
+ rdoStdDev: 18.0,
62
+ rdoMultithreading: true,
63
+ zstd: 18,
64
+ ...GLOBAL_DEFAULTS,
65
+ };
66
+ /**********************************************************************************************
67
+ * Utilities.
68
+ */
69
+ /** Create CLI parameters from the given options. Attempts to write only non-default options. */
70
+ export function createParams(slots, channels, size, logger, numTextures, options, version) {
71
+ const params = [];
72
+ params.push('--genmipmap');
73
+ if (options.filter !== GLOBAL_DEFAULTS.filter)
74
+ params.push('--filter', options.filter);
75
+ if (options.filterScale !== GLOBAL_DEFAULTS.filterScale) {
76
+ params.push('--fscale', options.filterScale);
77
+ }
78
+ if (options.mode === Mode.UASTC) {
79
+ const _options = options;
80
+ params.push('--uastc', _options.level);
81
+ if (_options.rdo !== UASTC_DEFAULTS.rdo) {
82
+ params.push('--uastc_rdo_l', _options.rdo);
83
+ }
84
+ if (_options.rdoDictionarySize !== UASTC_DEFAULTS.rdoDictionarySize) {
85
+ params.push('--uastc_rdo_d', _options.rdoDictionarySize);
86
+ }
87
+ if (_options.rdoBlockScale !== UASTC_DEFAULTS.rdoBlockScale) {
88
+ params.push('--uastc_rdo_b', _options.rdoBlockScale);
89
+ }
90
+ if (_options.rdoStdDev !== UASTC_DEFAULTS.rdoStdDev) {
91
+ params.push('--uastc_rdo_s', _options.rdoStdDev);
92
+ }
93
+ if (!_options.rdoMultithreading) {
94
+ params.push('--uastc_rdo_m');
95
+ }
96
+ if (_options.zstd && _options.zstd > 0)
97
+ params.push('--zcmp', _options.zstd);
98
+ }
99
+ else {
100
+ const _options = options;
101
+ params.push('--bcmp');
102
+ if (_options.quality !== ETC1S_DEFAULTS.quality) {
103
+ params.push('--qlevel', _options.quality);
104
+ }
105
+ if (_options.compression !== ETC1S_DEFAULTS.compression) {
106
+ params.push('--clevel', _options.compression);
107
+ }
108
+ if (_options.maxEndpoints)
109
+ params.push('--max_endpoints', _options.maxEndpoints);
110
+ if (_options.maxSelectors)
111
+ params.push('--max_selectors', _options.maxSelectors);
112
+ if (_options.rdoOff) {
113
+ params.push('--no_endpoint_rdo', '--no_selector_rdo');
114
+ }
115
+ else if (_options.rdoThreshold) {
116
+ params.push('--endpoint_rdo_threshold', _options.rdoThreshold);
117
+ params.push('--selector_rdo_threshold', _options.rdoThreshold);
118
+ }
119
+ }
120
+ if (slots.find((slot) => micromatch.isMatch(slot, '*normal*', MICROMATCH_OPTIONS))) {
121
+ // See: https://github.com/KhronosGroup/KTX-Software/issues/600
122
+ if (semver.gte(version, KTX_SOFTWARE_VERSION_ACTIVE)) {
123
+ params.push('--normal_mode', '--input_swizzle', 'rgb1');
124
+ }
125
+ else if (options.mode === Mode.ETC1S) {
126
+ params.push('--normal_map');
127
+ }
128
+ }
129
+ if (slots.length && !slots.find((slot) => micromatch.isMatch(slot, '*{color,emissive}*', MICROMATCH_OPTIONS))) {
130
+ // See: https://github.com/donmccurdy/glTF-Transform/issues/215
131
+ params.push('--assign_oetf', 'linear', '--assign_primaries', 'none');
132
+ }
133
+ if (channels === R) {
134
+ params.push('--target_type', 'R');
135
+ }
136
+ else if (channels === G || channels === (R | G)) {
137
+ params.push('--target_type', 'RG');
138
+ }
139
+ // Minimum size on any dimension is 4px.
140
+ // See: https://github.com/donmccurdy/glTF-Transform/issues/502
141
+ let width;
142
+ let height;
143
+ if (options.powerOfTwo) {
144
+ width = preferredPowerOfTwo(size[0]);
145
+ height = preferredPowerOfTwo(size[1]);
146
+ }
147
+ else {
148
+ if (!isPowerOfTwo(size[0]) || !isPowerOfTwo(size[1])) {
149
+ logger.warn(`toktx: Texture dimensions ${size[0]}x${size[1]} are NPOT, and may` +
150
+ ' fail in older APIs (including WebGL 1.0) on certain devices.');
151
+ }
152
+ width = isMultipleOfFour(size[0]) ? size[0] : ceilMultipleOfFour(size[0]);
153
+ height = isMultipleOfFour(size[1]) ? size[1] : ceilMultipleOfFour(size[1]);
154
+ }
155
+ if (width !== size[0] || height !== size[1]) {
156
+ if (width > 4096 || height > 4096) {
157
+ logger.warn(`toktx: Resizing to nearest power of two, ${width}x${height}px. Texture dimensions` +
158
+ ' greater than 4096px may not render on some mobile devices.' +
159
+ ' Resize to a lower resolution before compressing, if needed.');
160
+ }
161
+ params.push('--resize', `${width}x${height}`);
162
+ }
163
+ if (options.jobs && options.jobs > 1 && numTextures > 1) {
164
+ // See: https://github.com/donmccurdy/glTF-Transform/pull/389#issuecomment-1089842185
165
+ const threads = Math.max(2, Math.min(NUM_CPUS, (3 * NUM_CPUS) / numTextures));
166
+ params.push('--threads', threads);
167
+ }
168
+ return params;
169
+ }
170
+ export async function checkKTXSoftware(logger, env) {
171
+ const [status, stdout, stderr] = await waitExit(spawn('toktx', ['--version'], { env: env }));
172
+ const version = (stdout || stderr)
173
+ .replace(/toktx\s+/, '')
174
+ .replace(/~\d+/, '')
175
+ .trim();
176
+ if (status !== 0 || !semver.valid(semver.clean(version))) {
177
+ throw new Error('Unable to find "toktx" version. Confirm KTX-Software is installed from:\n\nhttps://github.com/KhronosGroup/KTX-Software.');
178
+ }
179
+ else if (semver.lt(semver.clean(version), KTX_SOFTWARE_VERSION_MIN)) {
180
+ logger.warn(`toktx: Expected KTX-Software >= v${KTX_SOFTWARE_VERSION_MIN}, found ${version}.`);
181
+ }
182
+ else {
183
+ logger.debug(`toktx: Found KTX-Software ${version}.`);
184
+ }
185
+ return semver.clean(version);
186
+ }
187
+ async function toktxCommandExistsSync(env) {
188
+ if (env) {
189
+ // if env is defined use that to check if toktx is installed
190
+ try {
191
+ execSync('toktx --version', { encoding: 'utf-8', env: env });
192
+ return true;
193
+ }
194
+ catch (error) {
195
+ return false;
196
+ }
197
+ }
198
+ else if (!await commandExists('toktx')) {
199
+ return false;
200
+ }
201
+ return true;
202
+ }
203
+ function isPowerOfTwo(value) {
204
+ if (value <= 2)
205
+ return true;
206
+ return (value & (value - 1)) === 0 && value !== 0;
207
+ }
208
+ function preferredPowerOfTwo(value) {
209
+ if (value <= 4)
210
+ return 4;
211
+ const lo = floorPowerOfTwo(value);
212
+ const hi = ceilPowerOfTwo(value);
213
+ if (hi - value > value - lo)
214
+ return lo;
215
+ return hi;
216
+ }
217
+ function floorPowerOfTwo(value) {
218
+ return Math.pow(2, Math.floor(Math.log(value) / Math.LN2));
219
+ }
220
+ function ceilPowerOfTwo(value) {
221
+ return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2));
222
+ }
223
+ function isMultipleOfFour(value) {
224
+ return value % 4 === 0;
225
+ }
226
+ function ceilMultipleOfFour(value) {
227
+ if (value <= 4)
228
+ return 4;
229
+ return value % 4 ? value + 4 - (value % 4) : value;
230
+ }
@@ -0,0 +1,31 @@
1
+ export declare class WebPEncoderOptions {
2
+ quality: number;
3
+ target_size: number;
4
+ target_PSNR: number;
5
+ method: number;
6
+ sns_strength: number;
7
+ filter_strength: number;
8
+ filter_sharpness: number;
9
+ /** 1 = strong filter */
10
+ filter_type: number;
11
+ partitions: number;
12
+ segments: number;
13
+ pass: number;
14
+ show_compressed: number;
15
+ preprocessing: number;
16
+ autofilter: number;
17
+ partition_limit: number;
18
+ alpha_compression: number;
19
+ alpha_filtering: number;
20
+ alpha_quality: number;
21
+ lossless: number;
22
+ exact: number;
23
+ image_hint: number;
24
+ emulate_jpeg_size: number;
25
+ thread_level: number;
26
+ low_memory: number;
27
+ near_lossless: number;
28
+ use_delta_palette: number;
29
+ /** probably sharp rgb -> YUV conversion */
30
+ use_sharp_yuv: number;
31
+ }
@@ -0,0 +1,33 @@
1
+ // client: https://github.com/GoogleChromeLabs/squoosh/blob/97d13de273b67ee01a80cb32161f1bb2103b73cb/src/features/encoders/webP/client/index.tsx#L292
2
+ // emscripten bindings:
3
+ export class WebPEncoderOptions {
4
+ quality = 75;
5
+ target_size = 0;
6
+ target_PSNR = 0;
7
+ method = 4;
8
+ sns_strength = 50;
9
+ filter_strength = 60;
10
+ filter_sharpness = 0;
11
+ /** 1 = strong filter */
12
+ filter_type = 1;
13
+ partitions = 0;
14
+ segments = 4;
15
+ pass = 1;
16
+ show_compressed = 0;
17
+ preprocessing = 0;
18
+ autofilter = 0;
19
+ partition_limit = 0;
20
+ alpha_compression = 1;
21
+ alpha_filtering = 1;
22
+ alpha_quality = 100;
23
+ lossless = 0;
24
+ exact = 0;
25
+ image_hint = 0;
26
+ emulate_jpeg_size = 0;
27
+ thread_level = 0;
28
+ low_memory = 0;
29
+ near_lossless = 100;
30
+ use_delta_palette = 0;
31
+ /** probably sharp rgb -> YUV conversion */
32
+ use_sharp_yuv = 0;
33
+ }
@@ -0,0 +1,21 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { spawn as _spawn } from 'child_process';
3
+ import type { ChildProcess } from 'child_process';
4
+ export declare const XMPContext: Record<string, string>;
5
+ export declare const MICROMATCH_OPTIONS: {
6
+ nocase: boolean;
7
+ contains: boolean;
8
+ };
9
+ export declare let spawn: typeof _spawn;
10
+ export declare let commandExists: (cmd: string) => any;
11
+ export declare let waitExit: typeof _waitExit;
12
+ export declare function mockSpawn(_spawn: unknown): void;
13
+ export declare function mockCommandExists(_commandExists: (n: string) => Promise<boolean>): void;
14
+ export declare function mockWaitExit(_waitExit: (process: ChildProcess) => Promise<[unknown, string, string]>): void;
15
+ export declare function _waitExit(process: ChildProcess): Promise<[unknown, string, string]>;
16
+ export declare function formatLong(x: number): string;
17
+ export declare function formatBytes(bytes: number, decimals?: number): string;
18
+ export declare function formatParagraph(str: string): string;
19
+ export declare function formatHeader(title: string): string;
20
+ export declare function formatXMP(value: string | number | boolean | Record<string, unknown> | null): string;
21
+ export declare function underline(str: string): string;
@@ -0,0 +1,90 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ import { spawn as _spawn } from 'child_process';
3
+ import _commandExists from 'command-exists';
4
+ // Constants.
5
+ export const XMPContext = {
6
+ dc: 'http://purl.org/dc/elements/1.1/',
7
+ model3d: 'https://schema.khronos.org/model3d/xsd/1.0/',
8
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
9
+ xmp: 'http://ns.adobe.com/xap/1.0/',
10
+ xmpRights: 'http://ns.adobe.com/xap/1.0/rights/',
11
+ };
12
+ // Using 'micromatch' because 'contains: true' did not work as expected with
13
+ // minimatch. Need to ensure that '*' matches patterns like 'image/png'.
14
+ export const MICROMATCH_OPTIONS = { nocase: true, contains: true };
15
+ // Mocks for tests.
16
+ export let spawn = _spawn;
17
+ // See https://github.com/mathisonian/command-exists/issues/22
18
+ export let commandExists = (cmd) => _commandExists(cmd)?.catch(() => false) ?? false;
19
+ export let waitExit = _waitExit;
20
+ export function mockSpawn(_spawn) {
21
+ //@ts-ignore
22
+ spawn = _spawn;
23
+ }
24
+ export function mockCommandExists(_commandExists) {
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ commandExists = _commandExists;
27
+ }
28
+ export function mockWaitExit(_waitExit) {
29
+ waitExit = _waitExit;
30
+ }
31
+ export async function _waitExit(process) {
32
+ let stdout = '';
33
+ if (process.stdout) {
34
+ for await (const chunk of process.stdout) {
35
+ stdout += chunk;
36
+ }
37
+ }
38
+ let stderr = '';
39
+ if (process.stderr) {
40
+ for await (const chunk of process.stderr) {
41
+ stderr += chunk;
42
+ }
43
+ }
44
+ const status = await new Promise((resolve, _) => {
45
+ process.on('close', resolve);
46
+ });
47
+ return [status, stdout, stderr];
48
+ }
49
+ // Formatting.
50
+ export function formatLong(x) {
51
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
52
+ }
53
+ export function formatBytes(bytes, decimals = 2) {
54
+ if (bytes === 0)
55
+ return '0 Bytes';
56
+ const k = 1000;
57
+ const dm = decimals < 0 ? 0 : decimals;
58
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
59
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
60
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
61
+ }
62
+ export function formatParagraph(str) {
63
+ return str
64
+ .match(/.{1,80}(\s|$)/g)
65
+ .map((line) => line.trim())
66
+ .join('\n');
67
+ }
68
+ export function formatHeader(title) {
69
+ return '' + '\n ' + title.toUpperCase() + '\n ────────────────────────────────────────────';
70
+ }
71
+ export function formatXMP(value) {
72
+ if (!value) {
73
+ return '';
74
+ }
75
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
76
+ return value.toString();
77
+ }
78
+ if (value['@list']) {
79
+ const list = value['@list'];
80
+ const hasCommas = list.some((value) => value.indexOf(',') > 0);
81
+ return list.join(hasCommas ? '; ' : ', ');
82
+ }
83
+ if (value['@type'] === 'rdf:Alt') {
84
+ return value['rdf:_1']['@value'];
85
+ }
86
+ return JSON.stringify(value);
87
+ }
88
+ export function underline(str) {
89
+ return `\x1b[4m${str}\x1b[0m`;
90
+ }
@@ -0,0 +1,2 @@
1
+ export declare type GLTFFileCallback = (file: string) => void | Promise<void>;
2
+ export declare function foreachGLTF(args: string | string[], callback: GLTFFileCallback): void;
@@ -0,0 +1,95 @@
1
+ import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
2
+ import { isAbsolute } from 'path';
3
+ export function foreachGLTF(args, callback) {
4
+ // get original directory this run was invoked from
5
+ const initDir = process.env.INIT_CWD;
6
+ const context = {
7
+ baseDirectory: initDir,
8
+ filesFound: 0
9
+ };
10
+ // traverse recursively through all files and directories
11
+ if (Array.isArray(args)) {
12
+ for (const arg of args) {
13
+ testEntry(arg, callback, context);
14
+ }
15
+ }
16
+ else if (typeof args === "string") {
17
+ testEntry(args, callback, context);
18
+ }
19
+ // try to run with needle config
20
+ if (context.filesFound <= 0 && context.baseDirectory) {
21
+ const needleConfigPath = context.baseDirectory + "/needle.config.json";
22
+ if (existsSync(needleConfigPath)) {
23
+ const config = JSON.parse(readFileSync(needleConfigPath, "utf8"));
24
+ const buildDirectory = config.buildDirectory;
25
+ if (buildDirectory?.length) {
26
+ // If it's an absolute path
27
+ if (existsSync(buildDirectory)) {
28
+ console.log("INFO: Using build directory from needle.config.json: " + buildDirectory);
29
+ testEntry(buildDirectory, callback, context);
30
+ }
31
+ else { // if its a relative path
32
+ const newPath = context.baseDirectory + "/" + buildDirectory;
33
+ if (existsSync(newPath)) {
34
+ console.log("INFO: Using build directory from needle.config.json: " + newPath);
35
+ testEntry(newPath, callback, context);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ if (context.filesFound <= 0) {
42
+ console.log("WARN: No glTF files found. Make sure to invoke with a valid path to a glTF file or directory. This path may be relative to " + initDir);
43
+ }
44
+ }
45
+ function testEntry(entry, callback, context) {
46
+ if (entry === "." || entry === "./") {
47
+ if (context.baseDirectory) {
48
+ entry = context.baseDirectory;
49
+ }
50
+ }
51
+ if (existsSync(entry) === false) {
52
+ // If no base directory is defined we cannot try to resolve the path
53
+ if (context.baseDirectory === undefined) {
54
+ return;
55
+ }
56
+ // Try resolve potential relative path
57
+ const newPath = context.baseDirectory + "/" + entry;
58
+ console.log(newPath);
59
+ if (existsSync(newPath) === false) {
60
+ return;
61
+ }
62
+ else {
63
+ console.log(`INFO: Resolved path: ${entry} -> ${newPath}`);
64
+ entry = newPath;
65
+ }
66
+ }
67
+ else {
68
+ // When passing in a relative path (e.g. `dist`) we dont want to accidentally traverse this package's directory
69
+ // So in that case make sure we resolve to an absolute path relative to the calling directory
70
+ const isAbs = isAbsolute(entry);
71
+ if (!isAbs) {
72
+ if (context.baseDirectory) {
73
+ const newPath = context.baseDirectory + "/" + entry;
74
+ console.log(`INFO: Resolved path: ${entry} -> ${newPath}`);
75
+ entry = newPath;
76
+ if (!existsSync(entry)) {
77
+ return;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (entry.endsWith(".glb") || entry.endsWith(".gltf")) {
83
+ context.filesFound += 1;
84
+ callback(entry);
85
+ }
86
+ else {
87
+ const stat = statSync(entry);
88
+ if (stat.isDirectory()) {
89
+ const files = readdirSync(entry);
90
+ for (const file of files) {
91
+ testEntry(entry + "/" + file, callback, context);
92
+ }
93
+ }
94
+ }
95
+ }
@@ -0,0 +1 @@
1
+ export * from "./fileutils.js";
@@ -0,0 +1 @@
1
+ export * from "./fileutils.js";
package/fileutils.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { foreachGLTF as foreach } from './dist/index.js';
2
+
3
+
4
+ export function foreachGLTF(args, callback) {
5
+ return foreach(args, callback);
6
+ }
package/index.mjs ADDED
@@ -0,0 +1,33 @@
1
+
2
+
3
+ import { spawnSync } from "child_process";
4
+ const workingDir = process.cwd();
5
+
6
+ // Original invoked dir set by npm
7
+ // const dir = process.env.INIT_CWD;
8
+
9
+
10
+ const defaultArgs = { shell: true, cwd: workingDir, stdio: 'inherit' }
11
+
12
+ function getCommandLineArgs(_cmd) {
13
+ // Wrap paths with parentheses if they contain spaces
14
+ const args = process.argv.slice(2);
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg.includes("\\") || arg.includes("/")) {
18
+ if (arg.includes(" "))
19
+ args[i] = "\"" + arg + "\"";
20
+ }
21
+ }
22
+ return args;
23
+ }
24
+
25
+
26
+ function runWithArgs(cmd) {
27
+ const commandline_args = getCommandLineArgs(cmd).join(" ");
28
+ spawnSync(cmd + " " + commandline_args, defaultArgs);
29
+ }
30
+
31
+
32
+ runWithArgs("node make-progressive.mjs");
33
+ runWithArgs("node pack-gltf.mjs");