pixifact 0.7.0 → 0.9.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.
@@ -9,8 +9,10 @@ const runtimeAnnounceEvent = 'pixifact:runtime:announce';
9
9
  const runtimeRequestEvent = 'pixifact:runtime:request';
10
10
  const runtimeResponseEvent = 'pixifact:runtime:response';
11
11
  const runtimeApiRoot = '/__pixifact_runtime__';
12
+ const runtimeScreenshotPath = `${runtimeApiRoot}/screenshot`;
12
13
  const virtualRuntimeClientId = 'virtual:pixifact-runtime-client';
13
14
  const resolvedVirtualRuntimeClientId = `\0${virtualRuntimeClientId}`;
15
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
14
16
  function isRecord(value) {
15
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
16
18
  }
@@ -57,6 +59,27 @@ function isRuntimeRequest(value) {
57
59
  && typeof value.key === 'string'
58
60
  && value.key.length > 0;
59
61
  }
62
+ function runtimeScreenshotResult(value) {
63
+ if (!isRecord(value)
64
+ || typeof value.runtimeId !== 'string'
65
+ || !Number.isInteger(value.width)
66
+ || Number(value.width) <= 0
67
+ || !Number.isInteger(value.height)
68
+ || Number(value.height) <= 0
69
+ || typeof value.dataUrl !== 'string') {
70
+ return undefined;
71
+ }
72
+ const match = value.dataUrl.match(/^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/);
73
+ const png = match ? Buffer.from(match[1], 'base64') : undefined;
74
+ if (!png || !png.subarray(0, pngSignature.length).equals(pngSignature))
75
+ return undefined;
76
+ return {
77
+ runtimeId: value.runtimeId,
78
+ width: Number(value.width),
79
+ height: Number(value.height),
80
+ png,
81
+ };
82
+ }
60
83
  export function createRuntimeHostSession(options) {
61
84
  const requestTimeoutMs = options.requestTimeoutMs ?? 5_000;
62
85
  const now = options.now ?? (() => new Date());
@@ -96,6 +119,29 @@ export function createRuntimeHostSession(options) {
96
119
  }
97
120
  }
98
121
  }
122
+ function forwardRequest(runtime, request, responseType) {
123
+ const requestId = `runtime-${++requestSequence}`;
124
+ return new Promise((resolve) => {
125
+ const timeout = setTimeout(() => {
126
+ settle(requestId, jsonResponse({
127
+ ok: false,
128
+ error: 'Pixifact Runtime request timed out.',
129
+ }, 504));
130
+ }, requestTimeoutMs);
131
+ pending.set(requestId, {
132
+ resolve,
133
+ runtimeId: runtime.runtimeId,
134
+ socket: runtime.socket,
135
+ responseType,
136
+ timeout,
137
+ });
138
+ runtime.socket.send(runtimeRequestEvent, {
139
+ requestId,
140
+ runtimeId: runtime.runtimeId,
141
+ request,
142
+ });
143
+ });
144
+ }
99
145
  function message(socket, event, data) {
100
146
  if (!sockets.has(socket))
101
147
  return;
@@ -122,9 +168,38 @@ export function createRuntimeHostSession(options) {
122
168
  const current = pending.get(data.requestId);
123
169
  if (!current || current.socket !== socket || current.runtimeId !== data.runtimeId)
124
170
  return;
125
- settle(data.requestId, data.ok
126
- ? jsonResponse(data.result)
127
- : jsonResponse({ ok: false, error: data.error }, 422));
171
+ if (!data.ok) {
172
+ settle(data.requestId, jsonResponse({ ok: false, error: data.error }, 422));
173
+ return;
174
+ }
175
+ if (current.responseType === 'json') {
176
+ settle(data.requestId, jsonResponse(data.result));
177
+ return;
178
+ }
179
+ const screenshot = runtimeScreenshotResult(data.result);
180
+ if (!screenshot) {
181
+ settle(data.requestId, jsonResponse({
182
+ ok: false,
183
+ error: 'Pixifact Runtime returned invalid screenshot data.',
184
+ }, 502));
185
+ return;
186
+ }
187
+ if (screenshot.runtimeId !== data.runtimeId) {
188
+ settle(data.requestId, jsonResponse({
189
+ ok: false,
190
+ error: 'Pixifact Runtime returned a screenshot for a different runtime.',
191
+ }, 502));
192
+ return;
193
+ }
194
+ settle(data.requestId, new Response(screenshot.png, {
195
+ headers: {
196
+ 'cache-control': 'no-store',
197
+ 'content-type': 'image/png',
198
+ 'x-pixifact-height': String(screenshot.height),
199
+ 'x-pixifact-runtime': screenshot.runtimeId,
200
+ 'x-pixifact-width': String(screenshot.width),
201
+ },
202
+ }));
128
203
  }
129
204
  async function fetchRequest(request) {
130
205
  const pathname = new URL(request.url).pathname;
@@ -150,6 +225,23 @@ export function createRuntimeHostSession(options) {
150
225
  .sort((left, right) => left.connectedAt.localeCompare(right.connectedAt)),
151
226
  });
152
227
  }
228
+ if (pathname === runtimeScreenshotPath) {
229
+ if (request.method !== 'POST') {
230
+ return jsonResponse({ ok: false, error: 'Pixifact Runtime screenshot requests require POST.' }, 405);
231
+ }
232
+ const body = await request.json();
233
+ if (!isRecord(body) || typeof body.runtimeId !== 'string') {
234
+ return jsonResponse({ ok: false, error: 'Pixifact Runtime screenshot request is invalid.' }, 400);
235
+ }
236
+ const runtime = runtimes.get(body.runtimeId);
237
+ if (!runtime) {
238
+ return jsonResponse({
239
+ ok: false,
240
+ error: `Pixifact Runtime "${body.runtimeId}" is not connected.`,
241
+ }, 404);
242
+ }
243
+ return forwardRequest(runtime, { type: 'screenshot' }, 'screenshot');
244
+ }
153
245
  if (pathname !== `${runtimeApiRoot}/request`)
154
246
  return undefined;
155
247
  if (request.method !== 'POST') {
@@ -166,26 +258,7 @@ export function createRuntimeHostSession(options) {
166
258
  error: `Pixifact Runtime "${body.runtimeId}" is not connected.`,
167
259
  }, 404);
168
260
  }
169
- const requestId = `runtime-${++requestSequence}`;
170
- return new Promise((resolve) => {
171
- const timeout = setTimeout(() => {
172
- settle(requestId, jsonResponse({
173
- ok: false,
174
- error: 'Pixifact Runtime request timed out.',
175
- }, 504));
176
- }, requestTimeoutMs);
177
- pending.set(requestId, {
178
- resolve,
179
- runtimeId: runtime.runtimeId,
180
- socket: runtime.socket,
181
- timeout,
182
- });
183
- runtime.socket.send(runtimeRequestEvent, {
184
- requestId,
185
- runtimeId: runtime.runtimeId,
186
- request: body.request,
187
- });
188
- });
261
+ return forwardRequest(runtime, body.request, 'json');
189
262
  }
190
263
  return {
191
264
  open,
@@ -1,4 +1,5 @@
1
1
  import { access, readFile, readdir, stat } from 'node:fs/promises';
2
+ import { realpathSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { loadEnv, } from 'vite';
@@ -204,7 +205,7 @@ export function loadPixifactEnv(projectRoot, mode) {
204
205
  }
205
206
  async function configureState(state, options, config, env) {
206
207
  const configuredRoot = options.projectRoot ?? config.root;
207
- const projectRoot = path.resolve(projectRootPath(configuredRoot));
208
+ const projectRoot = realpathSync(path.resolve(projectRootPath(configuredRoot)));
208
209
  const loaded = loadPixifactEnv(projectRoot, env.mode);
209
210
  state.projectRoot = projectRoot;
210
211
  state.mode = env.mode;
@@ -2,4 +2,4 @@ import type { Application } from 'pixi.js';
2
2
  import { type RegisterPixiRuntimeOptions } from './runtimeClient.js';
3
3
  export declare function registerPixiRuntime(app: Application, options?: RegisterPixiRuntimeOptions): void;
4
4
  export type { RegisterPixiRuntimeOptions } from './runtimeClient.js';
5
- export type { RuntimeJsonValue, RuntimeLogLevel, RuntimePageDescriptor, RuntimeRequest, } from './protocol.js';
5
+ export type { RuntimeJsonValue, RuntimeLogLevel, RuntimePageDescriptor, RuntimeObservationRequest, RuntimeRequest, RuntimeScreenshotRequest, RuntimeScreenshotResult, } from './protocol.js';
@@ -22,7 +22,7 @@ export type RuntimeInputRequest = {
22
22
  action: 'key' | 'keydown' | 'keyup';
23
23
  key: string;
24
24
  };
25
- export type RuntimeRequest = {
25
+ export type RuntimeObservationRequest = {
26
26
  type: 'tree';
27
27
  } | {
28
28
  type: 'node';
@@ -34,6 +34,17 @@ export type RuntimeRequest = {
34
34
  after?: number;
35
35
  level?: RuntimeLogLevel;
36
36
  } | RuntimeInputRequest;
37
+ export type RuntimeScreenshotRequest = {
38
+ type: 'screenshot';
39
+ };
40
+ export type RuntimeRequest = RuntimeObservationRequest | RuntimeScreenshotRequest;
41
+ export interface RuntimeScreenshotResult {
42
+ [key: string]: RuntimeJsonValue;
43
+ runtimeId: string;
44
+ width: number;
45
+ height: number;
46
+ dataUrl: string;
47
+ }
37
48
  export interface RuntimePageDescriptor {
38
49
  runtimeId: string;
39
50
  url: string;
@@ -1,4 +1,4 @@
1
- import { BitmapText, Graphics, Sprite, Text, } from 'pixi.js';
1
+ import { BitmapText, Graphics, Rectangle, Sprite, Text, } from 'pixi.js';
2
2
  const maxRetainedLogs = 500;
3
3
  const runtimeRequestEvent = 'pixifact:runtime:request';
4
4
  const runtimeResponseEvent = 'pixifact:runtime:response';
@@ -303,6 +303,23 @@ export function createPixifactRuntimeClient(options) {
303
303
  throw new Error('PixiJS Application has not been registered in this runtime.');
304
304
  return app;
305
305
  }
306
+ async function captureScreenshot(currentApp) {
307
+ const width = Math.max(1, Math.floor(currentApp.screen.width));
308
+ const height = Math.max(1, Math.floor(currentApp.screen.height));
309
+ const dataUrl = await currentApp.renderer.extract.base64({
310
+ target: currentApp.stage,
311
+ frame: new Rectangle(0, 0, width, height),
312
+ resolution: 1,
313
+ format: 'png',
314
+ clearColor: currentApp.renderer.background.colorRgba,
315
+ });
316
+ return {
317
+ runtimeId: options.runtimeId,
318
+ width,
319
+ height,
320
+ dataUrl,
321
+ };
322
+ }
306
323
  async function handleRequest(request) {
307
324
  if (request.type === 'logs') {
308
325
  return {
@@ -343,6 +360,9 @@ export function createPixifactRuntimeClient(options) {
343
360
  state: null,
344
361
  };
345
362
  }
363
+ if (request.type === 'screenshot') {
364
+ return captureScreenshot(currentApp);
365
+ }
346
366
  if (request.type === 'input') {
347
367
  dispatchRuntimeInput(options.window, currentApp, request);
348
368
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixifact",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pixifact 2D UI 和轻量游戏 Scene 语义框架。",
5
5
  "packageManager": "bun@1.3.13",
6
6
  "main": "./dist/index.js",