cozyclay 1.9.0 → 2.0.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/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Chrome + Vite plumbing for the ground-truth renderer (#428).
|
|
3
|
+
* Same launch/seed pattern as tools/qa-browser.mjs, but owned by one CLI run:
|
|
4
|
+
* every child it starts is terminated on exit or signal.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { spawnOwned, terminateOwned } from "../process-supervisor.mjs";
|
|
10
|
+
|
|
11
|
+
const CHROME_CANDIDATES = [
|
|
12
|
+
process.env.CHROME_PATH,
|
|
13
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
14
|
+
"/usr/bin/google-chrome",
|
|
15
|
+
"/usr/bin/chromium",
|
|
16
|
+
].filter(Boolean);
|
|
17
|
+
|
|
18
|
+
/** Poll `probe` until it resolves truthy; bounded, fails loudly. */
|
|
19
|
+
export async function waitFor(label, probe, { timeoutMs = 60000, intervalMs = 100, child = null } = {}) {
|
|
20
|
+
const deadline = Date.now() + timeoutMs;
|
|
21
|
+
for (;;) {
|
|
22
|
+
if (child && (child.exitCode !== null || child.signalCode !== null)) throw new Error(`${label}: process exited early`);
|
|
23
|
+
try {
|
|
24
|
+
const value = await probe();
|
|
25
|
+
if (value) return value;
|
|
26
|
+
} catch {
|
|
27
|
+
// not ready yet; retried until the deadline
|
|
28
|
+
}
|
|
29
|
+
if (Date.now() >= deadline) throw new Error(`${label}: timed out after ${timeoutMs} ms`);
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function portAnswers(url) {
|
|
35
|
+
try {
|
|
36
|
+
await fetch(url);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Start Vite on `port` in `root` unless `url` is given. */
|
|
44
|
+
export async function startVite({ root, port, children }) {
|
|
45
|
+
const url = `http://127.0.0.1:${port}`;
|
|
46
|
+
if (await portAnswers(url)) throw new Error(`port ${port} is already in use; stop it or pass --url to reuse a running server`);
|
|
47
|
+
const vite = spawnOwned(process.execPath, [join(root, "node_modules/vite/bin/vite.js"), "--host", "127.0.0.1", "--port", String(port), "--strictPort"], {
|
|
48
|
+
cwd: root,
|
|
49
|
+
// Point the Studio's live-control socket at an unused port so it can
|
|
50
|
+
// never attach to another session's hub (the default is 5184).
|
|
51
|
+
env: { ...process.env, COZYCLAY_LIVE_PORT: process.env.COZYCLAY_LIVE_PORT || "5728" },
|
|
52
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
53
|
+
});
|
|
54
|
+
children.push(vite);
|
|
55
|
+
await waitFor("vite startup", async () => (await fetch(`${url}/app/`)).ok, { child: vite });
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function startChrome({ port, children, cleanups }) {
|
|
60
|
+
const chromePath = CHROME_CANDIDATES.find(existsSync);
|
|
61
|
+
if (!chromePath) throw new Error("Google Chrome/Chromium not found; set CHROME_PATH");
|
|
62
|
+
const versionUrl = `http://127.0.0.1:${port}/json/version`;
|
|
63
|
+
if (await portAnswers(versionUrl)) throw new Error(`CDP port ${port} is already in use`);
|
|
64
|
+
const profileDir = mkdtempSync(join(tmpdir(), "cozyclay-gt-render-"));
|
|
65
|
+
cleanups.push(() => rmSync(profileDir, { recursive: true, force: true }));
|
|
66
|
+
const chrome = spawnOwned(chromePath, [
|
|
67
|
+
"--headless=new",
|
|
68
|
+
`--remote-debugging-port=${port}`,
|
|
69
|
+
`--user-data-dir=${profileDir}`,
|
|
70
|
+
"--window-size=1600,1000",
|
|
71
|
+
"--no-first-run",
|
|
72
|
+
"--no-default-browser-check",
|
|
73
|
+
"about:blank",
|
|
74
|
+
], { stdio: ["ignore", "ignore", "ignore"] });
|
|
75
|
+
children.push(chrome);
|
|
76
|
+
await waitFor("chrome CDP startup", async () => (await fetch(versionUrl)).ok, { child: chrome, timeoutMs: 30000 });
|
|
77
|
+
const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
|
|
78
|
+
const page = targets.find((target) => target.type === "page" && target.webSocketDebuggerUrl);
|
|
79
|
+
if (!page) throw new Error("headless Chrome exposed no page target");
|
|
80
|
+
return connect(page.webSocketDebuggerUrl);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Minimal CDP client over Node's global WebSocket. */
|
|
84
|
+
export async function connect(wsUrl) {
|
|
85
|
+
const ws = new WebSocket(wsUrl);
|
|
86
|
+
await new Promise((resolve, reject) => {
|
|
87
|
+
ws.onopen = resolve;
|
|
88
|
+
ws.onerror = () => reject(new Error(`CDP connect failed: ${wsUrl}`));
|
|
89
|
+
});
|
|
90
|
+
let nextId = 1;
|
|
91
|
+
const pending = new Map();
|
|
92
|
+
const listeners = new Set();
|
|
93
|
+
ws.onmessage = (event) => {
|
|
94
|
+
const message = JSON.parse(event.data);
|
|
95
|
+
if (message.id && pending.has(message.id)) {
|
|
96
|
+
const { resolve, reject } = pending.get(message.id);
|
|
97
|
+
pending.delete(message.id);
|
|
98
|
+
if (message.error) reject(new Error(`${message.error.message} (${message.error.code})`));
|
|
99
|
+
else resolve(message.result);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const listener of listeners) listener(message);
|
|
103
|
+
};
|
|
104
|
+
ws.onclose = () => {
|
|
105
|
+
for (const { reject } of pending.values()) reject(new Error("CDP connection closed"));
|
|
106
|
+
pending.clear();
|
|
107
|
+
};
|
|
108
|
+
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
109
|
+
const id = nextId++;
|
|
110
|
+
pending.set(id, { resolve, reject });
|
|
111
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
112
|
+
});
|
|
113
|
+
const evaluate = async (expression) => {
|
|
114
|
+
const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
|
|
115
|
+
if (result.exceptionDetails) {
|
|
116
|
+
throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || "page evaluation failed");
|
|
117
|
+
}
|
|
118
|
+
return result.result?.value;
|
|
119
|
+
};
|
|
120
|
+
/** Resolve on the next event `method`; subscribe BEFORE triggering it. */
|
|
121
|
+
const once = (method, timeoutMs = 60000) => {
|
|
122
|
+
let listener;
|
|
123
|
+
let timer;
|
|
124
|
+
const promise = new Promise((resolve, reject) => {
|
|
125
|
+
listener = (message) => {
|
|
126
|
+
if (message.method === method) resolve(message.params);
|
|
127
|
+
};
|
|
128
|
+
listeners.add(listener);
|
|
129
|
+
timer = setTimeout(() => reject(new Error(`${method}: no event within ${timeoutMs} ms`)), timeoutMs);
|
|
130
|
+
});
|
|
131
|
+
return promise.finally(() => {
|
|
132
|
+
listeners.delete(listener);
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
return { send, evaluate, once, close: () => ws.close() };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Open the Studio on `studioUrl` with a clean origin: storage wiped (so a
|
|
141
|
+
* previous motion's autosaved session cannot come back and mask ?motion=),
|
|
142
|
+
* then the project session seeded so the first-run chooser does not open.
|
|
143
|
+
*/
|
|
144
|
+
export async function openStudioClean(cdp, studioUrl) {
|
|
145
|
+
const origin = new URL(studioUrl).origin;
|
|
146
|
+
await cdp.send("Page.enable");
|
|
147
|
+
await cdp.send("Storage.clearDataForOrigin", { origin, storageTypes: "all" });
|
|
148
|
+
let loaded = cdp.once("Page.loadEventFired");
|
|
149
|
+
await cdp.send("Page.navigate", { url: `${origin}/favicon.ico` });
|
|
150
|
+
await loaded;
|
|
151
|
+
await cdp.evaluate("localStorage.setItem('cozyclay.project-session.v1', JSON.stringify({ name: 'gt-render', updatedAt: Date.now() }))");
|
|
152
|
+
loaded = cdp.once("Page.loadEventFired");
|
|
153
|
+
await cdp.send("Page.navigate", { url: studioUrl });
|
|
154
|
+
await loaded;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function terminateAll(children) {
|
|
158
|
+
await Promise.allSettled(children.splice(0).reverse().map((child) => terminateOwned(child)));
|
|
159
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure camera math for the ground-truth renderer (#428): GVHMR focal
|
|
3
|
+
* conversion, Three.js-compatible intrinsics, static camera placement and
|
|
4
|
+
* pinhole projection. No DOM, no Three, no I/O, so it is unit-tested under
|
|
5
|
+
* Node (test/verify-gt-render-camera.mjs) and `supportValues` is injected
|
|
6
|
+
* verbatim into the Studio page by render.mjs.
|
|
7
|
+
*
|
|
8
|
+
* Conventions (stated once, used everywhere):
|
|
9
|
+
* - World: the Studio's Three.js scene. Right-handed, +Y up, metres.
|
|
10
|
+
* - Studio camera: Three.js PerspectiveCamera, rotation order "YXZ",
|
|
11
|
+
* rotation = (pitch, yaw, 0), looking down its local -Z, local +Y up.
|
|
12
|
+
* `R` below is its camera->world rotation Ry(yaw) * Rx(pitch), row-major.
|
|
13
|
+
* - Azimuth/elevation place the camera around the subject: azimuth 0 puts it
|
|
14
|
+
* on +Z looking toward -Z (the rig's front for a take facing +Z), positive
|
|
15
|
+
* azimuth swings it toward +X; positive elevation raises it and tilts it
|
|
16
|
+
* down. yaw = azimuth, pitch = -elevation.
|
|
17
|
+
* - Image: pixel coordinates are continuous, origin at the top-left corner of
|
|
18
|
+
* the top-left pixel, u to the right, v DOWN. Pixel (col, row) covers
|
|
19
|
+
* [col, col+1) x [row, row+1); its centre is (col + 0.5, row + 0.5).
|
|
20
|
+
* cx = W/2, cy = H/2, square pixels (fx = fy). This is exactly where
|
|
21
|
+
* Three's NDC lands after the viewport transform.
|
|
22
|
+
* - OpenCV extrinsics: X_cv = R_cv * X_world + t_cv with camera x right,
|
|
23
|
+
* y down, z forward; u = fx * X/Z + cx, v = fy * Y/Z + cy.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The 35 mm full-frame diagonal GVHMR's `--f-mm` is measured against. */
|
|
27
|
+
export const FULL_FRAME_DIAGONAL_MM = Math.hypot(24, 36);
|
|
28
|
+
|
|
29
|
+
const DEG = Math.PI / 180;
|
|
30
|
+
|
|
31
|
+
/** Focal length in pixels GVHMR derives from `--f-mm` for a W x H video. */
|
|
32
|
+
export function gvhmrFocalPx(fMm, width, height) {
|
|
33
|
+
return (Math.hypot(width, height) / FULL_FRAME_DIAGONAL_MM) * fMm;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Inverse of gvhmrFocalPx (not rounded). */
|
|
37
|
+
export function gvhmrFMmFromFocalPx(focalPx, width, height) {
|
|
38
|
+
return (focalPx * FULL_FRAME_DIAGONAL_MM) / Math.hypot(width, height);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Three's vertical field of view (degrees) that renders with gvhmrFocalPx. */
|
|
42
|
+
export function fovDegFromFMm(fMm, width, height) {
|
|
43
|
+
return (2 * Math.atan(height / 2 / gvhmrFocalPx(fMm, width, height))) / DEG;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pinhole intrinsics of a Three PerspectiveCamera with vertical `fovDeg`
|
|
47
|
+
* rendered at width x height with aspect = width / height. */
|
|
48
|
+
export function intrinsicsFromFov({ width, height, fovDeg }) {
|
|
49
|
+
const fy = height / 2 / Math.tan((fovDeg * DEG) / 2);
|
|
50
|
+
return { width, height, fovDeg, fx: fy, fy, cx: width / 2, cy: height / 2 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Yaw/pitch (radians) and the camera->world rotation for an orbit angle. */
|
|
54
|
+
export function orbitRotation(azimuthDeg, elevationDeg) {
|
|
55
|
+
const yaw = azimuthDeg * DEG;
|
|
56
|
+
const pitch = -elevationDeg * DEG;
|
|
57
|
+
return { yaw, pitch, R: rotationYXZ(yaw, pitch) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Ry(yaw) * Rx(pitch), row-major 3x3: the camera->world rotation Three
|
|
61
|
+
* builds from Euler(pitch, yaw, 0, "YXZ"). Columns are the camera's x (right),
|
|
62
|
+
* y (up) and z (backward) axes in world space. */
|
|
63
|
+
export function rotationYXZ(yaw, pitch) {
|
|
64
|
+
const cy = Math.cos(yaw);
|
|
65
|
+
const sy = Math.sin(yaw);
|
|
66
|
+
const cp = Math.cos(pitch);
|
|
67
|
+
const sp = Math.sin(pitch);
|
|
68
|
+
return [
|
|
69
|
+
cy, sy * sp, sy * cp,
|
|
70
|
+
0, cp, -sp,
|
|
71
|
+
-sy, cy * sp, cy * cp,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Placement slopes: a point at lateral offset x and depth d is inside the
|
|
76
|
+
* margin box iff kx * |x| <= d (and likewise ky for y). `margin` is the
|
|
77
|
+
* fraction of each image dimension kept clear on every side. */
|
|
78
|
+
export function marginSlopes({ fx, fy, width, height }, margin) {
|
|
79
|
+
if (!(margin >= 0 && margin < 0.5)) throw new Error(`margin must be in [0, 0.5), got ${margin}`);
|
|
80
|
+
return { kx: fx / ((0.5 - margin) * width), ky: fy / ((0.5 - margin) * height) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Support values of a point set for the camera solve, in the camera's
|
|
85
|
+
* rotated frame p' = R^T * P. Exact reduction: the minimal camera distance
|
|
86
|
+
* over any set is a function of these five maxima alone, so the page reduces
|
|
87
|
+
* every skinned vertex of a frame to five numbers.
|
|
88
|
+
*
|
|
89
|
+
* Self-contained on purpose (no outer references): render.mjs injects its
|
|
90
|
+
* source into the browser with Function.prototype.toString.
|
|
91
|
+
*
|
|
92
|
+
* @param {ArrayLike<number>} points flat [x0, y0, z0, x1, ...] in world metres
|
|
93
|
+
* @param {number[]} R row-major camera->world rotation
|
|
94
|
+
* @returns {{ axp: number, axm: number, ayp: number, aym: number, zmax: number, count: number }}
|
|
95
|
+
*/
|
|
96
|
+
export function supportValues(points, R, kx, ky) {
|
|
97
|
+
let axp = -Infinity;
|
|
98
|
+
let axm = -Infinity;
|
|
99
|
+
let ayp = -Infinity;
|
|
100
|
+
let aym = -Infinity;
|
|
101
|
+
let zmax = -Infinity;
|
|
102
|
+
const count = Math.floor(points.length / 3);
|
|
103
|
+
for (let i = 0; i < count; i += 1) {
|
|
104
|
+
const x = points[i * 3];
|
|
105
|
+
const y = points[i * 3 + 1];
|
|
106
|
+
const z = points[i * 3 + 2];
|
|
107
|
+
const px = R[0] * x + R[3] * y + R[6] * z;
|
|
108
|
+
const py = R[1] * x + R[4] * y + R[7] * z;
|
|
109
|
+
const pz = R[2] * x + R[5] * y + R[8] * z;
|
|
110
|
+
if (pz + kx * px > axp) axp = pz + kx * px;
|
|
111
|
+
if (pz - kx * px > axm) axm = pz - kx * px;
|
|
112
|
+
if (pz + ky * py > ayp) ayp = pz + ky * py;
|
|
113
|
+
if (pz - ky * py > aym) aym = pz - ky * py;
|
|
114
|
+
if (pz > zmax) zmax = pz;
|
|
115
|
+
}
|
|
116
|
+
return { axp, axm, ayp, aym, zmax, count };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Union of support values (the support of the union of the point sets). */
|
|
120
|
+
export function mergeSupports(list) {
|
|
121
|
+
const out = { axp: -Infinity, axm: -Infinity, ayp: -Infinity, aym: -Infinity, zmax: -Infinity, count: 0 };
|
|
122
|
+
for (const s of list) {
|
|
123
|
+
for (const key of ["axp", "axm", "ayp", "aym", "zmax"]) out[key] = Math.max(out[key], s[key]);
|
|
124
|
+
out.count += s.count;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The closest static camera with orientation R that keeps every supported
|
|
131
|
+
* point inside the margin box. In the rotated frame the camera sits at
|
|
132
|
+
* (tx, ty, D); a point p' has depth D - p'z and lateral offset p'x - tx, so
|
|
133
|
+
* the box constraint is D >= p'z + kx * |p'x - tx|. Maximised over the set
|
|
134
|
+
* that is max(axp - kx*tx, axm + kx*tx), minimised at tx = (axp - axm)/(2kx):
|
|
135
|
+
* the extreme points end up symmetric about the image centre.
|
|
136
|
+
*
|
|
137
|
+
* @returns {{ position: {x,y,z}, rotated: [number, number, number], bindingAxis: "x"|"y"|"depth" }}
|
|
138
|
+
*/
|
|
139
|
+
export function solvePlacement(support, R, { kx, ky }, minDepth = 0.3) {
|
|
140
|
+
if (!(support.count > 0)) throw new Error("camera placement needs at least one point");
|
|
141
|
+
const tx = (support.axp - support.axm) / (2 * kx);
|
|
142
|
+
const ty = (support.ayp - support.aym) / (2 * ky);
|
|
143
|
+
const dx = (support.axp + support.axm) / 2;
|
|
144
|
+
const dy = (support.ayp + support.aym) / 2;
|
|
145
|
+
const dz = support.zmax + minDepth;
|
|
146
|
+
const D = Math.max(dx, dy, dz);
|
|
147
|
+
const bindingAxis = D === dx ? "x" : D === dy ? "y" : "depth";
|
|
148
|
+
const rotated = [tx, ty, D];
|
|
149
|
+
return { position: applyRotation(R, rotated), rotated, bindingAxis };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyRotation(R, [x, y, z]) {
|
|
153
|
+
return {
|
|
154
|
+
x: R[0] * x + R[1] * y + R[2] * z,
|
|
155
|
+
y: R[3] * x + R[4] * y + R[5] * z,
|
|
156
|
+
z: R[6] * x + R[7] * y + R[8] * z,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** OpenCV world->camera 4x4 (row-major): camera x right, y down, z forward. */
|
|
161
|
+
export function worldToCameraCv(R, position) {
|
|
162
|
+
// R_cv = diag(1, -1, -1) * R^T
|
|
163
|
+
const Rcv = [
|
|
164
|
+
R[0], R[3], R[6],
|
|
165
|
+
-R[1], -R[4], -R[7],
|
|
166
|
+
-R[2], -R[5], -R[8],
|
|
167
|
+
];
|
|
168
|
+
const t = [0, 1, 2].map((row) => -(Rcv[row * 3] * position.x + Rcv[row * 3 + 1] * position.y + Rcv[row * 3 + 2] * position.z));
|
|
169
|
+
return [
|
|
170
|
+
[Rcv[0], Rcv[1], Rcv[2], t[0]],
|
|
171
|
+
[Rcv[3], Rcv[4], Rcv[5], t[1]],
|
|
172
|
+
[Rcv[6], Rcv[7], Rcv[8], t[2]],
|
|
173
|
+
[0, 0, 0, 1],
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Three/OpenGL view matrix (camera.matrixWorldInverse) as row-major 4x4:
|
|
178
|
+
* camera x right, y up, looking down -z. */
|
|
179
|
+
export function worldToCameraGl(R, position) {
|
|
180
|
+
const t = [0, 1, 2].map((col) => -(R[col] * position.x + R[3 + col] * position.y + R[6 + col] * position.z));
|
|
181
|
+
return [
|
|
182
|
+
[R[0], R[3], R[6], t[0]],
|
|
183
|
+
[R[1], R[4], R[7], t[1]],
|
|
184
|
+
[R[2], R[5], R[8], t[2]],
|
|
185
|
+
[0, 0, 0, 1],
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Project a world point with a camera record ({ fx, fy, cx, cy,
|
|
190
|
+
* worldToCameraCv }). Returns [u, v, depth] in the image convention above;
|
|
191
|
+
* depth <= 0 means behind the camera. */
|
|
192
|
+
export function projectPoint([x, y, z], camera) {
|
|
193
|
+
const M = camera.worldToCameraCv;
|
|
194
|
+
const X = M[0][0] * x + M[0][1] * y + M[0][2] * z + M[0][3];
|
|
195
|
+
const Y = M[1][0] * x + M[1][1] * y + M[1][2] * z + M[1][3];
|
|
196
|
+
const Z = M[2][0] * x + M[2][1] * y + M[2][2] * z + M[2][3];
|
|
197
|
+
return [camera.fx * (X / Z) + camera.cx, camera.fy * (Y / Z) + camera.cy, Z];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Full static camera for a set of support values: intrinsics from an integer
|
|
202
|
+
* GVHMR f-mm, orientation from azimuth/elevation, position from the solve.
|
|
203
|
+
* The returned record is what camera.json stores.
|
|
204
|
+
*/
|
|
205
|
+
export function buildCamera({ width, height, fMm, azimuthDeg, elevationDeg, margin, support, minDepth = 0.3 }) {
|
|
206
|
+
if (!Number.isInteger(fMm) || fMm <= 0) throw new Error(`f-mm must be a positive integer (GVHMR --f-mm), got ${fMm}`);
|
|
207
|
+
const fovDeg = fovDegFromFMm(fMm, width, height);
|
|
208
|
+
const intrinsics = intrinsicsFromFov({ width, height, fovDeg });
|
|
209
|
+
const { yaw, pitch, R } = orbitRotation(azimuthDeg, elevationDeg);
|
|
210
|
+
const slopes = marginSlopes(intrinsics, margin);
|
|
211
|
+
const placement = solvePlacement(support, R, slopes, minDepth);
|
|
212
|
+
return {
|
|
213
|
+
...intrinsics,
|
|
214
|
+
gvhmrFMm: fMm,
|
|
215
|
+
azimuthDeg,
|
|
216
|
+
elevationDeg,
|
|
217
|
+
margin,
|
|
218
|
+
position: placement.position,
|
|
219
|
+
yaw,
|
|
220
|
+
pitch,
|
|
221
|
+
bindingAxis: placement.bindingAxis,
|
|
222
|
+
cameraToWorldRotation: R,
|
|
223
|
+
worldToCameraCv: worldToCameraCv(R, placement.position),
|
|
224
|
+
worldToCameraGl: worldToCameraGl(R, placement.position),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Rebuild the camera record from a camera.json written by render.mjs, so a
|
|
230
|
+
* later render uses that exact camera (--camera). The pose is taken from
|
|
231
|
+
* position/yaw/pitch/fovDeg and checked against the stored intrinsics and
|
|
232
|
+
* worldToCamera, so a file in another convention is refused, not reinterpreted.
|
|
233
|
+
*/
|
|
234
|
+
export function cameraFromRecord(record) {
|
|
235
|
+
const { width, height, fovDeg, position, yaw, pitch } = record ?? {};
|
|
236
|
+
const finite = [width, height, fovDeg, yaw, pitch, position?.x, position?.y, position?.z].every(Number.isFinite);
|
|
237
|
+
if (!finite) throw new Error("camera.json needs width, height, fovDeg, yaw, pitch and position {x, y, z}");
|
|
238
|
+
const intrinsics = intrinsicsFromFov({ width, height, fovDeg });
|
|
239
|
+
for (const key of ["fx", "fy", "cx", "cy"]) {
|
|
240
|
+
if (record[key] !== undefined && Math.abs(record[key] - intrinsics[key]) > 1e-6) throw new Error(`camera.json ${key} ${record[key]} does not match fovDeg (${intrinsics[key]})`);
|
|
241
|
+
}
|
|
242
|
+
const R = rotationYXZ(yaw, pitch);
|
|
243
|
+
const camera = {
|
|
244
|
+
...intrinsics,
|
|
245
|
+
gvhmrFMm: record.gvhmrFMm ?? null,
|
|
246
|
+
azimuthDeg: record.azimuthDeg ?? null,
|
|
247
|
+
elevationDeg: record.elevationDeg ?? null,
|
|
248
|
+
margin: record.margin ?? null,
|
|
249
|
+
position: { x: position.x, y: position.y, z: position.z },
|
|
250
|
+
yaw,
|
|
251
|
+
pitch,
|
|
252
|
+
bindingAxis: record.bindingAxis ?? "given",
|
|
253
|
+
cameraToWorldRotation: R,
|
|
254
|
+
worldToCameraCv: worldToCameraCv(R, position),
|
|
255
|
+
worldToCameraGl: worldToCameraGl(R, position),
|
|
256
|
+
};
|
|
257
|
+
if (record.worldToCamera) {
|
|
258
|
+
const gap = Math.max(...record.worldToCamera.flatMap((row, r) => row.map((value, c) => Math.abs(value - camera.worldToCameraCv[r][c]))));
|
|
259
|
+
if (!(gap <= 1e-9)) throw new Error(`camera.json worldToCamera differs from its position/yaw/pitch by ${gap}`);
|
|
260
|
+
}
|
|
261
|
+
return camera;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The same camera moved by `offset` (world metres); orientation unchanged. */
|
|
265
|
+
export function translateCamera(camera, offset) {
|
|
266
|
+
const position = { x: camera.position.x + offset.x, y: camera.position.y + offset.y, z: camera.position.z + offset.z };
|
|
267
|
+
const R = camera.cameraToWorldRotation;
|
|
268
|
+
return { ...camera, position, worldToCameraCv: worldToCameraCv(R, position), worldToCameraGl: worldToCameraGl(R, position) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The minimal support-value arguments a caller needs for a camera orientation. */
|
|
272
|
+
export function supportArgs({ width, height, fMm, azimuthDeg, elevationDeg, margin }) {
|
|
273
|
+
const intrinsics = intrinsicsFromFov({ width, height, fovDeg: fovDegFromFMm(fMm, width, height) });
|
|
274
|
+
const { R } = orbitRotation(azimuthDeg, elevationDeg);
|
|
275
|
+
return { R, ...marginSlopes(intrinsics, margin) };
|
|
276
|
+
}
|