multiplane 0.3.0 → 0.3.3

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.en.md CHANGED
@@ -48,9 +48,11 @@ Requires Node.js 24 or later and FFmpeg with libx264 support.
48
48
  npm install multiplane
49
49
  npx playwright install chromium
50
50
  npx multiplane doctor
51
- npx multiplane render node_modules/multiplane/examples/animation.html
51
+ npx multiplane render examples/animation.html --root node_modules/multiplane
52
52
  ```
53
53
 
54
+ The examples live inside the installed package, so `--root` points the resource root at the package; for safety, renders never serve files from under `node_modules`.
55
+
54
56
  Recent npm versions do not run install scripts of dependencies, so the second command downloads the Chromium build that Playwright uses. To work from a clone of this repository instead, run `npm install` in it and render `examples/animation.html` directly.
55
57
 
56
58
  Give your agent the usage skill with `npx multiplane skill install --host all` (Claude Code: `~/.claude/skills/multiplane`, Codex: `~/.agents/skills/multiplane`), or let the desktop app install it. Start a new agent session afterwards.
@@ -252,6 +254,8 @@ npx multiplane stills shot.html --every 0.5
252
254
 
253
255
  ## 3D layer
254
256
 
257
+ The desktop app and its bundled command already include three.js (pinned at 0.180.0) for the 3D layer; nothing else to install. With the npm package, three.js is an optional dependency that projects using the 3D layer install themselves:
258
+
255
259
  ```sh
256
260
  npm install three@0.180.0
257
261
  ```
package/README.md CHANGED
@@ -48,9 +48,11 @@
48
48
  npm install multiplane
49
49
  npx playwright install chromium
50
50
  npx multiplane doctor
51
- npx multiplane render node_modules/multiplane/examples/animation.html
51
+ npx multiplane render examples/animation.html --root node_modules/multiplane
52
52
  ```
53
53
 
54
+ 示例在安装好的包里,所以用 `--root` 把资源根指向包目录;出于安全,渲染时不会提供 `node_modules` 下的文件。
55
+
54
56
  新版 npm 默认不运行依赖包的安装脚本,所以第二条命令用来下载 Playwright 使用的 Chromium。如果是克隆了本仓库,在仓库里运行 `npm install`,直接渲染 `examples/animation.html` 即可。
55
57
 
56
58
  把使用说明 skill 交给你的 agent:运行 `npx multiplane skill install --host all`(Claude Code 装到 `~/.claude/skills/multiplane`,Codex 装到 `~/.agents/skills/multiplane`),或者让桌面软件来安装。装好后新开一个 agent 会话。
@@ -252,6 +254,8 @@ npx multiplane stills shot.html --every 0.5
252
254
 
253
255
  ## 三维层
254
256
 
257
+ 用桌面软件或它自带的命令行渲染时,三维层要用的 three.js(固定 0.180.0)已经带在软件里,不用另装。通过 npm 使用时,three.js 是可选依赖,用三维层的项目自己装:
258
+
255
259
  ```sh
256
260
  npm install three@0.180.0
257
261
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiplane",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -117,7 +117,7 @@ Use `/* multiplane-disable-next-line css_animation */` in CSS and `<!-- multipla
117
117
 
118
118
  ## 3D layer (optional)
119
119
 
120
- Use `multiplane/three` when you need 3D cards or depth of field. Install `three@0.180.0` in the project (`npm install three@0.180.0`).
120
+ Use `multiplane/three` when you need 3D cards or depth of field. When the renderer is the desktop app's bundled command, three.js 0.180.0 is already included. With the npm package, install `three@0.180.0` in the project (`npm install three@0.180.0`). `multiplane doctor` reports whether three.js is available, its version and where it comes from.
121
121
 
122
122
  ```html
123
123
  <script type="importmap">{"imports":{
package/src/capture.mjs CHANGED
@@ -35,6 +35,10 @@ async function openShotOnce(shot, scale = 1, options = {}) {
35
35
  relative = path.relative(root, await normalizeResourcePath(entry));
36
36
  }
37
37
  if (path.extname(relative).toLowerCase() !== '.html') throw invalidOption('shot', 'Shot must be an HTML file.');
38
+ if (relative.split(path.sep).includes('node_modules')) throw new MultiplaneError('shot_outside_root', undefined, {
39
+ details: { path: relative },
40
+ hint: 'Files under node_modules are not served. Use --root to point to the package directory (for example, --root node_modules/multiplane).',
41
+ });
38
42
  try { await access(entry); } catch { throw new MultiplaneError('shot_not_found'); }
39
43
  const server = await startServer(0, root);
40
44
  let browser, page, crashed = false;
@@ -42,7 +46,7 @@ async function openShotOnce(shot, scale = 1, options = {}) {
42
46
  browser = await launchBrowser(options);
43
47
  await options.onBrowser?.(browser);
44
48
  page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: scale });
45
- if (reuseEnabled(options)) await page.addInitScript(installStaticObserver);
49
+ if (reuseEnabled(options)) await page.addInitScript(installStaticObserver, { platform: process.platform });
46
50
  page.on('crash', () => { crashed = true; });
47
51
  const errors = [];
48
52
  const observerProbe = request => {
@@ -57,8 +61,12 @@ async function openShotOnce(shot, scale = 1, options = {}) {
57
61
  page.on('response', response => { if (response.status() >= 400 && !observerProbe(response.request())) errors.push(new MultiplaneError(response.headers()['x-multiplane-error'] === 'three_not_installed' ? 'three_not_installed' : 'resource_failed', undefined, { details: { status: response.status(), url: response.url() } })); });
58
62
  page.on('requestfailed', request => { if (!observerProbe(request)) errors.push(new MultiplaneError('resource_failed', undefined, { details: { url: request.url() } })); });
59
63
  const url = `${server.url}/${relative.split(path.sep).map(encodeURIComponent).join('/')}?mode=render`;
60
- const response = await page.goto(url).catch(cause => { throw new MultiplaneError('shot_load_failed', undefined, { cause }); });
61
- if (!response.ok()) throw new MultiplaneError('shot_load_failed');
64
+ const response = await page.goto(url).catch(cause => {
65
+ const failedResponse = errors.find(error => error.details?.url === url && error.details.status);
66
+ throw new MultiplaneError('shot_load_failed', undefined, { cause,
67
+ details: { path: relative, url, status: failedResponse?.details.status ?? null, reason: cause.message } });
68
+ });
69
+ if (!response?.ok()) throw new MultiplaneError('shot_load_failed', undefined, { details: { path: relative, url, status: response?.status() ?? null } });
62
70
  if (errors.length) throw errors[0];
63
71
  if (!await page.evaluate(() => typeof window.SHOT?.render === 'function')) throw new MultiplaneError('shot_contract_invalid');
64
72
  const metadata = await page.evaluate(async () => {
package/src/doctor.mjs CHANGED
@@ -3,6 +3,7 @@ import { runCommand } from './events.mjs';
3
3
  import { launchBrowser, environment, warnGpuFallback } from './environment.mjs';
4
4
  import { inspectFFmpeg, ffmpegOutput } from './ffmpeg.mjs';
5
5
  import { MultiplaneError } from './errors.mjs';
6
+ import { threeCheck } from './three-package.mjs';
6
7
 
7
8
  export function benchmarkCheck(record, machine) {
8
9
  const current = validBenchmark(record, machine);
@@ -48,8 +49,7 @@ export const doctor = (options = {}) => runCommand('doctor', null, options, asyn
48
49
  for (const name of ['libvpx-vp9', 'prores_ks']) checks.push({ name, ok: false, detail: `${name} is optional; encoder availability could not be inspected.` });
49
50
  }
50
51
  if (info) events.emit('environment', { ...info, ffmpeg: ffmpeg ?? null });
51
- try { await import('three'); checks.push({ name: 'three', ok: true, detail: 'Optional three.js is available.' }); }
52
- catch { checks.push({ name: 'three', ok: false, detail: 'Optional three.js is not installed.' }); }
52
+ checks.push(await threeCheck());
53
53
  if (failure && checks.some(c => ['ffmpeg', 'libx264'].includes(c.name) && !c.ok)) failure.hint = (failure.hint ? failure.hint + ' ' : '') + 'Run multiplane config set ffmpeg <path> to save an FFmpeg executable.';
54
54
  if (failure) { failure.result = { data: { checks, config: events.config } }; throw failure; }
55
55
  return { checks, config: events.config };
package/src/server.mjs CHANGED
@@ -5,6 +5,7 @@ import { stat, readdir } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { MultiplaneError, invalidOption } from './errors.mjs';
8
+ import { resolveThree } from './three-package.mjs';
8
9
 
9
10
  export const root = process.cwd();
10
11
  const packageRoot = fileURLToPath(new URL('../', import.meta.url));
@@ -52,7 +53,7 @@ export async function startServer(port = 0, root = process.cwd()) {
52
53
  if (pathname.startsWith('/multiplane/runtime/')) { resourceRoot = path.join(packageRoot, 'src/runtime'); resourcePath = pathname.slice('/multiplane/runtime'.length); }
53
54
  else if (pathname === '/multiplane/three.js') { resourceRoot = path.join(packageRoot, 'src/three'); resourcePath = '/index.js'; }
54
55
  else if (pathname.startsWith('/multiplane/vendor/three/')) {
55
- resourceRoot = path.dirname(path.dirname(fileURLToPath(import.meta.resolve('three'))));
56
+ resourceRoot = (await resolveThree()).root;
56
57
  resourcePath = pathname.slice('/multiplane/vendor/three'.length);
57
58
  if (!/^\/(build|examples\/jsm)\/.*\.js$/.test(resourcePath)) { res.writeHead(403); return res.end(); }
58
59
  } else { res.writeHead(404); return res.end(); }
@@ -47,7 +47,7 @@ export async function* verifiedFrames(source, options, count, { owned = false }
47
47
  }
48
48
 
49
49
  // Runs before any shot script, so saved drawing functions are instrumented too.
50
- export function installStaticObserver() {
50
+ export function installStaticObserver({ platform } = {}) {
51
51
  let dirty = true, suspended = false, unsafe = false, styleSignature, stylesDirty = true, stylesUnsafe = false, previousSheets = [], readingStyles = false;
52
52
  const exposedDeclarations = new Map();
53
53
  let reason = null;
@@ -248,6 +248,8 @@ export function installStaticObserver() {
248
248
  finally { readingStyles = false; }
249
249
  };
250
250
  const uncertain = () => unsafe ? 'unobservable_api' : !hooks.every(check => check()) ? 'hook_replaced'
251
+ // Native macOS textarea scrollbars can animate outside observable DOM/CSS state.
252
+ : platform === 'darwin' && document.querySelector('textarea') ? 'unobservable_api'
251
253
  : document.querySelector('video,iframe,object,embed,marquee,animate,animateTransform,animateMotion,set') ? 'dynamic_element'
252
254
  : document.activeElement?.matches('input,textarea,[contenteditable]:not([contenteditable="false"])') ? 'editable_focus'
253
255
  : document.getAnimations().some(animation => animation.playState === 'running' || (animation.pending && animation.playState !== 'paused')) || document.fonts.status !== 'loaded' ? 'animation_or_font'
@@ -0,0 +1,26 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Desktop bundles keep the tested subset separate from optional npm peers.
6
+ export async function resolveThree() {
7
+ let root = fileURLToPath(new URL('../vendor/three/', import.meta.url));
8
+ let source = 'bundled';
9
+ let pkg;
10
+ try { pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')); }
11
+ catch (error) {
12
+ if (error.code !== 'ENOENT') throw error;
13
+ root = path.dirname(path.dirname(fileURLToPath(import.meta.resolve('three'))));
14
+ pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
15
+ source = 'project';
16
+ }
17
+ return { root, version: pkg.version, source };
18
+ }
19
+
20
+ export async function threeCheck() {
21
+ try {
22
+ const { root, version, source } = await resolveThree();
23
+ await readFile(path.join(root, 'build/three.module.js'));
24
+ return { name: 'three', ok: true, detail: `three.js ${version} is available (${source === 'bundled' ? 'bundled with software' : 'installed by project'}).` };
25
+ } catch { return { name: 'three', ok: false, detail: 'Optional three.js is not installed.' }; }
26
+ }