@mlightcad/cad-simple-viewer-cli 1.7.4 → 1.7.6-preview.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/README.md CHANGED
@@ -68,10 +68,13 @@ npx cad-simple-viewer-cli -i ./drawing.dwg -s ./export-png.scr -o ./out
68
68
  | `--circle-sides <n>` | Max segments for circle tessellation (default: `50` draft) |
69
69
  | `--locale <code>` | Prompt/keyword locale (`en`, `zh`, …) |
70
70
  | `--base-url <url>` | Resource base URL for fonts and templates (`http(s)`). Fonts load from `<url>/fonts/`. Default: CDN `cad-data` |
71
+ | `--disable-gpu` | Force software WebGL (SwiftShader). Without this flag, hardware GPU is preferred and the CLI falls back to software automatically when no usable GPU is present |
71
72
  | `--logfile <path>` | Append start / finish / download log lines |
72
73
 
73
74
  Progressive rendering is always off in CLI mode so drawings open as quickly as possible (headless scripts do not need mid-open paints).
74
75
 
76
+ Headless Chromium prefers a real GPU for WebGL by default. If the machine has no usable GPU (or the driver is blocked), the CLI automatically falls back to software rendering — you do not need `--disable-gpu` for that case.
77
+
75
78
  ## AcCoreConsole mapping
76
79
 
77
80
  | AcCoreConsole | cad-simple-viewer-cli |
@@ -146,6 +149,7 @@ const { outputDir, savedFiles } = await runHeadless({
146
149
  circleSides: 50, // optional
147
150
  baseUrl: 'https://cdn.example.com/cad-data/', // optional
148
151
  locale: 'en', // optional
152
+ useGpu: true, // optional; default true (pass false for --disable-gpu)
149
153
  logfile: './cli.log' // optional
150
154
  })
151
155
 
@@ -155,5 +159,5 @@ console.log(outputDir, savedFiles)
155
159
  ## How it works
156
160
 
157
161
  1. The published package includes a prebuilt Playwright runner (`dist-runner/`).
158
- 2. The CLI starts headless Chromium, opens the drawing (or a blank template) with progressive rendering forced off, waits for entity convert / deferred text geometry, then runs `AcApDocManager.runScript()`.
162
+ 2. The CLI starts headless Chromium (GPU WebGL by default, SwiftShader fallback), opens the drawing (or a blank template) with progressive rendering forced off, waits for entity convert / deferred text geometry, then runs `AcApDocManager.runScript()`.
159
163
  3. Export commands trigger downloads; the CLI writes captured files under `-o`.
package/dist/cli.js CHANGED
@@ -58,6 +58,7 @@ program
58
58
  .option('--circle-sides <n>', 'Max segments for circle tessellation (default: 50 draft)', parseCircleSides)
59
59
  .option('--locale <code>', 'UI locale for prompts/keywords (e.g. en, zh)', 'en')
60
60
  .option('--base-url <url>', 'Resource base URL for fonts and templates (default: CDN cad-data). Fonts load from <url>/fonts/', parseBaseUrl)
61
+ .option('--disable-gpu', 'Force software WebGL (SwiftShader); omit to prefer hardware GPU with automatic fallback')
61
62
  .option('--logfile <path>', 'Append runtime log lines to this file')
62
63
  .action(async (opts) => {
63
64
  try {
@@ -82,6 +83,7 @@ program
82
83
  circleSides: opts.circleSides,
83
84
  baseUrl: opts.baseUrl,
84
85
  locale: opts.locale,
86
+ useGpu: opts.disableGpu ? false : undefined,
85
87
  logfile: opts.logfile ? path.resolve(opts.logfile) : undefined
86
88
  });
87
89
  if (result.savedFiles.length === 0) {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { chromium } from 'playwright';
2
+ /**
3
+ * Hardware backends for this OS, then SwiftShader.
4
+ *
5
+ * Playwright's headless Chromium otherwise stays on SwiftShader even when a
6
+ * GPU is present. Each entry is probed; the first real GPU wins.
7
+ */
8
+ export function gpuBackendOrder(platform = process.platform) {
9
+ switch (platform) {
10
+ case 'win32':
11
+ return ['d3d11', 'd3d9', 'swiftshader'];
12
+ case 'darwin':
13
+ return ['metal', 'gl', 'swiftshader'];
14
+ default:
15
+ return ['vulkan', 'gl', 'egl', 'gles', 'swiftshader'];
16
+ }
17
+ }
18
+ /** True for CPU rasterizers (SwiftShader, llvmpipe, WARP, …). */
19
+ export function isSoftwareWebGlRenderer(renderer) {
20
+ return /swiftshader|llvmpipe|softpipe|lavapipe|microsoft basic render|software raster/i.test(renderer);
21
+ }
22
+ function chromiumArgsForBackend(backend) {
23
+ const shared = ['--ignore-gpu-blocklist', '--enable-gpu', '--enable-webgl'];
24
+ if (backend === 'swiftshader') {
25
+ return [
26
+ ...shared,
27
+ '--use-gl=angle',
28
+ '--use-angle=swiftshader',
29
+ '--enable-unsafe-swiftshader'
30
+ ];
31
+ }
32
+ if (backend === 'egl') {
33
+ return [...shared, '--use-gl=egl'];
34
+ }
35
+ return [...shared, '--use-gl=angle', `--use-angle=${backend}`];
36
+ }
37
+ async function probeWebGlRenderer(browser) {
38
+ const page = await browser.newPage();
39
+ try {
40
+ return await page.evaluate(() => {
41
+ const canvas = document.createElement('canvas');
42
+ const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
43
+ if (!gl)
44
+ return 'unavailable';
45
+ const ext = gl.getExtension('WEBGL_debug_renderer_info');
46
+ const renderer = ext
47
+ ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
48
+ : gl.getParameter(gl.RENDERER);
49
+ return renderer ? String(renderer) : 'unavailable';
50
+ });
51
+ }
52
+ catch {
53
+ return 'unavailable';
54
+ }
55
+ finally {
56
+ await page.close().catch(() => undefined);
57
+ }
58
+ }
59
+ async function launchSwiftShader(base) {
60
+ const browser = await chromium.launch({
61
+ ...base,
62
+ args: chromiumArgsForBackend('swiftshader')
63
+ });
64
+ const renderer = await probeWebGlRenderer(browser);
65
+ return { browser, renderer, backend: 'swiftshader' };
66
+ }
67
+ /**
68
+ * Launches headless Chromium for WebGL.
69
+ *
70
+ * With {@link LaunchHeadlessChromiumOptions.useGpu} `true` (default), probes
71
+ * hardware backends for this OS and falls back to SwiftShader when none work.
72
+ * With `useGpu` `false`, skips the probe and uses SwiftShader immediately.
73
+ */
74
+ export async function launchHeadlessChromium(options = {}) {
75
+ const channel = options.channel;
76
+ const platform = options.platform ?? process.platform;
77
+ const useGpu = options.useGpu !== false;
78
+ const base = {
79
+ headless: true,
80
+ ...(channel ? { channel } : {})
81
+ };
82
+ if (!useGpu) {
83
+ console.log('[cad-simple-viewer-cli] GPU disabled; using software renderer (SwiftShader)');
84
+ return launchSwiftShader(base);
85
+ }
86
+ let software;
87
+ const skipped = [];
88
+ for (const backend of gpuBackendOrder(platform)) {
89
+ let browser;
90
+ try {
91
+ browser = await chromium.launch({
92
+ ...base,
93
+ args: chromiumArgsForBackend(backend)
94
+ });
95
+ }
96
+ catch (error) {
97
+ skipped.push(`${backend} (${error instanceof Error ? error.message : String(error)})`);
98
+ continue;
99
+ }
100
+ const renderer = await probeWebGlRenderer(browser);
101
+ if (renderer === 'unavailable') {
102
+ skipped.push(`${backend} (no WebGL)`);
103
+ await browser.close().catch(() => undefined);
104
+ continue;
105
+ }
106
+ if (!isSoftwareWebGlRenderer(renderer)) {
107
+ if (software) {
108
+ await software.browser.close().catch(() => undefined);
109
+ }
110
+ return { browser, renderer, backend };
111
+ }
112
+ skipped.push(`${backend} (${renderer})`);
113
+ if (!software) {
114
+ software = { browser, renderer, backend };
115
+ }
116
+ else {
117
+ await browser.close().catch(() => undefined);
118
+ }
119
+ }
120
+ if (software) {
121
+ if (skipped.length > 0) {
122
+ console.log(`[cad-simple-viewer-cli] no hardware WebGL; using software renderer. Tried ${skipped.join('; ')}`);
123
+ }
124
+ return software;
125
+ }
126
+ console.log('[cad-simple-viewer-cli] no hardware WebGL; falling back to SwiftShader');
127
+ return launchSwiftShader(base);
128
+ }
129
+ //# sourceMappingURL=headlessGpu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headlessGpu.js","sourceRoot":"","sources":["../src/headlessGpu.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { createServer } from 'node:http';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { chromium } from 'playwright';
6
+ import { launchHeadlessChromium } from './headlessGpu.js';
7
7
  /**
8
8
  * Resolves the `@mlightcad/cad-simple-viewer-cli` package root
9
9
  * (parent of the compiled `dist/` directory).
@@ -82,56 +82,76 @@ function drawingNameFromUrl(urlString) {
82
82
  }
83
83
  return 'drawing.dwg';
84
84
  }
85
- /**
86
- * Loads drawing bytes from a local path or remote URL.
87
- */
88
- async function loadDrawingInput(inputPath) {
89
- if (isHttpUrl(inputPath)) {
90
- const fileName = drawingNameFromUrl(inputPath);
91
- const ext = path.extname(fileName).toLowerCase();
92
- if (ext !== '.dxf' && ext !== '.dwg') {
93
- throw new Error(`Unsupported remote drawing "${fileName}". URL path must end with .dxf or .dwg.`);
94
- }
95
- const response = await fetch(inputPath);
96
- if (!response.ok) {
97
- throw new Error(`Failed to download drawing (${response.status} ${response.statusText}): ${inputPath}`);
98
- }
99
- const bytes = Buffer.from(await response.arrayBuffer());
100
- return {
101
- label: inputPath,
102
- fileName,
103
- base64: bytes.toString('base64')
104
- };
105
- }
106
- const absoluteInput = path.resolve(inputPath);
107
- const ext = path.extname(absoluteInput).toLowerCase();
108
- if (ext !== '.dxf' && ext !== '.dwg') {
109
- throw new Error(`Unsupported file type "${ext}". Only .dxf and .dwg are supported.`);
110
- }
111
- if (!existsSync(absoluteInput)) {
112
- throw new Error(`Input drawing not found: ${absoluteInput}`);
113
- }
114
- const fileBytes = await readFile(absoluteInput);
115
- return {
116
- label: absoluteInput,
117
- fileName: path.basename(absoluteInput),
118
- base64: fileBytes.toString('base64')
85
+ function contentTypeFor(filePath) {
86
+ const ext = path.extname(filePath).toLowerCase();
87
+ const types = {
88
+ '.html': 'text/html; charset=utf-8',
89
+ '.js': 'text/javascript; charset=utf-8',
90
+ '.css': 'text/css; charset=utf-8',
91
+ '.json': 'application/json',
92
+ '.wasm': 'application/wasm',
93
+ '.dwg': 'application/octet-stream',
94
+ '.dxf': 'application/octet-stream'
119
95
  };
96
+ return types[ext] ?? 'application/octet-stream';
97
+ }
98
+ async function readRequestBody(req) {
99
+ const chunks = [];
100
+ for await (const chunk of req) {
101
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
102
+ }
103
+ return Buffer.concat(chunks);
120
104
  }
121
105
  /**
122
- * Starts a loopback HTTP server that serves static files from `root`.
123
- *
124
- * Used to load the Vite-built runner page into Playwright without packing
125
- * assets into a `file://` URL.
126
- *
127
- * @param root - Absolute directory to serve (typically {@link runnerDistDir})
128
- * @returns Server base URL and a `close` function that shuts the listener down
106
+ * Starts a loopback HTTP server for the CLI runner, drawing inputs, and
107
+ * binary download saves (avoids base64 over CDP).
129
108
  */
130
- function startStaticServer(root) {
109
+ function startCliStaticServer(root) {
131
110
  return new Promise((resolve, reject) => {
111
+ const mountedInputs = new Map();
112
+ let saveDir = null;
113
+ let savedFiles = [];
114
+ let mountSeq = 0;
132
115
  const server = createServer((req, res) => {
116
+ void handleRequest(req, res);
117
+ });
118
+ async function handleRequest(req, res) {
133
119
  try {
134
- const urlPath = decodeURIComponent((req.url ?? '/').split('?')[0]);
120
+ const urlPath = decodeURIComponent((req.url ?? '/').split('?')[0] ?? '/');
121
+ if (req.method === 'POST' && urlPath.startsWith('/__cli_save__/')) {
122
+ if (!saveDir) {
123
+ res.writeHead(500);
124
+ res.end('Save directory not configured');
125
+ return;
126
+ }
127
+ const fileName = path.basename(urlPath.slice('/__cli_save__/'.length));
128
+ if (!fileName) {
129
+ res.writeHead(400);
130
+ res.end('Missing file name');
131
+ return;
132
+ }
133
+ const body = await readRequestBody(req);
134
+ const dest = uniqueOutputPath(saveDir, fileName);
135
+ await writeFile(dest, body);
136
+ savedFiles.push(dest);
137
+ res.writeHead(204);
138
+ res.end();
139
+ return;
140
+ }
141
+ if (urlPath.startsWith('/__cli_input__/')) {
142
+ const absolute = mountedInputs.get(urlPath);
143
+ if (!absolute || !existsSync(absolute)) {
144
+ res.writeHead(404);
145
+ res.end();
146
+ return;
147
+ }
148
+ const body = await readFile(absolute);
149
+ res.setHeader('Content-Type', contentTypeFor(absolute));
150
+ res.setHeader('Content-Length', String(body.byteLength));
151
+ res.writeHead(200);
152
+ res.end(body);
153
+ return;
154
+ }
135
155
  const relative = urlPath === '/' ? 'index.html' : urlPath.replace(/^\//, '');
136
156
  const filePath = path.join(root, relative);
137
157
  if (!filePath.startsWith(root)) {
@@ -144,33 +164,42 @@ function startStaticServer(root) {
144
164
  res.end();
145
165
  return;
146
166
  }
147
- const ext = path.extname(filePath).toLowerCase();
148
- const types = {
149
- '.html': 'text/html; charset=utf-8',
150
- '.js': 'text/javascript; charset=utf-8',
151
- '.css': 'text/css; charset=utf-8',
152
- '.json': 'application/json',
153
- '.wasm': 'application/wasm'
154
- };
155
- res.setHeader('Content-Type', types[ext] ?? 'application/octet-stream');
156
- void readFile(filePath).then(body => {
157
- res.writeHead(200);
158
- res.end(body);
159
- });
167
+ res.setHeader('Content-Type', contentTypeFor(filePath));
168
+ const body = await readFile(filePath);
169
+ res.writeHead(200);
170
+ res.end(body);
160
171
  }
161
172
  catch (error) {
162
173
  res.writeHead(500);
163
174
  res.end(String(error));
164
175
  }
165
- });
176
+ }
166
177
  server.listen(0, '127.0.0.1', () => {
167
178
  const address = server.address();
168
179
  if (!address || typeof address === 'string') {
169
180
  reject(new Error('Failed to start static server for CLI runner.'));
170
181
  return;
171
182
  }
183
+ const baseUrl = `http://127.0.0.1:${address.port}`;
172
184
  resolve({
173
- url: `http://127.0.0.1:${address.port}`,
185
+ url: baseUrl,
186
+ mountInput: (absolutePath, _fileName) => {
187
+ const token = String(++mountSeq);
188
+ const urlPath = `/__cli_input__/${token}`;
189
+ mountedInputs.set(urlPath, absolutePath);
190
+ return `${baseUrl}${urlPath}`;
191
+ },
192
+ setSaveDir: dir => {
193
+ saveDir = dir;
194
+ },
195
+ takeSavedFiles: () => {
196
+ const files = savedFiles;
197
+ savedFiles = [];
198
+ return files;
199
+ },
200
+ resetSavedFiles: () => {
201
+ savedFiles = [];
202
+ },
174
203
  close: () => new Promise((closeResolve, closeReject) => {
175
204
  server.close(err => (err ? closeReject(err) : closeResolve()));
176
205
  })
@@ -179,93 +208,135 @@ function startStaticServer(root) {
179
208
  });
180
209
  }
181
210
  /**
182
- * Runs a `.scr` command script in headless Chromium and writes captured
183
- * export downloads into `outputDir`.
184
- *
185
- * When `inputPath` is omitted, the runner starts from a blank ISO template
186
- * (write mode) so scripts can create geometry from scratch. The page waits for
187
- * entity convert / deferred font geometry before executing the script so
188
- * exports such as `pngout` include rendered text.
189
- *
190
- * @param options - Script path, optional input drawing, output dir, locale,
191
- * open mode / view / tessellation options, resource base URL, logfile
192
- * @returns Absolute output directory and list of saved file paths
193
- * @throws If the script or input file is missing, the file type is unsupported,
194
- * the runner build is missing, or the in-page script fails
211
+ * Long-lived headless Chromium session that can run many drawings without
212
+ * relaunching the browser or reloading WASM/fonts for each file.
195
213
  */
196
- export async function runHeadless(options) {
197
- const absoluteScript = path.resolve(options.scriptPath);
198
- if (!existsSync(absoluteScript)) {
199
- throw new Error(`Script not found: ${absoluteScript}`);
200
- }
201
- let absoluteInput;
202
- let fileName = null;
203
- let base64 = null;
204
- if (options.inputPath) {
205
- const drawing = await loadDrawingInput(options.inputPath);
206
- absoluteInput = drawing.label;
207
- fileName = drawing.fileName;
208
- base64 = drawing.base64;
209
- }
210
- const outputDir = path.resolve(options.outputDir ??
211
- (absoluteInput && !isHttpUrl(absoluteInput)
212
- ? path.dirname(absoluteInput)
213
- : process.cwd()));
214
- await mkdir(outputDir, { recursive: true });
215
- const runnerDir = runnerDistDir();
216
- if (!existsSync(path.join(runnerDir, 'index.html'))) {
217
- throw new Error('CLI runner is not built. Run "pnpm --filter @mlightcad/cad-simple-viewer-cli build".');
214
+ export class HeadlessCadSession {
215
+ browser;
216
+ page;
217
+ server;
218
+ launchMs;
219
+ constructor(browser, page, server, launchMs) {
220
+ this.browser = browser;
221
+ this.page = page;
222
+ this.server = server;
223
+ this.launchMs = launchMs;
218
224
  }
219
- const scriptText = await readFile(absoluteScript, 'utf8');
220
- const mode = options.mode ?? (absoluteInput ? 'read' : 'write');
221
- const logfile = options.logfile ? path.resolve(options.logfile) : undefined;
222
- await appendLog(logfile, `[cad-simple-viewer-cli] input=${absoluteInput ?? '(blank)'} script=${absoluteScript} output=${outputDir}`);
223
- const server = await startStaticServer(runnerDir);
224
- const channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
225
- const browser = await chromium.launch({
226
- headless: true,
227
- ...(channel ? { channel } : {})
228
- });
229
- const savedFiles = [];
230
- try {
225
+ static async create(options = {}) {
226
+ const runnerDir = runnerDistDir();
227
+ if (!existsSync(path.join(runnerDir, 'index.html'))) {
228
+ throw new Error('CLI runner is not built. Run "pnpm --filter @mlightcad/cad-simple-viewer-cli build".');
229
+ }
230
+ const t0 = performance.now();
231
+ const server = await startCliStaticServer(runnerDir);
232
+ const channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
233
+ const gpu = await launchHeadlessChromium({
234
+ channel,
235
+ useGpu: options.useGpu
236
+ });
237
+ const browser = gpu.browser;
238
+ console.log(`[cad-simple-viewer-cli] webgl: ${gpu.renderer} (${gpu.backend})`);
231
239
  const context = await browser.newContext({ acceptDownloads: true });
232
240
  const page = await context.newPage();
241
+ page.on('console', msg => {
242
+ const text = msg.text();
243
+ if (text.includes('GL Driver Message')) {
244
+ return;
245
+ }
246
+ if (text.startsWith('[cad-simple-viewer-cli]') ||
247
+ text.startsWith('[chtml]') ||
248
+ msg.type() === 'error' ||
249
+ msg.type() === 'warning') {
250
+ console.log(text);
251
+ }
252
+ });
253
+ page.on('pageerror', error => {
254
+ console.error('[cad-simple-viewer-cli] page error:', error.message);
255
+ });
256
+ page.on('response', response => {
257
+ if (response.status() < 400)
258
+ return;
259
+ console.warn(`[cad-simple-viewer-cli] HTTP ${response.status()} ${response.url()}`);
260
+ });
233
261
  await page.goto(`${server.url}/index.html`, { waitUntil: 'networkidle' });
262
+ const launchMs = performance.now() - t0;
263
+ console.log(`[cad-simple-viewer-cli] browser ready: ${launchMs.toFixed(0)} ms`);
264
+ return new HeadlessCadSession(browser, page, server, launchMs);
265
+ }
266
+ get browserLaunchMs() {
267
+ return this.launchMs;
268
+ }
269
+ async run(options) {
270
+ const absoluteScript = path.resolve(options.scriptPath);
271
+ if (!existsSync(absoluteScript)) {
272
+ throw new Error(`Script not found: ${absoluteScript}`);
273
+ }
274
+ const timings = {
275
+ browserLaunch: this.launchMs
276
+ };
277
+ const runT0 = performance.now();
278
+ let absoluteInput;
279
+ let fileName = null;
280
+ let drawingUrl = null;
281
+ if (options.inputPath) {
282
+ const tLoad = performance.now();
283
+ if (isHttpUrl(options.inputPath)) {
284
+ absoluteInput = options.inputPath;
285
+ fileName = drawingNameFromUrl(options.inputPath);
286
+ const ext = path.extname(fileName).toLowerCase();
287
+ if (ext !== '.dxf' && ext !== '.dwg') {
288
+ throw new Error(`Unsupported remote drawing "${fileName}". URL path must end with .dxf or .dwg.`);
289
+ }
290
+ drawingUrl = options.inputPath;
291
+ }
292
+ else {
293
+ absoluteInput = path.resolve(options.inputPath);
294
+ const ext = path.extname(absoluteInput).toLowerCase();
295
+ if (ext !== '.dxf' && ext !== '.dwg') {
296
+ throw new Error(`Unsupported file type "${ext}". Only .dxf and .dwg are supported.`);
297
+ }
298
+ if (!existsSync(absoluteInput)) {
299
+ throw new Error(`Input drawing not found: ${absoluteInput}`);
300
+ }
301
+ fileName = path.basename(absoluteInput);
302
+ drawingUrl = this.server.mountInput(absoluteInput, fileName);
303
+ }
304
+ timings.mountInput = performance.now() - tLoad;
305
+ }
306
+ const outputDir = path.resolve(options.outputDir ??
307
+ (absoluteInput && !isHttpUrl(absoluteInput)
308
+ ? path.dirname(absoluteInput)
309
+ : process.cwd()));
310
+ await mkdir(outputDir, { recursive: true });
311
+ this.server.setSaveDir(outputDir);
312
+ this.server.resetSavedFiles();
313
+ const scriptText = await readFile(absoluteScript, 'utf8');
314
+ const mode = options.mode ?? (absoluteInput ? 'read' : 'write');
315
+ const logfile = options.logfile ? path.resolve(options.logfile) : undefined;
316
+ await appendLog(logfile, `[cad-simple-viewer-cli] input=${absoluteInput ?? '(blank)'} script=${absoluteScript} output=${outputDir}`);
234
317
  const openExtras = {
235
318
  openViewMode: options.openViewMode,
236
319
  drawNoPlotLayers: options.drawNoPlotLayers,
237
320
  circleSides: options.circleSides,
238
- baseUrl: options.baseUrl
321
+ baseUrl: options.baseUrl,
322
+ saveBaseUrl: `${this.server.url}/__cli_save__/`
239
323
  };
240
- let result;
324
+ const tScript = performance.now();
241
325
  try {
242
- result = await page.evaluate(async ({ name, data, script, locale, mode: openMode, startBlank, openViewMode, drawNoPlotLayers, circleSides, baseUrl }) => {
243
- const runOptions = {
326
+ await this.page.evaluate(async ({ name, drawingUrl: url, script, locale, mode: openMode, startBlank, openViewMode, drawNoPlotLayers, circleSides, baseUrl, saveBaseUrl }) => {
327
+ return window.runCadScript(name, url, script, {
244
328
  locale,
245
329
  mode: openMode,
330
+ startBlank,
246
331
  openViewMode,
247
332
  drawNoPlotLayers,
248
333
  circleSides,
249
- baseUrl
250
- };
251
- if (data == null || name == null) {
252
- return window.runCadScript(null, null, script, {
253
- ...runOptions,
254
- startBlank
255
- });
256
- }
257
- const binary = atob(data);
258
- const bytes = new Uint8Array(binary.length);
259
- for (let i = 0; i < binary.length; i++) {
260
- bytes[i] = binary.charCodeAt(i);
261
- }
262
- return window.runCadScript(name, bytes, script, {
263
- ...runOptions,
264
- startBlank: false
334
+ baseUrl,
335
+ saveBaseUrl
265
336
  });
266
337
  }, {
267
338
  name: fileName,
268
- data: base64,
339
+ drawingUrl,
269
340
  script: scriptText,
270
341
  locale: options.locale,
271
342
  mode,
@@ -278,18 +349,60 @@ export async function runHeadless(options) {
278
349
  await appendLog(logfile, `[cad-simple-viewer-cli] script failed: ${message}`);
279
350
  throw error;
280
351
  }
281
- for (const file of result.files) {
282
- const dest = uniqueOutputPath(outputDir, file.fileName);
283
- await writeFile(dest, Buffer.from(file.base64, 'base64'));
284
- savedFiles.push(dest);
352
+ timings.scriptAndExport = performance.now() - tScript;
353
+ const savedFiles = this.server.takeSavedFiles();
354
+ for (const dest of savedFiles) {
285
355
  await appendLog(logfile, `[cad-simple-viewer-cli] saved ${dest}`);
286
356
  }
287
- await appendLog(logfile, `[cad-simple-viewer-cli] done saved=${savedFiles.length}`);
288
- return { outputDir, savedFiles };
357
+ timings.total = performance.now() - runT0;
358
+ await appendLog(logfile, `[cad-simple-viewer-cli] done saved=${savedFiles.length} totalMs=${timings.total.toFixed(0)}`);
359
+ console.log('[cad-simple-viewer-cli] timings: ' +
360
+ Object.entries(timings)
361
+ .map(([k, v]) => `${k}=${v.toFixed(0)}ms`)
362
+ .join(' '));
363
+ return { outputDir, savedFiles, timings };
364
+ }
365
+ async close() {
366
+ await this.browser.close();
367
+ await this.server.close();
368
+ }
369
+ }
370
+ /**
371
+ * Runs a `.scr` command script in headless Chromium and writes captured
372
+ * export downloads into `outputDir`.
373
+ *
374
+ * Prefer {@link HeadlessCadSession} when exporting many drawings so the
375
+ * browser and WASM parser stay warm across files.
376
+ */
377
+ export async function runHeadless(options) {
378
+ const session = await HeadlessCadSession.create({ useGpu: options.useGpu });
379
+ try {
380
+ return await session.run(options);
381
+ }
382
+ finally {
383
+ await session.close();
384
+ }
385
+ }
386
+ /**
387
+ * Runs the same script against many drawings in one Chromium session.
388
+ *
389
+ * GPU preference is taken from the first job's {@link RunHeadlessOptions.useGpu}
390
+ * (default: hardware when available).
391
+ */
392
+ export async function runHeadlessBatch(jobs) {
393
+ if (jobs.length === 0) {
394
+ return [];
395
+ }
396
+ const session = await HeadlessCadSession.create({ useGpu: jobs[0].useGpu });
397
+ const results = [];
398
+ try {
399
+ for (const job of jobs) {
400
+ results.push(await session.run(job));
401
+ }
402
+ return results;
289
403
  }
290
404
  finally {
291
- await browser.close();
292
- await server.close();
405
+ await session.close();
293
406
  }
294
407
  }
295
408
  //# sourceMappingURL=runHeadless.js.map