axstack 0.36.1 → 0.38.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 -4
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/host-operations.md +9 -5
- package/docs/installation.md +20 -41
- package/docs/workflows.md +12 -9
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +3 -3
- package/profiles/presets/codex-only.json +3 -3
- package/profiles/presets/mixed.json +5 -4
- package/skills/axstack/references/automations.md +7 -2
- package/skills/axstack/references/lifecycle.md +2 -1
- package/skills/axstack/references/review-manager-prompt.md +44 -3
- package/skills/axstack/references/role-roster.md +13 -6
- package/skills/axstack/references/routing.md +2 -1
- package/skills/axstack/references/run-record.md +2 -0
- package/skills/axstack/references/ui-verification.md +6 -10
- package/skills/axstack-anatomy/ANATOMY.md +172 -0
- package/skills/axstack-anatomy/CHANGELOG.md +70 -0
- package/skills/axstack-anatomy/LICENSE +21 -0
- package/skills/axstack-anatomy/PROVENANCE.md +21 -0
- package/skills/axstack-anatomy/README.md +316 -0
- package/skills/axstack-anatomy/SKILL.md +74 -0
- package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
- package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
- package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
- package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
- package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
- package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
- package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
- package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
- package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
- package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
- package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
- package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
- package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
- package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
- package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
- package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
- package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
- package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
- package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
- package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
- package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
- package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
- package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
- package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
- package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
- package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
- package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
- package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
- package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
- package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
- package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
- package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
- package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
- package/skills/axstack-anatomy/kit/gl.mjs +184 -0
- package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
- package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
- package/skills/axstack-anatomy/kit/iso.css +106 -0
- package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
- package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
- package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
- package/skills/axstack-anatomy/kit/tube.mjs +306 -0
- package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
- package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
- package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
- package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
- package/skills/axstack-anatomy/references/3d.md +352 -0
- package/skills/axstack-anatomy/references/craft.md +325 -0
- package/skills/axstack-anatomy/references/kit.md +419 -0
- package/skills/axstack-anatomy/references/motion.md +165 -0
- package/skills/axstack-anatomy/references/react.md +42 -0
- package/skills/axstack-anatomy/references/verify.md +351 -0
- package/skills/axstack-anatomy/references/walkthrough.md +75 -0
- package/skills/axstack-anatomy/references/webgl.md +473 -0
- package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
- package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
- package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
- package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
- package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
- package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
- package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
- package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
- package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
- package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
- package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
- package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
- package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
- package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
- package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
- package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
- package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
- package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
- package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
- package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
- package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
- package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
- package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
- package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
- package/skills/axstack-anatomy/theme.js +56 -0
- package/skills/axstack-explain/SKILL.md +28 -15
- package/skills/axstack-explain/references/delivery.md +22 -0
- package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
- package/skills/axstack-explain/references/inline-pages.md +8 -8
- package/skills/axstack-explain/references/visual-qa.md +3 -5
- package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
- package/skills/axstack-research/SKILL.md +18 -6
- package/src/archify-legacy.js +70 -0
- package/src/capabilities.js +1 -29
- package/src/installer.js +11 -31
- package/skills/axstack-diagram/SKILL.md +0 -28
- package/skills/axstack-diagram/references/archify.md +0 -76
- package/src/archify-pin.js +0 -5
- package/src/archify.js +0 -151
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import * as G from "../kit/lathe.mjs";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const ts = require("typescript");
|
|
10
|
+
const React = require("react");
|
|
11
|
+
const { renderToStaticMarkup } = require("react-dom/server");
|
|
12
|
+
const directory = mkdtempSync(join(tmpdir(), "anatomy-react-solid-"));
|
|
13
|
+
try {
|
|
14
|
+
for (const file of ["kit/iso-kit.ts", "kit/react/live.tsx", "kit/react/draw.tsx"]) {
|
|
15
|
+
const output = join(directory, file.replace(/\.tsx?$/, ".js"));
|
|
16
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
17
|
+
const result = ts.transpileModule(readFileSync(resolve(file), "utf8"), { fileName: file, compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.React } });
|
|
18
|
+
writeFileSync(output, result.outputText);
|
|
19
|
+
}
|
|
20
|
+
const { Dots } = require(join(directory, "kit/react/draw.js"));
|
|
21
|
+
const P = { origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 };
|
|
22
|
+
test("actual React renderer preserves the kit's faded bolt circle", () => {
|
|
23
|
+
const points = G.dotsOf(G.frameAlong([0, 0, 0], [0, 0, 1]), 2, 4, 32, P, { fade: [0.04, 0.42] });
|
|
24
|
+
assert.ok(points.some((point) => point.length === 3));
|
|
25
|
+
const html = renderToStaticMarkup(React.createElement(Dots, { points }));
|
|
26
|
+
const circles = [...html.matchAll(/<circle[^>]*>/g)].map(([circle]) => circle);
|
|
27
|
+
assert.equal(circles.length, points.length);
|
|
28
|
+
points.forEach((point, index) => {
|
|
29
|
+
if (point[2] === undefined) assert.doesNotMatch(circles[index], /fill-opacity/);
|
|
30
|
+
else assert.equal(Number(circles[index].match(/fill-opacity="([^"]+)"/)[1]), point[2]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
test("actual React renderer preserves opacity while dots pulse", () => {
|
|
34
|
+
const html = renderToStaticMarkup(React.createElement(Dots, { points: [[2, 3, 0.25]], pulse: true }));
|
|
35
|
+
assert.match(html, /fill-opacity="0.25"/);
|
|
36
|
+
assert.match(html, /data-pulse=""/);
|
|
37
|
+
assert.match(html, /--pulse-at:0ms/);
|
|
38
|
+
});
|
|
39
|
+
test("actual React renderer clamps negative opacity to zero", () => {
|
|
40
|
+
const html = renderToStaticMarkup(React.createElement(Dots, { points: [[0, 0, -0.5]] }));
|
|
41
|
+
assert.match(html, /fill-opacity="0"/);
|
|
42
|
+
});
|
|
43
|
+
test("actual React renderer preserves ordinary opaque points", () => {
|
|
44
|
+
const html = renderToStaticMarkup(React.createElement(Dots, { points: [[2.22, 3.33], [4, 5, 1]], size: 0.75, tone: "lo" }));
|
|
45
|
+
assert.doesNotMatch(html, /fill-opacity/);
|
|
46
|
+
assert.match(html, /cx="2.2" cy="3.3" r="0.75"/);
|
|
47
|
+
assert.match(html, /data-tone="lo"/);
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(directory, { recursive: true, force: true });
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { build } = require("esbuild");
|
|
11
|
+
const nodePaths = (process.env.NODE_PATH ?? "").split(":").filter(Boolean);
|
|
12
|
+
const alias = Object.fromEntries(["react", "react-dom"].map(name => [name, dirname(require.resolve(name+"/package.json", { paths: nodePaths }))]));
|
|
13
|
+
const directory = mkdtempSync(join(tmpdir(), "anatomy-react-readout-"));
|
|
14
|
+
let observed;
|
|
15
|
+
try {
|
|
16
|
+
const entry = `import React,{useEffect,useLayoutEffect} from 'react';import {createRoot} from 'react-dom/client';import {flushSync} from 'react-dom';import {useReadout} from ${JSON.stringify(resolve("kit/react/live.tsx"))};
|
|
17
|
+
const changes={};function Probe({mode}){const{anchor,say}=useReadout();useLayoutEffect(()=>{const node=anchor.current.closest('.iso-plate').querySelector('[data-readout]');const observer=new MutationObserver(records=>changes[mode]=(changes[mode]??0)+records.length);observer.observe(node,{childList:true});if(mode==='layout')say('layout');return()=>observer.disconnect();},[]);useEffect(()=>{if(mode==='empty')say('');if(mode==='healthy'||mode==='repeat')say(mode);setTimeout(()=>{if(mode==='layout')say('layout');if(mode==='repeat')say('repeat');},20);},[]);return React.createElement('div',{className:'iso-plate',id:mode},React.createElement('span',{'data-readout':true},'rest'),React.createElement('div',{ref:anchor}));}
|
|
18
|
+
flushSync(()=>createRoot(document.getElementById('root')).render(React.createElement(React.Fragment,null,...['layout','empty','healthy','repeat'].map(mode=>React.createElement(Probe,{key:mode,mode})))));setTimeout(()=>{const values=Object.fromEntries(['layout','empty','healthy','repeat'].map(mode=>[mode,document.querySelector('#'+mode+' [data-readout]').textContent]));window.readoutObserved={values,changes};document.getElementById('results').textContent=JSON.stringify(window.readoutObserved);},100);`;
|
|
19
|
+
await build({ stdin: { contents: entry, resolveDir: process.cwd(), loader: "jsx" }, bundle: true, outfile: join(directory, "fixture.js"), platform: "browser", nodePaths, alias });
|
|
20
|
+
writeFileSync(join(directory, "fixture.html"), '<!doctype html><html><body><div id="root"></div><pre id="results"></pre><script src="fixture.js"></script></body></html>');
|
|
21
|
+
const result = await promisify(execFile)(process.execPath, ["scripts/drive.mjs", join(directory, "fixture.html"), JSON.stringify([["eval", "({observed:window.readoutObserved,html:document.body.innerHTML})"]])], { timeout: 30000, maxBuffer: 16 * 1024 * 1024 });
|
|
22
|
+
const payload = result.stdout.match(/eval: (.*)/);
|
|
23
|
+
assert.ok(payload, "actual ReactDOM client must complete and publish its native DOM observations");
|
|
24
|
+
const inspection = JSON.parse(payload[1]);
|
|
25
|
+
observed = inspection.observed;
|
|
26
|
+
console.log("actual client inspection:", JSON.stringify(inspection));
|
|
27
|
+
assert.ok(observed?.values, `actual native observations are required: ${result.stdout}`);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(directory, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
test("native ReactDOM readout accepts its first empty text", () => assert.equal(observed.values.empty, ""));
|
|
32
|
+
test("native ReactDOM readout accepts layout-effect text before passive binding", () => assert.equal(observed.values.layout, "layout"));
|
|
33
|
+
test("native ReactDOM readout preserves ordinary passive-effect text", () => assert.equal(observed.values.healthy, "healthy"));
|
|
34
|
+
test("native ReactDOM readout preserves duplicate-write suppression", () => {
|
|
35
|
+
assert.equal(observed.values.repeat, "repeat");
|
|
36
|
+
assert.equal(observed.changes.repeat, 1);
|
|
37
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import * as G from "../kit/lathe.mjs";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const ts = require("typescript");
|
|
10
|
+
const React = require("react");
|
|
11
|
+
const { renderToStaticMarkup } = require("react-dom/server");
|
|
12
|
+
const directory = mkdtempSync(join(tmpdir(), "anatomy-react-solid-"));
|
|
13
|
+
try {
|
|
14
|
+
for (const file of ["kit/iso-kit.ts", "kit/react/live.tsx", "kit/react/draw.tsx"]) {
|
|
15
|
+
const output = join(directory, file.replace(/\.tsx?$/, ".js"));
|
|
16
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
17
|
+
const result = ts.transpileModule(readFileSync(resolve(file), "utf8"), { fileName: file, compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.React } });
|
|
18
|
+
writeFileSync(output, result.outputText);
|
|
19
|
+
}
|
|
20
|
+
const { Solid } = require(join(directory, "kit/react/draw.js"));
|
|
21
|
+
const P = { origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 };
|
|
22
|
+
for (const lit of [false, true]) test(`actual React renderer preserves seamless sphere tone regions with lit=${lit}`, () => {
|
|
23
|
+
const paths = G.sphereOf([0, 0, 0], 4, P);
|
|
24
|
+
assert.equal(paths.seamless, true);
|
|
25
|
+
assert.ok(paths.shades.some(Boolean));
|
|
26
|
+
const html = renderToStaticMarkup(React.createElement("svg", null, React.createElement(Solid, { paths, lit })));
|
|
27
|
+
assert.match(html, /data-seamless=""/);
|
|
28
|
+
assert.equal(/data-lit=""/.test(html), lit);
|
|
29
|
+
assert.match(html, /class="iso-shade"/);
|
|
30
|
+
});
|
|
31
|
+
test("actual React renderer preserves ordinary hard solids", () => {
|
|
32
|
+
const paths = G.prismOf(G.frameAlong([0, 0, 0], [0, 0, 1]), [[-1, -1], [1, -1], [1, 1], [-1, 1]], 0, 2, P);
|
|
33
|
+
assert.equal(Boolean(paths.seamless), false);
|
|
34
|
+
const html = renderToStaticMarkup(React.createElement(Solid, { paths }));
|
|
35
|
+
assert.doesNotMatch(html, /data-seamless/);
|
|
36
|
+
assert.match(html, /class="iso-line iso-edge"/);
|
|
37
|
+
});
|
|
38
|
+
test("actual React renderer preserves seamless opt-out and child ordering", () => {
|
|
39
|
+
const paths = { ...G.sphereOf([0, 0, 0], 4, P), seamless: false };
|
|
40
|
+
const html = renderToStaticMarkup(React.createElement(Solid, { paths, inner: React.createElement("circle", { id: "inner" }) }, React.createElement("circle", { id: "child" })));
|
|
41
|
+
assert.doesNotMatch(html, /data-seamless/);
|
|
42
|
+
assert.ok(html.indexOf('id="inner"') < html.indexOf('class="iso-line iso-edge"'));
|
|
43
|
+
assert.ok(html.indexOf('id="child"') > html.indexOf('class="iso-line iso-edge"'));
|
|
44
|
+
});
|
|
45
|
+
} finally {
|
|
46
|
+
rmSync(directory, { recursive: true, force: true });
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { build } = require("esbuild");
|
|
11
|
+
const nodePaths = (process.env.NODE_PATH ?? "").split(":").filter(Boolean);
|
|
12
|
+
const alias = Object.fromEntries(["react", "react-dom"].map(name => [name, dirname(require.resolve(name+"/package.json", { paths: nodePaths }))]));
|
|
13
|
+
const directory = await mkdtemp(join(tmpdir(), "anatomy-visibility-target-"));
|
|
14
|
+
let observed;
|
|
15
|
+
try {
|
|
16
|
+
const entry = `import React,{StrictMode,useRef,useState,useEffect} from 'react';import {createRoot} from 'react-dom/client';import {flushSync} from 'react-dom';import {useInView} from ${JSON.stringify(resolve("kit/react/live.tsx"))};
|
|
17
|
+
function Probe({mode}){const ref=useRef(null);const [phase,setPhase]=useState(false);const visible=useInView(ref);useEffect(()=>{const timer=setTimeout(()=>setPhase(true),150);return()=>clearTimeout(timer)},[]);const absent=mode==='late'&&!phase;const distant=(mode==='swap'&&!phase)||(mode==='moved'&&phase);return React.createElement('section',{id:'probe-'+mode,'data-visible':String(visible),'data-phase':String(phase)},absent?null:React.createElement('div',{ref,key:mode==='swap'?String(phase):'stable','data-target':mode,style:{position:'absolute',top:distant?'5000px':'20px',left:'20px',width:'20px',height:'20px'}},mode));}
|
|
18
|
+
for(const mode of ['swap','late','healthy','moved','strict'])flushSync(()=>createRoot(document.getElementById(mode)).render(mode==='strict'?React.createElement(StrictMode,null,React.createElement(Probe,{mode})):React.createElement(Probe,{mode})));setTimeout(()=>{window.visibilityObserved=Object.fromEntries(['swap','late','healthy','moved','strict'].map(mode=>[mode,{visible:document.querySelector('#probe-'+mode).dataset.visible,phase:document.querySelector('#probe-'+mode).dataset.phase,top:document.querySelector('[data-target="'+mode+'"]').getBoundingClientRect().top}]))},700);`;
|
|
19
|
+
await build({ stdin: { contents: entry, resolveDir: process.cwd(), loader: "jsx" }, bundle: true, outfile: join(directory,"fixture.js"), platform: "browser", nodePaths, alias });
|
|
20
|
+
const page = join(directory,"figure.html");
|
|
21
|
+
await writeFile(page,'<!doctype html><div class="iso-plate"><div id="swap"></div><div id="late"></div><div id="healthy"></div><div id="moved"></div><div id="strict"></div></div><script src="fixture.js"></script>');
|
|
22
|
+
const result = await promisify(execFile)(process.execPath,["scripts/drive.mjs",page,JSON.stringify([["eval","window.visibilityObserved"]])],{timeout:30000});
|
|
23
|
+
const payload = result.stdout.match(/eval: (.*)/);
|
|
24
|
+
assert.ok(payload, result.stdout);
|
|
25
|
+
observed = JSON.parse(payload[1]);
|
|
26
|
+
assert.ok(observed, result.stdout);
|
|
27
|
+
} finally { await rm(directory,{recursive:true,force:true}); }
|
|
28
|
+
test("native visibility observes a replacement node on the same ref",()=>{
|
|
29
|
+
assert.equal(observed.swap.phase,"true");assert.equal(observed.swap.top,20);assert.equal(observed.swap.visible,"true");
|
|
30
|
+
});
|
|
31
|
+
test("native visibility observes a target mounted after the initial empty ref",()=>{
|
|
32
|
+
assert.equal(observed.late.phase,"true");assert.equal(observed.late.top,20);assert.equal(observed.late.visible,"true");
|
|
33
|
+
});
|
|
34
|
+
test("an unchanged visible target stays observed",()=>assert.equal(observed.healthy.visible,"true"));
|
|
35
|
+
test("the same target becoming distant retains real visibility updates",()=>{
|
|
36
|
+
assert.equal(observed.moved.top,5000);assert.equal(observed.moved.visible,"false");
|
|
37
|
+
});
|
|
38
|
+
test("native StrictMode effect replay preserves ordinary observation",()=>assert.equal(observed.strict.visible,"true"));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { TURN } from "../kit/turn.mjs";
|
|
4
|
+
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
if (!argv.length || argv.includes("--help")) {
|
|
7
|
+
console.log(`Time a 3D figure's runtime in Node: emit and order over 360 angles, CPU time, median of 7 rounds.
|
|
8
|
+
|
|
9
|
+
node scripts/turn-bench.mjs examples/turning-dial/build.mjs [--rounds 7] [--unit 1]
|
|
10
|
+
|
|
11
|
+
The build module must export T (from turning()); if it also exports follow(values), every angle is mapped through it.`);
|
|
12
|
+
process.exit(argv.length ? 0 : 1);
|
|
13
|
+
}
|
|
14
|
+
const take = (flag, fallback) => {
|
|
15
|
+
const at = argv.indexOf(flag);
|
|
16
|
+
return at < 0 ? fallback : Number(argv[at + 1]);
|
|
17
|
+
};
|
|
18
|
+
const rounds = take("--rounds", 7);
|
|
19
|
+
const unit = take("--unit", 1);
|
|
20
|
+
const mod = await import(pathToFileURL(resolve(argv[0])).href);
|
|
21
|
+
const T = mod.T;
|
|
22
|
+
const follow = mod.follow ?? ((values) => values);
|
|
23
|
+
const scene = TURN.prepare(T.data, { unit });
|
|
24
|
+
const runs = scene.runs;
|
|
25
|
+
const frames = runs.map((run) => TURN.frame(run));
|
|
26
|
+
const turn = T.groups.find((g) => g.kind === "turn");
|
|
27
|
+
const angles = Array.from({ length: 360 }, (_, i) => i + 0.37);
|
|
28
|
+
const cams = angles.map((angle) => TURN.cams(scene, follow({ [turn.name]: angle })));
|
|
29
|
+
for (let i = 0; i < 20; i++) for (const run of runs) TURN.emit(run, run.group >= 0 ? cams[i][run.group] : cams[i].world, frames[run.index]);
|
|
30
|
+
const perPart = runs.map(() => []);
|
|
31
|
+
const orderTimes = [];
|
|
32
|
+
const frameTimes = [];
|
|
33
|
+
const cpuFrames = [];
|
|
34
|
+
for (let round = 0; round < rounds; round++) {
|
|
35
|
+
const sums = new Float64Array(runs.length);
|
|
36
|
+
let orderSum = 0;
|
|
37
|
+
const cpu0 = process.cpuUsage();
|
|
38
|
+
const wall0 = performance.now();
|
|
39
|
+
const previous = scene.layers.map((layer) => layer.rest);
|
|
40
|
+
for (let a = 0; a < angles.length; a++) {
|
|
41
|
+
const c = cams[a];
|
|
42
|
+
for (const run of runs) {
|
|
43
|
+
const t0 = performance.now();
|
|
44
|
+
TURN.emit(run, run.group >= 0 ? c[run.group] : c.world, frames[run.index]);
|
|
45
|
+
sums[run.index] += performance.now() - t0;
|
|
46
|
+
}
|
|
47
|
+
const t1 = performance.now();
|
|
48
|
+
scene.layers.forEach((layer, index) => {
|
|
49
|
+
previous[index] = TURN.order(layer, frames, c, previous[index], {}, scene);
|
|
50
|
+
});
|
|
51
|
+
orderSum += performance.now() - t1;
|
|
52
|
+
}
|
|
53
|
+
const cpu = process.cpuUsage(cpu0);
|
|
54
|
+
cpuFrames.push((cpu.user + cpu.system) / 1000 / angles.length);
|
|
55
|
+
frameTimes.push((performance.now() - wall0) / angles.length);
|
|
56
|
+
orderTimes.push(orderSum / angles.length);
|
|
57
|
+
runs.forEach((run, index) => perPart[index].push(sums[index] / angles.length));
|
|
58
|
+
}
|
|
59
|
+
const median = (list) => list.slice().sort((x, y) => x - y)[Math.floor(list.length / 2)];
|
|
60
|
+
const kinds = new Map();
|
|
61
|
+
runs.forEach((run, index) => {
|
|
62
|
+
const kind = run.kind === "tube" ? "tube chunk" : run.kind;
|
|
63
|
+
if (!kinds.has(kind)) kinds.set(kind, []);
|
|
64
|
+
kinds.get(kind).push(median(perPart[index]));
|
|
65
|
+
});
|
|
66
|
+
console.log(`turn-bench ${argv[0]} · ${runs.length} parts · ${angles.length} angles · median of ${rounds} rounds · unit ${unit}`);
|
|
67
|
+
for (const [kind, list] of [...kinds].sort((a, b) => b[1].reduce((s, v) => s + v, 0) - a[1].reduce((s, v) => s + v, 0))) {
|
|
68
|
+
const total = list.reduce((s, v) => s + v, 0);
|
|
69
|
+
console.log(` ${kind.padEnd(11)} ${String(list.length).padStart(4)} parts · ${(total / list.length).toFixed(4)} ms per part · ${total.toFixed(3)} ms per frame · slowest ${Math.max(...list).toFixed(4)} ms`);
|
|
70
|
+
}
|
|
71
|
+
const slow = runs.map((run, index) => [run.name, median(perPart[index])]).sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
72
|
+
console.log(` slowest parts: ${slow.map(([name, ms]) => `${name} ${ms.toFixed(3)}`).join(" · ")}`);
|
|
73
|
+
console.log(` order ${median(orderTimes).toFixed(3)} ms per frame (${scene.layers.map((layer) => `${layer.name}: ${layer.parts.length} parts, ${layer.planar.length / 5} planes, ${layer.dynamic.length / 2} dynamic`).join("; ")})`);
|
|
74
|
+
console.log(` frame ${median(frameTimes).toFixed(3)} ms wall · ${median(cpuFrames).toFixed(3)} ms CPU`);
|