@reshotdev/screenshot 0.0.1-beta.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.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- package/web/subtitle-editor/index.html +295 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// viewport-presets.js - Robust viewport configuration with named presets
|
|
2
|
+
// Provides flexible viewport sizing with device presets, custom sizes, and crop regions
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Standard device viewport presets
|
|
6
|
+
* Each preset includes viewport dimensions and optional deviceScaleFactor
|
|
7
|
+
*/
|
|
8
|
+
const VIEWPORT_PRESETS = {
|
|
9
|
+
// Desktop presets
|
|
10
|
+
"desktop-hd": {
|
|
11
|
+
name: "Desktop HD",
|
|
12
|
+
category: "desktop",
|
|
13
|
+
width: 1920,
|
|
14
|
+
height: 1080,
|
|
15
|
+
deviceScaleFactor: 1,
|
|
16
|
+
description: "Full HD desktop (1920×1080)",
|
|
17
|
+
},
|
|
18
|
+
"desktop": {
|
|
19
|
+
name: "Desktop",
|
|
20
|
+
category: "desktop",
|
|
21
|
+
width: 1280,
|
|
22
|
+
height: 720,
|
|
23
|
+
deviceScaleFactor: 1,
|
|
24
|
+
description: "Standard desktop (1280×720)",
|
|
25
|
+
},
|
|
26
|
+
"desktop-small": {
|
|
27
|
+
name: "Desktop Small",
|
|
28
|
+
category: "desktop",
|
|
29
|
+
width: 1024,
|
|
30
|
+
height: 768,
|
|
31
|
+
deviceScaleFactor: 1,
|
|
32
|
+
description: "Small desktop/laptop (1024×768)",
|
|
33
|
+
},
|
|
34
|
+
"desktop-retina": {
|
|
35
|
+
name: "Desktop Retina",
|
|
36
|
+
category: "desktop",
|
|
37
|
+
width: 1280,
|
|
38
|
+
height: 720,
|
|
39
|
+
deviceScaleFactor: 2,
|
|
40
|
+
description: "Retina desktop (2x scale)",
|
|
41
|
+
},
|
|
42
|
+
"desktop-4k": {
|
|
43
|
+
name: "Desktop 4K",
|
|
44
|
+
category: "desktop",
|
|
45
|
+
width: 3840,
|
|
46
|
+
height: 2160,
|
|
47
|
+
deviceScaleFactor: 1,
|
|
48
|
+
description: "4K UHD desktop (3840×2160)",
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Tablet presets
|
|
52
|
+
"tablet-landscape": {
|
|
53
|
+
name: "Tablet Landscape",
|
|
54
|
+
category: "tablet",
|
|
55
|
+
width: 1024,
|
|
56
|
+
height: 768,
|
|
57
|
+
deviceScaleFactor: 2,
|
|
58
|
+
description: "iPad-like landscape (1024×768)",
|
|
59
|
+
},
|
|
60
|
+
"tablet-portrait": {
|
|
61
|
+
name: "Tablet Portrait",
|
|
62
|
+
category: "tablet",
|
|
63
|
+
width: 768,
|
|
64
|
+
height: 1024,
|
|
65
|
+
deviceScaleFactor: 2,
|
|
66
|
+
description: "iPad-like portrait (768×1024)",
|
|
67
|
+
},
|
|
68
|
+
"tablet-pro-landscape": {
|
|
69
|
+
name: "Tablet Pro Landscape",
|
|
70
|
+
category: "tablet",
|
|
71
|
+
width: 1366,
|
|
72
|
+
height: 1024,
|
|
73
|
+
deviceScaleFactor: 2,
|
|
74
|
+
description: "iPad Pro-like landscape",
|
|
75
|
+
},
|
|
76
|
+
"tablet-pro-portrait": {
|
|
77
|
+
name: "Tablet Pro Portrait",
|
|
78
|
+
category: "tablet",
|
|
79
|
+
width: 1024,
|
|
80
|
+
height: 1366,
|
|
81
|
+
deviceScaleFactor: 2,
|
|
82
|
+
description: "iPad Pro-like portrait",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Mobile presets
|
|
86
|
+
"mobile": {
|
|
87
|
+
name: "Mobile",
|
|
88
|
+
category: "mobile",
|
|
89
|
+
width: 375,
|
|
90
|
+
height: 667,
|
|
91
|
+
deviceScaleFactor: 2,
|
|
92
|
+
description: "iPhone-like (375×667)",
|
|
93
|
+
},
|
|
94
|
+
"mobile-small": {
|
|
95
|
+
name: "Mobile Small",
|
|
96
|
+
category: "mobile",
|
|
97
|
+
width: 320,
|
|
98
|
+
height: 568,
|
|
99
|
+
deviceScaleFactor: 2,
|
|
100
|
+
description: "Small mobile (320×568)",
|
|
101
|
+
},
|
|
102
|
+
"mobile-large": {
|
|
103
|
+
name: "Mobile Large",
|
|
104
|
+
category: "mobile",
|
|
105
|
+
width: 414,
|
|
106
|
+
height: 896,
|
|
107
|
+
deviceScaleFactor: 3,
|
|
108
|
+
description: "Large mobile / iPhone Pro Max-like",
|
|
109
|
+
},
|
|
110
|
+
"mobile-landscape": {
|
|
111
|
+
name: "Mobile Landscape",
|
|
112
|
+
category: "mobile",
|
|
113
|
+
width: 667,
|
|
114
|
+
height: 375,
|
|
115
|
+
deviceScaleFactor: 2,
|
|
116
|
+
description: "Mobile landscape orientation",
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Documentation-specific presets
|
|
120
|
+
"docs-wide": {
|
|
121
|
+
name: "Docs Wide",
|
|
122
|
+
category: "docs",
|
|
123
|
+
width: 1200,
|
|
124
|
+
height: 800,
|
|
125
|
+
deviceScaleFactor: 2,
|
|
126
|
+
description: "Wide documentation screenshots",
|
|
127
|
+
},
|
|
128
|
+
"docs-standard": {
|
|
129
|
+
name: "Docs Standard",
|
|
130
|
+
category: "docs",
|
|
131
|
+
width: 960,
|
|
132
|
+
height: 640,
|
|
133
|
+
deviceScaleFactor: 2,
|
|
134
|
+
description: "Standard documentation screenshots",
|
|
135
|
+
},
|
|
136
|
+
"docs-narrow": {
|
|
137
|
+
name: "Docs Narrow",
|
|
138
|
+
category: "docs",
|
|
139
|
+
width: 720,
|
|
140
|
+
height: 480,
|
|
141
|
+
deviceScaleFactor: 2,
|
|
142
|
+
description: "Narrow documentation screenshots",
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Social/Marketing presets
|
|
146
|
+
"social-og": {
|
|
147
|
+
name: "Open Graph",
|
|
148
|
+
category: "social",
|
|
149
|
+
width: 1200,
|
|
150
|
+
height: 630,
|
|
151
|
+
deviceScaleFactor: 2,
|
|
152
|
+
description: "Open Graph / Facebook sharing (1200×630)",
|
|
153
|
+
},
|
|
154
|
+
"social-twitter": {
|
|
155
|
+
name: "Twitter Card",
|
|
156
|
+
category: "social",
|
|
157
|
+
width: 1200,
|
|
158
|
+
height: 600,
|
|
159
|
+
deviceScaleFactor: 2,
|
|
160
|
+
description: "Twitter card image (1200×600)",
|
|
161
|
+
},
|
|
162
|
+
"social-linkedin": {
|
|
163
|
+
name: "LinkedIn",
|
|
164
|
+
category: "social",
|
|
165
|
+
width: 1200,
|
|
166
|
+
height: 627,
|
|
167
|
+
deviceScaleFactor: 2,
|
|
168
|
+
description: "LinkedIn post image (1200×627)",
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Crop region presets for common UI sections
|
|
174
|
+
*/
|
|
175
|
+
const CROP_PRESETS = {
|
|
176
|
+
// Navigation regions
|
|
177
|
+
"header": {
|
|
178
|
+
name: "Header Only",
|
|
179
|
+
description: "Top navigation header region",
|
|
180
|
+
// Percentage-based - will be calculated based on viewport
|
|
181
|
+
percentBased: true,
|
|
182
|
+
region: { x: 0, y: 0, widthPercent: 100, heightPercent: 10 },
|
|
183
|
+
},
|
|
184
|
+
"sidebar": {
|
|
185
|
+
name: "Sidebar Only",
|
|
186
|
+
description: "Left sidebar navigation",
|
|
187
|
+
percentBased: true,
|
|
188
|
+
region: { x: 0, y: 0, widthPercent: 20, heightPercent: 100 },
|
|
189
|
+
},
|
|
190
|
+
"main-content": {
|
|
191
|
+
name: "Main Content",
|
|
192
|
+
description: "Main content area (excluding sidebar)",
|
|
193
|
+
percentBased: true,
|
|
194
|
+
region: { xPercent: 20, y: 0, widthPercent: 80, heightPercent: 100 },
|
|
195
|
+
},
|
|
196
|
+
"center-modal": {
|
|
197
|
+
name: "Center Modal",
|
|
198
|
+
description: "Centered modal dialog area",
|
|
199
|
+
percentBased: true,
|
|
200
|
+
region: { xPercent: 15, yPercent: 15, widthPercent: 70, heightPercent: 70 },
|
|
201
|
+
},
|
|
202
|
+
"full": {
|
|
203
|
+
name: "Full Viewport",
|
|
204
|
+
description: "No cropping - full viewport",
|
|
205
|
+
percentBased: false,
|
|
206
|
+
enabled: false,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get a viewport preset by name
|
|
212
|
+
* @param {string} presetName - Name of the preset
|
|
213
|
+
* @returns {Object|null} Viewport configuration or null if not found
|
|
214
|
+
*/
|
|
215
|
+
function getViewportPreset(presetName) {
|
|
216
|
+
return VIEWPORT_PRESETS[presetName] || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all viewport presets
|
|
221
|
+
* @returns {Object} All viewport presets keyed by name
|
|
222
|
+
*/
|
|
223
|
+
function getAllViewportPresets() {
|
|
224
|
+
return { ...VIEWPORT_PRESETS };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get viewport presets grouped by category
|
|
229
|
+
* @returns {Object} Presets grouped by category
|
|
230
|
+
*/
|
|
231
|
+
function getViewportPresetsByCategory() {
|
|
232
|
+
const grouped = {};
|
|
233
|
+
for (const [key, preset] of Object.entries(VIEWPORT_PRESETS)) {
|
|
234
|
+
const category = preset.category || "other";
|
|
235
|
+
if (!grouped[category]) {
|
|
236
|
+
grouped[category] = [];
|
|
237
|
+
}
|
|
238
|
+
grouped[category].push({ key, ...preset });
|
|
239
|
+
}
|
|
240
|
+
return grouped;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve viewport configuration from various inputs
|
|
245
|
+
* Supports:
|
|
246
|
+
* - Preset name: "desktop", "mobile", etc.
|
|
247
|
+
* - Shorthand: "1280x720" or "1280x720@2x"
|
|
248
|
+
* - Object: { width: 1280, height: 720, deviceScaleFactor: 2 }
|
|
249
|
+
*
|
|
250
|
+
* @param {string|Object} input - Viewport specification
|
|
251
|
+
* @param {Object} defaults - Default values to use
|
|
252
|
+
* @returns {Object} Resolved viewport config { width, height, deviceScaleFactor, presetName? }
|
|
253
|
+
*/
|
|
254
|
+
function resolveViewport(input, defaults = { width: 1280, height: 720, deviceScaleFactor: 2 }) {
|
|
255
|
+
// Handle null/undefined - return defaults
|
|
256
|
+
if (input == null) {
|
|
257
|
+
return { ...defaults, presetName: null };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle preset name string
|
|
261
|
+
if (typeof input === "string") {
|
|
262
|
+
// Check if it's a preset name
|
|
263
|
+
const preset = VIEWPORT_PRESETS[input];
|
|
264
|
+
if (preset) {
|
|
265
|
+
return {
|
|
266
|
+
width: preset.width,
|
|
267
|
+
height: preset.height,
|
|
268
|
+
deviceScaleFactor: preset.deviceScaleFactor || defaults.deviceScaleFactor,
|
|
269
|
+
presetName: input,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for shorthand format: "WIDTHxHEIGHT" or "WIDTHxHEIGHT@Nx"
|
|
274
|
+
const shorthandMatch = input.match(/^(\d+)x(\d+)(?:@(\d+)x)?$/i);
|
|
275
|
+
if (shorthandMatch) {
|
|
276
|
+
const [, width, height, scale] = shorthandMatch;
|
|
277
|
+
return {
|
|
278
|
+
width: parseInt(width, 10),
|
|
279
|
+
height: parseInt(height, 10),
|
|
280
|
+
deviceScaleFactor: scale ? parseInt(scale, 10) : defaults.deviceScaleFactor,
|
|
281
|
+
presetName: null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Unknown string - return defaults
|
|
286
|
+
console.warn(`Unknown viewport specification: "${input}", using defaults`);
|
|
287
|
+
return { ...defaults, presetName: null };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Handle object format
|
|
291
|
+
if (typeof input === "object") {
|
|
292
|
+
return {
|
|
293
|
+
width: input.width || defaults.width,
|
|
294
|
+
height: input.height || defaults.height,
|
|
295
|
+
deviceScaleFactor: input.deviceScaleFactor ?? input.dpr ?? defaults.deviceScaleFactor,
|
|
296
|
+
presetName: input.preset || input.presetName || null,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ...defaults, presetName: null };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Validate viewport configuration
|
|
305
|
+
* @param {Object} viewport - Viewport object to validate
|
|
306
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
307
|
+
*/
|
|
308
|
+
function validateViewport(viewport) {
|
|
309
|
+
if (!viewport) {
|
|
310
|
+
return { valid: false, error: "Viewport is required" };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { width, height, deviceScaleFactor } = viewport;
|
|
314
|
+
|
|
315
|
+
if (typeof width !== "number" || width < 100 || width > 10000) {
|
|
316
|
+
return { valid: false, error: "Viewport width must be between 100 and 10000" };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (typeof height !== "number" || height < 100 || height > 10000) {
|
|
320
|
+
return { valid: false, error: "Viewport height must be between 100 and 10000" };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (deviceScaleFactor !== undefined) {
|
|
324
|
+
if (typeof deviceScaleFactor !== "number" || deviceScaleFactor < 1 || deviceScaleFactor > 4) {
|
|
325
|
+
return { valid: false, error: "deviceScaleFactor must be between 1 and 4" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { valid: true };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Resolve a crop region, supporting both absolute and percentage-based regions
|
|
334
|
+
* @param {Object} cropConfig - Crop configuration
|
|
335
|
+
* @param {Object} viewport - Current viewport { width, height }
|
|
336
|
+
* @returns {Object} Resolved absolute crop region { x, y, width, height }
|
|
337
|
+
*/
|
|
338
|
+
function resolveCropRegion(cropConfig, viewport) {
|
|
339
|
+
if (!cropConfig || !cropConfig.region) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const region = cropConfig.region;
|
|
344
|
+
|
|
345
|
+
// Check if this is a preset
|
|
346
|
+
if (cropConfig.preset && CROP_PRESETS[cropConfig.preset]) {
|
|
347
|
+
const presetConfig = CROP_PRESETS[cropConfig.preset];
|
|
348
|
+
if (!presetConfig.enabled && presetConfig.enabled !== undefined) {
|
|
349
|
+
return null; // Preset explicitly disabled (like "full")
|
|
350
|
+
}
|
|
351
|
+
return resolveCropRegion({ ...cropConfig, ...presetConfig }, viewport);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Handle percentage-based regions
|
|
355
|
+
if (cropConfig.percentBased) {
|
|
356
|
+
const x = region.xPercent !== undefined
|
|
357
|
+
? Math.round((region.xPercent / 100) * viewport.width)
|
|
358
|
+
: (region.x || 0);
|
|
359
|
+
const y = region.yPercent !== undefined
|
|
360
|
+
? Math.round((region.yPercent / 100) * viewport.height)
|
|
361
|
+
: (region.y || 0);
|
|
362
|
+
const width = region.widthPercent !== undefined
|
|
363
|
+
? Math.round((region.widthPercent / 100) * viewport.width)
|
|
364
|
+
: region.width;
|
|
365
|
+
const height = region.heightPercent !== undefined
|
|
366
|
+
? Math.round((region.heightPercent / 100) * viewport.height)
|
|
367
|
+
: region.height;
|
|
368
|
+
|
|
369
|
+
return { x, y, width, height };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Absolute region
|
|
373
|
+
return {
|
|
374
|
+
x: region.x || 0,
|
|
375
|
+
y: region.y || 0,
|
|
376
|
+
width: region.width,
|
|
377
|
+
height: region.height,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get a crop preset by name
|
|
383
|
+
* @param {string} presetName - Name of the crop preset
|
|
384
|
+
* @returns {Object|null} Crop preset configuration or null
|
|
385
|
+
*/
|
|
386
|
+
function getCropPreset(presetName) {
|
|
387
|
+
return CROP_PRESETS[presetName] || null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get all crop presets
|
|
392
|
+
* @returns {Object} All crop presets
|
|
393
|
+
*/
|
|
394
|
+
function getAllCropPresets() {
|
|
395
|
+
return { ...CROP_PRESETS };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Create a custom viewport configuration
|
|
400
|
+
* @param {Object} options - Custom viewport options
|
|
401
|
+
* @returns {Object} Viewport configuration object
|
|
402
|
+
*/
|
|
403
|
+
function createCustomViewport(options = {}) {
|
|
404
|
+
const {
|
|
405
|
+
width = 1280,
|
|
406
|
+
height = 720,
|
|
407
|
+
deviceScaleFactor = 2,
|
|
408
|
+
name = "Custom",
|
|
409
|
+
description = "",
|
|
410
|
+
} = options;
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
name,
|
|
414
|
+
category: "custom",
|
|
415
|
+
width,
|
|
416
|
+
height,
|
|
417
|
+
deviceScaleFactor,
|
|
418
|
+
description: description || `${width}×${height}${deviceScaleFactor > 1 ? ` @${deviceScaleFactor}x` : ""}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse viewport matrix configuration for multi-viewport captures
|
|
424
|
+
* @param {Array|Object|string} viewportConfig - Viewport configuration
|
|
425
|
+
* @returns {Array<Object>} Array of resolved viewport configs
|
|
426
|
+
*/
|
|
427
|
+
function parseViewportMatrix(viewportConfig) {
|
|
428
|
+
if (!viewportConfig) {
|
|
429
|
+
return [resolveViewport(null)];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Single viewport
|
|
433
|
+
if (typeof viewportConfig === "string" || !Array.isArray(viewportConfig)) {
|
|
434
|
+
return [resolveViewport(viewportConfig)];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Array of viewports - resolve each
|
|
438
|
+
return viewportConfig.map(v => resolveViewport(v));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
module.exports = {
|
|
442
|
+
VIEWPORT_PRESETS,
|
|
443
|
+
CROP_PRESETS,
|
|
444
|
+
getViewportPreset,
|
|
445
|
+
getAllViewportPresets,
|
|
446
|
+
getViewportPresetsByCategory,
|
|
447
|
+
resolveViewport,
|
|
448
|
+
validateViewport,
|
|
449
|
+
resolveCropRegion,
|
|
450
|
+
getCropPreset,
|
|
451
|
+
getAllCropPresets,
|
|
452
|
+
createCustomViewport,
|
|
453
|
+
parseViewportMatrix,
|
|
454
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// worker-pool.js - Streaming worker pool for concurrent task execution
|
|
2
|
+
// Replaces batch-based Promise.all — when any worker finishes, the next
|
|
3
|
+
// queued task starts immediately. No batch blocking.
|
|
4
|
+
|
|
5
|
+
class WorkerPool {
|
|
6
|
+
/**
|
|
7
|
+
* @param {number} concurrency - Max concurrent workers
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {Function} options.onProgress - Called after each task completes:
|
|
10
|
+
* ({ completed, total, active, durationMs, result, error }) => void
|
|
11
|
+
*/
|
|
12
|
+
constructor(concurrency, options = {}) {
|
|
13
|
+
this.concurrency = Math.max(1, concurrency);
|
|
14
|
+
this.onProgress = options.onProgress || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute all tasks with streaming concurrency.
|
|
19
|
+
* Returns results in the same order as the input tasks array.
|
|
20
|
+
*
|
|
21
|
+
* @param {Array} tasks - Array of task items
|
|
22
|
+
* @param {Function} executor - async (task, index) => result
|
|
23
|
+
* @returns {Promise<Array>} Results in input order
|
|
24
|
+
*/
|
|
25
|
+
async runAll(tasks, executor) {
|
|
26
|
+
const total = tasks.length;
|
|
27
|
+
const results = new Array(total);
|
|
28
|
+
let nextIndex = 0;
|
|
29
|
+
let completed = 0;
|
|
30
|
+
let active = 0;
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const startNext = () => {
|
|
34
|
+
while (active < this.concurrency && nextIndex < total) {
|
|
35
|
+
const index = nextIndex++;
|
|
36
|
+
active++;
|
|
37
|
+
|
|
38
|
+
const taskStart = Date.now();
|
|
39
|
+
|
|
40
|
+
executor(tasks[index], index)
|
|
41
|
+
.then((result) => {
|
|
42
|
+
results[index] = result;
|
|
43
|
+
active--;
|
|
44
|
+
completed++;
|
|
45
|
+
|
|
46
|
+
// Start next task BEFORE reporting progress so active count
|
|
47
|
+
// reflects the replacement worker already launched
|
|
48
|
+
if (completed < total) {
|
|
49
|
+
startNext();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.onProgress) {
|
|
53
|
+
try {
|
|
54
|
+
this.onProgress({
|
|
55
|
+
completed,
|
|
56
|
+
total,
|
|
57
|
+
active,
|
|
58
|
+
durationMs: Date.now() - taskStart,
|
|
59
|
+
result,
|
|
60
|
+
error: null,
|
|
61
|
+
task: tasks[index],
|
|
62
|
+
index,
|
|
63
|
+
});
|
|
64
|
+
} catch (_e) {
|
|
65
|
+
// Don't let progress callback errors break the pool
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (completed === total) {
|
|
70
|
+
resolve(results);
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
// Store error as a failed result rather than rejecting the whole pool
|
|
75
|
+
results[index] = { success: false, error: error.message };
|
|
76
|
+
active--;
|
|
77
|
+
completed++;
|
|
78
|
+
|
|
79
|
+
// Start next task BEFORE reporting progress
|
|
80
|
+
if (completed < total) {
|
|
81
|
+
startNext();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.onProgress) {
|
|
85
|
+
try {
|
|
86
|
+
this.onProgress({
|
|
87
|
+
completed,
|
|
88
|
+
total,
|
|
89
|
+
active,
|
|
90
|
+
durationMs: Date.now() - taskStart,
|
|
91
|
+
result: null,
|
|
92
|
+
error,
|
|
93
|
+
task: tasks[index],
|
|
94
|
+
index,
|
|
95
|
+
});
|
|
96
|
+
} catch (_e) {
|
|
97
|
+
// Don't let progress callback errors break the pool
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (completed === total) {
|
|
102
|
+
resolve(results);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (total === 0) {
|
|
109
|
+
resolve([]);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
startNext();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { WorkerPool };
|