@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 +5 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/headlessGpu.js +129 -0
- package/dist/headlessGpu.js.map +1 -0
- package/dist/runHeadless.js +248 -135
- package/dist/runHeadless.js.map +1 -1
- package/dist-runner/assets/cad-html-plugin-BubTPQfK.js +1564 -0
- package/dist-runner/assets/{cad-pdf-plugin-CJkwEiNn.js → cad-pdf-plugin-DZizxUR8.js} +69 -69
- package/dist-runner/assets/cad-svg-plugin-B8nYzClc.js +16 -0
- package/dist-runner/assets/{index-C09XPdM4.js → index-zOqzz2XY.js} +306 -270
- package/dist-runner/index.html +1 -1
- package/dist-runner/viewer-runtime.iife.js +219 -204
- package/dist-runner/workers/libredwg-parser-worker.js +20 -24
- package/dist-runner/workers/libredwg-web.wasm +0 -0
- package/dist-runner/workers/mtext-renderer-worker.js +2987 -2815
- package/examples/batch-export-html-multi-preview.mjs +117 -47
- package/examples/open-quit.scr +2 -0
- package/package.json +10 -10
- package/dist-runner/assets/cad-html-plugin-dtjd-0PT.js +0 -1564
- package/dist-runner/assets/cad-svg-plugin-DxTNeZjG.js +0 -16
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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"}
|
package/dist/runHeadless.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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:
|
|
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
|
-
*
|
|
183
|
-
*
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
324
|
+
const tScript = performance.now();
|
|
241
325
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
292
|
-
await server.close();
|
|
405
|
+
await session.close();
|
|
293
406
|
}
|
|
294
407
|
}
|
|
295
408
|
//# sourceMappingURL=runHeadless.js.map
|