pixifact-cli 0.11.2 → 0.12.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.
@@ -0,0 +1,103 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createEditorProjectService } from './editorServer';
5
+
6
+ interface SceneScreenshotOptions {
7
+ projectRoot: string;
8
+ scenePath: string;
9
+ }
10
+
11
+ interface BrowserSceneCapture {
12
+ scenePath: string;
13
+ revision: string;
14
+ width: number;
15
+ height: number;
16
+ dataUrl: string;
17
+ }
18
+
19
+ const pngDataUrlPrefix = 'data:image/png;base64,';
20
+
21
+ export async function captureSceneScreenshot(options: SceneScreenshotOptions) {
22
+ const staticRoot = path.resolve(import.meta.dir, '..', 'editor');
23
+ if (!fs.existsSync(path.join(staticRoot, 'capture.html'))) {
24
+ throw new Error('Scene screenshot frontend is not built. Run "bun run editor:frontend:build" first.');
25
+ }
26
+ const service = createEditorProjectService({
27
+ projectRoot: options.projectRoot,
28
+ staticRoot,
29
+ });
30
+ const token = randomBytes(32).toString('hex');
31
+ const server = Bun.serve({
32
+ hostname: '127.0.0.1',
33
+ port: 0,
34
+ fetch(request) {
35
+ if (request.headers.get('x-pixifact-capture-token') !== token) {
36
+ return Response.json({ ok: false, error: 'Scene screenshot request is unauthorized.' }, { status: 403 });
37
+ }
38
+ if (request.method !== 'GET') {
39
+ return Response.json({ ok: false, error: 'Scene screenshot service is read-only.' }, { status: 405 });
40
+ }
41
+ return service.fetch(request);
42
+ },
43
+ });
44
+ try {
45
+ const { chromium } = await import('playwright');
46
+ const browser = await chromium.launch({ channel: 'chrome', headless: true });
47
+ try {
48
+ const origin = `http://127.0.0.1:${server.port}`;
49
+ const context = await browser.newContext();
50
+ await context.route('**/*', (route) => {
51
+ if (new URL(route.request().url()).origin !== origin) return route.continue();
52
+ return route.continue({
53
+ headers: {
54
+ ...route.request().headers(),
55
+ 'x-pixifact-capture-token': token,
56
+ },
57
+ });
58
+ });
59
+ const page = await context.newPage();
60
+ await page.goto(`${origin}/capture.html`);
61
+ let timer: ReturnType<typeof setTimeout> | undefined;
62
+ const timeout = new Promise<never>((_, reject) => {
63
+ timer = setTimeout(() => reject(new Error('Scene 截图超过 60 秒仍未完成。')), 60_000);
64
+ });
65
+ let result: BrowserSceneCapture;
66
+ try {
67
+ result = await Promise.race([
68
+ page.evaluate((scenePath) => (
69
+ window as unknown as { capturePixifactScene: (path: string) => Promise<BrowserSceneCapture> }
70
+ ).capturePixifactScene(scenePath), options.scenePath),
71
+ timeout,
72
+ ]);
73
+ } finally {
74
+ if (timer) clearTimeout(timer);
75
+ }
76
+ if (
77
+ result.scenePath !== options.scenePath
78
+ || !Number.isInteger(result.width)
79
+ || result.width <= 0
80
+ || !Number.isInteger(result.height)
81
+ || result.height <= 0
82
+ || !result.dataUrl.startsWith(pngDataUrlPrefix)
83
+ ) {
84
+ throw new Error('Scene screenshot renderer returned invalid PNG metadata.');
85
+ }
86
+ const data = Buffer.from(result.dataUrl.slice(pngDataUrlPrefix.length), 'base64');
87
+ if (!data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
88
+ throw new Error('Scene screenshot renderer did not return PNG data.');
89
+ }
90
+ return {
91
+ scenePath: result.scenePath,
92
+ revision: result.revision,
93
+ width: result.width,
94
+ height: result.height,
95
+ data,
96
+ };
97
+ } finally {
98
+ await browser.close();
99
+ }
100
+ } finally {
101
+ server.stop(true);
102
+ }
103
+ }
@@ -1 +0,0 @@
1
- import{a as e,c as t,i as n,l as r,o as i,s as a,u as o}from"./index-CSHl3qLL.js";import"./init-CKNzy2w2.js";o.add(t),o.mixin(r,a),o.add(i),o.add(e),o.mixin(r,n);