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,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Which src/styles.css rules does the Studio still use? Drives /app/ through
|
|
3
|
+
// every v2 surface (modes 1-4 with a character and then a prop selected, the
|
|
4
|
+
// File/Edit/Window/Help menus with Export, the Generate Motion caret, the
|
|
5
|
+
// viewport pills, Preferences on every page, Content and Log, and the start
|
|
6
|
+
// screen) at 1600x1000 and 1280x800 while CDP CSS.startRuleUsageTracking
|
|
7
|
+
// records every style rule that matched. Rules that never matched AND whose
|
|
8
|
+
// class/id names appear in no src/**/*.{js,jsx} file are pruning candidates.
|
|
9
|
+
//
|
|
10
|
+
// QA_URL=http://127.0.0.1:5733/app/?motion=/demo/walk-then-stop.npz CDP_PORT=9733 \
|
|
11
|
+
// OUT=/tmp/css-usage.json node tools/qa-browser.mjs -- node tools/qa/css-rule-usage.mjs
|
|
12
|
+
//
|
|
13
|
+
// Writes OUT (default /tmp/css-rule-usage.json): { states, rules, used,
|
|
14
|
+
// candidates: [{ index, selector, line, endLine }] }. Rule indexes follow
|
|
15
|
+
// postcss walkRules order over src/styles.css.
|
|
16
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import postcss from "postcss";
|
|
19
|
+
|
|
20
|
+
const root = new URL("../..", import.meta.url).pathname;
|
|
21
|
+
const SHEET = "src/styles.css";
|
|
22
|
+
const out = process.env.OUT || "/tmp/css-rule-usage.json";
|
|
23
|
+
const port = Number(process.env.CDP_PORT || 9222);
|
|
24
|
+
const appUrl = new URL(process.env.QA_URL || "http://127.0.0.1:5180/app/");
|
|
25
|
+
|
|
26
|
+
const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
|
|
27
|
+
const page = targets.find((target) => target.type === "page" && target.webSocketDebuggerUrl);
|
|
28
|
+
if (!page) throw new Error("no page target on the QA browser");
|
|
29
|
+
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
30
|
+
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
|
31
|
+
let nextId = 1;
|
|
32
|
+
const pending = new Map();
|
|
33
|
+
const listeners = new Map();
|
|
34
|
+
ws.onmessage = (event) => {
|
|
35
|
+
const message = JSON.parse(event.data);
|
|
36
|
+
if (message.method) for (const listener of listeners.get(message.method) ?? []) listener(message.params);
|
|
37
|
+
if (!message.id || !pending.has(message.id)) return;
|
|
38
|
+
const { resolve, reject } = pending.get(message.id);
|
|
39
|
+
pending.delete(message.id);
|
|
40
|
+
if (message.error) reject(new Error(JSON.stringify(message.error)));
|
|
41
|
+
else resolve(message.result);
|
|
42
|
+
};
|
|
43
|
+
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
44
|
+
const id = nextId++;
|
|
45
|
+
pending.set(id, { resolve, reject });
|
|
46
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
47
|
+
});
|
|
48
|
+
const on = (method, listener) => { const set = listeners.get(method) ?? new Set(); set.add(listener); listeners.set(method, set); };
|
|
49
|
+
const once = (method, timeoutMs = 60000) => new Promise((resolve, reject) => {
|
|
50
|
+
const set = listeners.get(method) ?? new Set();
|
|
51
|
+
listeners.set(method, set);
|
|
52
|
+
const timer = setTimeout(() => { set.delete(handler); reject(new Error(`Missing CDP event ${method}`)); }, timeoutMs);
|
|
53
|
+
const handler = (params) => { set.delete(handler); clearTimeout(timer); resolve(params); };
|
|
54
|
+
set.add(handler);
|
|
55
|
+
});
|
|
56
|
+
const evaluate = async (expression) => {
|
|
57
|
+
const result = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
58
|
+
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || "evaluate failed");
|
|
59
|
+
return result.result.value;
|
|
60
|
+
};
|
|
61
|
+
const waitFor = (expression, timeoutMs = 20000) => evaluate(`new Promise((resolve, reject) => {
|
|
62
|
+
const test = () => { try { return Boolean(${expression}); } catch { return false; } };
|
|
63
|
+
if (test()) { resolve(true); return; }
|
|
64
|
+
const observer = new MutationObserver(() => { if (!test()) return; observer.disconnect(); clearTimeout(timer); resolve(true); });
|
|
65
|
+
observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
66
|
+
const timer = setTimeout(() => { observer.disconnect(); reject(new Error(${JSON.stringify(`Timed out waiting for: ${expression}`)})); }, ${timeoutMs});
|
|
67
|
+
})`);
|
|
68
|
+
// Two frames guarantee a style recalc (and so rule matching) for the new DOM.
|
|
69
|
+
const settle = () => evaluate("new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))");
|
|
70
|
+
const click = async (selector) => {
|
|
71
|
+
const found = await evaluate(`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return false; el.click(); return true; })()`);
|
|
72
|
+
if (!found) throw new Error(`missing control ${selector}`);
|
|
73
|
+
await settle();
|
|
74
|
+
};
|
|
75
|
+
const escape = async () => {
|
|
76
|
+
for (const type of ["keyDown", "keyUp"]) await send("Input.dispatchKeyEvent", { type, key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
77
|
+
await settle();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ---- stylesheet bookkeeping: find the <style> Vite injects for styles.css ----
|
|
81
|
+
const sheets = new Map();
|
|
82
|
+
on("CSS.styleSheetAdded", ({ header }) => sheets.set(header.styleSheetId, header));
|
|
83
|
+
const sheetSource = new Map(); // styleSheetId -> { file, text }
|
|
84
|
+
const sourceOf = async (styleSheetId) => {
|
|
85
|
+
if (sheetSource.has(styleSheetId)) return sheetSource.get(styleSheetId);
|
|
86
|
+
const header = sheets.get(styleSheetId);
|
|
87
|
+
let file = header?.sourceURL ? new URL(header.sourceURL, appUrl).pathname : "";
|
|
88
|
+
if (header?.ownerNode) {
|
|
89
|
+
const { node } = await send("DOM.describeNode", { backendNodeId: header.ownerNode }).catch(() => ({ node: null }));
|
|
90
|
+
const attrs = node?.attributes ?? [];
|
|
91
|
+
const devId = attrs[attrs.indexOf("data-vite-dev-id") + 1];
|
|
92
|
+
if (attrs.includes("data-vite-dev-id") && devId) file = devId;
|
|
93
|
+
}
|
|
94
|
+
const { text } = await send("CSS.getStyleSheetText", { styleSheetId });
|
|
95
|
+
const entry = { file, text };
|
|
96
|
+
sheetSource.set(styleSheetId, entry);
|
|
97
|
+
return entry;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const sourceText = readFileSync(join(root, SHEET), "utf8");
|
|
101
|
+
const sourceRules = [];
|
|
102
|
+
postcss.parse(sourceText).walkRules((rule) => {
|
|
103
|
+
if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
|
|
104
|
+
sourceRules.push(rule);
|
|
105
|
+
});
|
|
106
|
+
const servedIndex = new Map(); // served text -> Map(startOffset -> rule index)
|
|
107
|
+
const indexServed = (text) => {
|
|
108
|
+
if (servedIndex.has(text)) return servedIndex.get(text);
|
|
109
|
+
const byOffset = new Map();
|
|
110
|
+
let index = 0;
|
|
111
|
+
postcss.parse(text).walkRules((rule) => {
|
|
112
|
+
if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
|
|
113
|
+
if (rule.selector !== sourceRules[index]?.selector) throw new Error(`served ${SHEET} diverges from the file at rule ${index}: ${rule.selector}`);
|
|
114
|
+
byOffset.set(rule.source.start.offset, index);
|
|
115
|
+
index += 1;
|
|
116
|
+
});
|
|
117
|
+
if (index !== sourceRules.length) throw new Error(`served ${SHEET} has ${index} rules, the file ${sourceRules.length}`);
|
|
118
|
+
servedIndex.set(text, byOffset);
|
|
119
|
+
return byOffset;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const used = new Set();
|
|
123
|
+
const states = [];
|
|
124
|
+
const collect = async (state) => {
|
|
125
|
+
await settle();
|
|
126
|
+
const { coverage } = await send("CSS.takeCoverageDelta");
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (const usage of coverage) {
|
|
129
|
+
if (!usage.used) continue;
|
|
130
|
+
const { file, text } = await sourceOf(usage.styleSheetId);
|
|
131
|
+
if (!file.endsWith(`/${SHEET}`)) continue;
|
|
132
|
+
const byOffset = indexServed(text);
|
|
133
|
+
// The usage range starts at the selector; allow for leading whitespace.
|
|
134
|
+
let index = byOffset.get(usage.startOffset);
|
|
135
|
+
if (index === undefined) {
|
|
136
|
+
const trimmed = usage.startOffset + (text.slice(usage.startOffset).length - text.slice(usage.startOffset).trimStart().length);
|
|
137
|
+
index = byOffset.get(trimmed);
|
|
138
|
+
}
|
|
139
|
+
// Grouping rules (@layer legacy, @media, @supports) report usage too.
|
|
140
|
+
if (index === undefined && /@[\w-]+\s*$/.test(text.slice(Math.max(0, usage.startOffset - 40), usage.startOffset))) continue;
|
|
141
|
+
if (index === undefined) throw new Error(`no ${SHEET} rule at offset ${usage.startOffset}: ${text.slice(usage.startOffset, usage.startOffset + 60)}`);
|
|
142
|
+
if (!used.has(index)) count += 1;
|
|
143
|
+
used.add(index);
|
|
144
|
+
}
|
|
145
|
+
states.push({ state, newlyUsed: count, totalUsed: used.size });
|
|
146
|
+
console.log(`${state}: +${count} (${used.size}/${sourceRules.length})`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const startTracking = async () => {
|
|
150
|
+
await send("DOM.enable");
|
|
151
|
+
await send("CSS.enable");
|
|
152
|
+
await send("CSS.startRuleUsageTracking").catch(() => {});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
async function load({ session = true, width, height }) {
|
|
156
|
+
await send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: false });
|
|
157
|
+
const seeded = once("Page.loadEventFired");
|
|
158
|
+
await send("Page.navigate", { url: `${appUrl.origin}/favicon.ico` });
|
|
159
|
+
await seeded;
|
|
160
|
+
await evaluate(`(() => {
|
|
161
|
+
localStorage.setItem("cozyclay.locale", "en");
|
|
162
|
+
${session ? `localStorage.setItem("cozyclay.project-session.v1", JSON.stringify({ name: "QA", updatedAt: Date.now() }));` : `localStorage.removeItem("cozyclay.project-session.v1");`}
|
|
163
|
+
return true;
|
|
164
|
+
})()`);
|
|
165
|
+
sheets.clear();
|
|
166
|
+
sheetSource.clear();
|
|
167
|
+
const loaded = once("Page.loadEventFired");
|
|
168
|
+
await send("Page.navigate", { url: appUrl.href });
|
|
169
|
+
await loaded;
|
|
170
|
+
await startTracking();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const modeKeys = ["1", "2", "3", "4"];
|
|
174
|
+
async function walkStudio(label) {
|
|
175
|
+
await waitFor("!!window.__cozyclay?.rigA && window.__cozyclay?.motion?.frames > 0 && !!document.querySelector('[data-node-id=characterA] .hierarchy-row')", 90000);
|
|
176
|
+
await evaluate("window.__cozyclay.pause?.(); true");
|
|
177
|
+
await collect(`${label} loaded`);
|
|
178
|
+
for (const key of modeKeys) {
|
|
179
|
+
await click(`[data-mode-key="${key}"]`);
|
|
180
|
+
await click("[data-node-id=characterA] .hierarchy-row");
|
|
181
|
+
await collect(`${label} mode ${key} + character`);
|
|
182
|
+
}
|
|
183
|
+
// A prop: viewport + Add › Cube selects the new object.
|
|
184
|
+
await click("[data-testid=viewport-add]");
|
|
185
|
+
await waitFor("!!document.querySelector('.viewport-titlebar .add-object-swatch.cube')");
|
|
186
|
+
await collect(`${label} viewport Add menu`);
|
|
187
|
+
await evaluate("document.querySelector('.viewport-titlebar .add-object-swatch.cube').closest('button').click(); true");
|
|
188
|
+
await waitFor("document.querySelector('.inspector-sidebar')?.dataset.inspector?.startsWith('object:')");
|
|
189
|
+
const inspector = await evaluate("document.querySelector('.inspector-sidebar').dataset.inspector");
|
|
190
|
+
for (const key of modeKeys) {
|
|
191
|
+
await click(`[data-mode-key="${key}"]`);
|
|
192
|
+
// Re-select the prop through the Outliner in every mode.
|
|
193
|
+
await evaluate(`(() => { const id = ${JSON.stringify(inspector.slice("object:".length))}; const row = document.querySelector('[data-node-id="' + id + '"] .hierarchy-row') || document.querySelector('[data-node-id="object:' + id + '"] .hierarchy-row'); row?.click(); return !!row; })()`);
|
|
194
|
+
await settle();
|
|
195
|
+
await collect(`${label} mode ${key} + prop`);
|
|
196
|
+
}
|
|
197
|
+
await click('[data-mode-key="1"]');
|
|
198
|
+
await click("[data-node-id=characterA] .hierarchy-row");
|
|
199
|
+
for (const trigger of ["view-camera-trigger", "shading-trigger", "view-menu-trigger"]) {
|
|
200
|
+
if (!await evaluate(`!!document.querySelector('[data-testid=${trigger}]')`)) continue;
|
|
201
|
+
await click(`[data-testid=${trigger}]`);
|
|
202
|
+
await collect(`${label} ${trigger} open`);
|
|
203
|
+
await escape();
|
|
204
|
+
}
|
|
205
|
+
for (const menu of ["file", "edit", "window", "help"]) {
|
|
206
|
+
await click(`[data-testid=menu-${menu}]`);
|
|
207
|
+
await waitFor(`!!document.querySelector('.menubar-menu[data-menu=${menu}]')`);
|
|
208
|
+
if (menu === "file") {
|
|
209
|
+
await click("#export-menu-trigger");
|
|
210
|
+
await waitFor("!!document.querySelector('.export-menu')");
|
|
211
|
+
}
|
|
212
|
+
await collect(`${label} ${menu} menu`);
|
|
213
|
+
await escape();
|
|
214
|
+
if (await evaluate("!!document.querySelector('.menubar-menu')")) await click(".topbar-swatch");
|
|
215
|
+
}
|
|
216
|
+
await click("[data-testid=topbar-generate-menu]");
|
|
217
|
+
await waitFor("!!document.querySelector('[data-generate-action]')");
|
|
218
|
+
await collect(`${label} Generate Motion caret`);
|
|
219
|
+
await escape();
|
|
220
|
+
if (await evaluate("!!document.querySelector('[data-generate-action]')")) await click("[data-testid=topbar-generate-menu]");
|
|
221
|
+
// Preferences through Edit › Preferences…, then every section.
|
|
222
|
+
await click("[data-testid=menu-edit]");
|
|
223
|
+
await waitFor("!!document.querySelector('[data-testid=menu-preferences]')");
|
|
224
|
+
await click("[data-testid=menu-preferences]");
|
|
225
|
+
await waitFor("!!document.querySelector('[data-testid=preferences-nav-item]')");
|
|
226
|
+
const sections = await evaluate("[...document.querySelectorAll('[data-testid=preferences-nav-item]')].map((item) => item.textContent.trim())");
|
|
227
|
+
for (const section of sections) {
|
|
228
|
+
await evaluate(`[...document.querySelectorAll('[data-testid=preferences-nav-item]')].find((item) => item.textContent.trim() === ${JSON.stringify(section)})?.click(); true`);
|
|
229
|
+
await settle();
|
|
230
|
+
await collect(`${label} Preferences › ${section}`);
|
|
231
|
+
}
|
|
232
|
+
await click("[data-testid=preferences-close]");
|
|
233
|
+
for (const tab of ["log", "content"]) {
|
|
234
|
+
await click(`[data-testid=content-tab-${tab}]`);
|
|
235
|
+
await collect(`${label} ${tab} tab`);
|
|
236
|
+
}
|
|
237
|
+
await evaluate("[...document.querySelectorAll('.content-folders [data-testid^=content-folder-]')].map((folder) => folder.dataset.testid)").then(async (folders) => {
|
|
238
|
+
for (const folder of folders) { await click(`[data-testid=${folder}]`); }
|
|
239
|
+
await collect(`${label} Content folders`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await send("Runtime.enable");
|
|
244
|
+
await send("Page.enable");
|
|
245
|
+
await send("Network.enable");
|
|
246
|
+
await send("Network.setBlockedURLs", { urls: ["*cloudflareinsights.com*"] });
|
|
247
|
+
|
|
248
|
+
await load({ width: 1600, height: 1000 });
|
|
249
|
+
await walkStudio("1600");
|
|
250
|
+
await load({ width: 1280, height: 800 });
|
|
251
|
+
await walkStudio("1280");
|
|
252
|
+
await load({ session: false, width: 1440, height: 900 });
|
|
253
|
+
await waitFor("!!document.querySelector('.v2-start-screen')", 60000);
|
|
254
|
+
await collect("start screen");
|
|
255
|
+
|
|
256
|
+
const sourceFiles = [];
|
|
257
|
+
const walk = (dir) => {
|
|
258
|
+
for (const name of readdirSync(dir)) {
|
|
259
|
+
const path = join(dir, name);
|
|
260
|
+
if (statSync(path).isDirectory()) walk(path);
|
|
261
|
+
else if (/\.(jsx?|mjs)$/.test(name)) sourceFiles.push(readFileSync(path, "utf8"));
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
walk(join(root, "src"));
|
|
265
|
+
const sourceBlob = sourceFiles.join("\n");
|
|
266
|
+
const nameUsed = new Map();
|
|
267
|
+
const referenced = (name) => {
|
|
268
|
+
if (!nameUsed.has(name)) nameUsed.set(name, new RegExp(`(?<![\\w-])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`).test(sourceBlob));
|
|
269
|
+
return nameUsed.get(name);
|
|
270
|
+
};
|
|
271
|
+
const lineOf = (offset) => sourceText.slice(0, offset).split("\n").length;
|
|
272
|
+
const candidates = [];
|
|
273
|
+
sourceRules.forEach((rule, index) => {
|
|
274
|
+
if (used.has(index)) return;
|
|
275
|
+
const names = [...new Set([...rule.selector.matchAll(/[.#](-?[_a-zA-Z][\w-]*)/g)].map((match) => match[1]))];
|
|
276
|
+
if (!names.length) return; // element-only rules are never pruned by this pass
|
|
277
|
+
if (names.some(referenced)) return;
|
|
278
|
+
candidates.push({ index, selector: rule.selector.replace(/\s+/g, " "), line: lineOf(rule.source.start.offset), endLine: lineOf(rule.source.end.offset) });
|
|
279
|
+
});
|
|
280
|
+
writeFileSync(out, JSON.stringify({ sheet: SHEET, rules: sourceRules.length, used: used.size, states, candidates }, null, 2));
|
|
281
|
+
console.log(`${SHEET}: ${sourceRules.length} rules, ${used.size} used, ${candidates.length} unused with no src reference -> ${relative(process.cwd(), out)}`);
|
|
282
|
+
ws.close();
|
|
283
|
+
process.exit(0);
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
// Count the Studio
|
|
2
|
-
//
|
|
3
|
-
// docs/studio-ui-ia.md §1 (R6 mode budget: Scene <=35 / Camera <=38 / Motion <=52).
|
|
1
|
+
// Count what the Studio shows at once per workflow mode, screenshot each
|
|
2
|
+
// state, and hold the "controls" count to the v2 budgets (docs/studio-ui-ia.md §1).
|
|
4
3
|
//
|
|
5
|
-
// QA_URL=http://127.0.0.1:5180/app/ CDP_PORT=9241 OUT=/tmp/studio-count \
|
|
4
|
+
// QA_URL=http://127.0.0.1:5180/app/?motion=/demo/walk-then-stop.npz CDP_PORT=9241 OUT=/tmp/studio-count \
|
|
6
5
|
// node tools/qa-browser.mjs -- node tools/qa/studio-control-count.mjs
|
|
7
6
|
//
|
|
8
|
-
// Writes <OUT>-<state>.png and <OUT>-counts.json
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// Writes <OUT>-<state>.png and <OUT>-counts.json (default OUT /tmp/studio-count)
|
|
8
|
+
// and exits 1 when any state has more controls than its budget.
|
|
9
|
+
//
|
|
10
|
+
// Every rendered button/select/range/checkbox/a.topbar-action with a non-zero
|
|
11
|
+
// box is counted (CSS display:none does not count, hover-revealed chevrons do)
|
|
12
|
+
// and split in two:
|
|
13
|
+
// items - data, which grows with the project: Outliner tree rows and their
|
|
14
|
+
// disclosure carets, Content asset tiles and Content category rows;
|
|
15
|
+
// controls - everything else (chrome). Only controls have a budget.
|
|
16
|
+
// "total" (= controls + items) is the pre-v2 metric the R6 targets used.
|
|
17
|
+
|
|
18
|
+
// v2 baseline measured on 40767ed (motion 55 -> 52 after #550 moved generate to the top bar); the pre-v2 R6 targets were 35/45/38/52 (docs/studio-ui-ia.md §1).
|
|
19
|
+
const BUDGETS = { "stage-none": 37, "stage-char": 45, pose: 58, camera: 39, motion: 52 };
|
|
20
|
+
|
|
21
|
+
import { writeFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
const OUT = process.env.OUT || "/tmp/studio-count";
|
|
11
24
|
const port = Number(process.env.CDP_PORT || 9222);
|
|
12
25
|
const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
|
|
13
26
|
const page = targets.find((t) => t.type === "page" && t.webSocketDebuggerUrl);
|
|
@@ -17,47 +30,72 @@ let id = 0; const pending = new Map();
|
|
|
17
30
|
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? m); pending.delete(m.id); } };
|
|
18
31
|
const send = (method, params = {}) => new Promise((r) => { id += 1; pending.set(id, r); ws.send(JSON.stringify({ id, method, params })); });
|
|
19
32
|
const ev = async (x) => { const r = await send("Runtime.evaluate", { expression: x, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description); return r.result?.value; };
|
|
20
|
-
|
|
21
|
-
const
|
|
33
|
+
// A MutationObserver armed before the check resolves on the exact DOM state.
|
|
34
|
+
const waitFor = (expression, timeoutMs = 20000) => ev(`new Promise((resolve, reject) => {
|
|
35
|
+
const test = () => { try { return Boolean(${expression}); } catch { return false; } };
|
|
36
|
+
if (test()) { resolve(true); return; }
|
|
37
|
+
const observer = new MutationObserver(() => { if (!test()) return; observer.disconnect(); clearTimeout(timer); resolve(true); });
|
|
38
|
+
observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
39
|
+
const timer = setTimeout(() => { observer.disconnect(); reject(new Error(${JSON.stringify(`Timed out waiting for: ${expression}`)})); }, ${timeoutMs});
|
|
40
|
+
})`);
|
|
41
|
+
const settle = () => ev("new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))");
|
|
22
42
|
|
|
23
43
|
await send("Emulation.setDeviceMetricsOverride", { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
|
24
44
|
if (process.env.QA_URL) await send("Page.navigate", { url: process.env.QA_URL });
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await sleep(1500);
|
|
28
|
-
// hide the project chooser if present
|
|
29
|
-
await ev(`(()=>{for(const el of document.querySelectorAll("body *")){const text=el.textContent||"";const style=getComputedStyle(el);if(style.position==="fixed"&&/Choose a project to begin|Project name|시작할 프로젝트/i.test(text))el.style.display="none";}return true;})()`);
|
|
45
|
+
await waitFor("!!(window.__cozyclay && window.__cozyclay.rigA && window.__cozyclay.editorCam) && !!document.querySelector('[data-mode-key]') && !!document.querySelector('[data-node-id=characterA] .hierarchy-row')", 60000);
|
|
46
|
+
await ev("window.__cozyclay.pause?.(); true");
|
|
30
47
|
|
|
31
48
|
const COUNT = `(()=>{
|
|
32
49
|
const vis=(el)=>{const r=el.getBoundingClientRect();if(r.width<2||r.height<2)return false;const s=getComputedStyle(el);return s.visibility!=="hidden"&&s.display!=="none"&&s.opacity!=="0";};
|
|
33
50
|
const all=[...document.querySelectorAll("button, select, input[type=range], input[type=checkbox], a.topbar-action")].filter(vis);
|
|
51
|
+
const isItem=(el)=>(!!el.closest("[role=treeitem]")&&(el.classList.contains("hierarchy-row")||el.classList.contains("hierarchy-toggle")))
|
|
52
|
+
||el.matches("[data-testid=content-asset], [data-testid^=content-folder-]");
|
|
34
53
|
const region=(el)=>{
|
|
35
54
|
if(el.closest("header.topbar"))return "topbar";
|
|
36
|
-
if(el.closest(".
|
|
37
|
-
if(el.closest(".
|
|
38
|
-
if(el.closest(".
|
|
39
|
-
if(el.closest("
|
|
40
|
-
if(el.closest(".
|
|
55
|
+
if(el.closest(".v2-outliner, .hierarchy-left"))return "outliner";
|
|
56
|
+
if(el.closest(".inspector-sidebar, .inspector"))return "details";
|
|
57
|
+
if(el.closest(".v2-sequencer, .timeline"))return "sequencer";
|
|
58
|
+
if(el.closest("[data-testid=content-browser], .bottom-dock"))return "content";
|
|
59
|
+
if(el.closest(".v2-statusbar, .brandbar"))return "statusbar";
|
|
60
|
+
if(el.closest(".viewport"))return "viewport";
|
|
41
61
|
return "other:"+(el.closest("aside, section, div[class]")?.className||"").toString().slice(0,30);
|
|
42
62
|
};
|
|
43
|
-
const
|
|
44
|
-
|
|
63
|
+
const name=(el)=>(el.getAttribute("aria-label")||el.textContent||el.tagName).trim().replace(/\\s+/g," ").slice(0,24);
|
|
64
|
+
const by={controls:{},items:{}};let controls=0,items=0;
|
|
65
|
+
for(const el of all){const kind=isItem(el)?"items":"controls";if(kind==="items")items+=1;else controls+=1;(by[kind][region(el)]??=[]).push(name(el));}
|
|
66
|
+
const pose=document.querySelector(".pose-details");
|
|
67
|
+
const poseDetails=pose?[...pose.querySelectorAll("button,select,input[type=range],input[type=checkbox]")].filter(vis).length:null;
|
|
68
|
+
return {controls,items,total:all.length,poseDetails,by};
|
|
45
69
|
})()`;
|
|
46
70
|
|
|
47
|
-
const
|
|
71
|
+
const mode = async (key) => {
|
|
72
|
+
await ev(`document.querySelector('[data-mode-key="${key}"]').click(); true`);
|
|
73
|
+
await waitFor(`document.querySelector('[data-mode-key="${key}"]')?.getAttribute('aria-selected') === 'true'`);
|
|
74
|
+
};
|
|
75
|
+
const selectCharacter = async () => {
|
|
76
|
+
await ev("document.querySelector('[data-node-id=characterA] .hierarchy-row').click(); true");
|
|
77
|
+
await waitFor("document.querySelector('[data-node-id=characterA]')?.getAttribute('aria-selected') === 'true'", 5000);
|
|
78
|
+
};
|
|
48
79
|
const states = [
|
|
49
|
-
["
|
|
50
|
-
["
|
|
51
|
-
|
|
52
|
-
["
|
|
80
|
+
["stage-none", async () => { await mode("1"); await ev("window.__cozyclay.selectHierarchy?.(null); true"); }],
|
|
81
|
+
["stage-char", async () => { await mode("1"); await selectCharacter(); }],
|
|
82
|
+
// Pose as #545 measures it: press 2, IK turns on, Pose Details renders.
|
|
83
|
+
["pose", async () => { await mode("2"); await waitFor("window.__cozyclay.ikMode === true && !!document.querySelector('.pose-details')"); }],
|
|
84
|
+
["camera", async () => { await mode("3"); }],
|
|
85
|
+
["motion", async () => { await mode("4"); await selectCharacter(); }],
|
|
53
86
|
];
|
|
54
87
|
const out = {};
|
|
88
|
+
const over = [];
|
|
55
89
|
for (const [name, setup] of states) {
|
|
56
|
-
await setup(); await
|
|
90
|
+
await setup(); await settle();
|
|
57
91
|
const shot = await send("Page.captureScreenshot", { format: "png" });
|
|
58
|
-
|
|
59
|
-
out[name] = await ev(COUNT);
|
|
60
|
-
|
|
92
|
+
writeFileSync(`${OUT}-${name}.png`, Buffer.from(shot.data, "base64"));
|
|
93
|
+
out[name] = { ...await ev(COUNT), budget: BUDGETS[name] };
|
|
94
|
+
const ok = out[name].controls <= BUDGETS[name];
|
|
95
|
+
if (!ok) over.push(name);
|
|
96
|
+
console.log(`${ok ? "PASS" : "FAIL"} ${name} controls ${out[name].controls} <= ${BUDGETS[name]} (items ${out[name].items}, total ${out[name].total})`);
|
|
61
97
|
}
|
|
62
|
-
|
|
63
|
-
ws.close();
|
|
98
|
+
writeFileSync(`${OUT}-counts.json`, JSON.stringify(out, null, 2));
|
|
99
|
+
ws.close();
|
|
100
|
+
if (over.length) console.log(`over budget: ${over.join(", ")}`);
|
|
101
|
+
process.exit(over.length ? 1 : 0);
|