Files

76 lines
6.3 KiB
JavaScript

// The picker wheel: press the middle mouse button anywhere and every offered action fans out under the pointer; release on one to run it.
import { composer as c } from "../../composer/composer.js";
import { offer } from "../../shell/actions.js";
import { allReferences, clearReferences } from "../../shell/edits.js";
// Saved with the project (a new rental has an empty browser); the browser copy is only the starting point for a project that has no choice yet. enabled null = everything offered except the opt-in actions.
const KEY = "funui_wheel";
const clean = (p) => ({ shape: p.shape === "half" ? "half" : "full", side: p.side === "left" ? "left" : "right", enabled: Array.isArray(p.enabled) ? p.enabled : null });
const fromBrowser = () => { try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch { return {}; } };
export function readPrefs(app) {
const saved = app && app.project && app.project.pref ? app.project.pref("wheel", null) : null;
return clean(saved && typeof saved === "object" ? saved : fromBrowser());
}
function writePrefs(app, p) {
if (app.project && app.project.setPref) app.project.setPref("wheel", p);
try { localStorage.setItem(KEY, JSON.stringify(p)); } catch { /* private window: lasts until reload */ }
}
export default {
id: "wheel",
mount: "menubar.menus",
needs: ["project", "dom"],
setup({ app }) {
let wheel = null;
const offs = [
offer(app, { id: "undo", label: "Undo", icon: "↶", run: () => app.project.undo() }),
offer(app, { id: "redo", label: "Redo", icon: "↷", run: () => app.project.redo() }),
offer(app, { id: "models", label: "Models & Pipeline", icon: "▤", when: () => Boolean(app.openSettings), run: () => app.openSettings("models") }),
offer(app, { id: "engine", label: "Engine settings", icon: "⚙", when: () => Boolean(app.openSettings), run: () => app.openSettings("engine") }),
];
const p = () => app.project, pipe = () => app.pipeline;
const say = (text) => c.toast.info({ text });
const toggles = [
offer(app, { id: "all-off", optIn: true, icon: "⊘", get label() { return pipe().allOff() ? "Enable enhancements" : "Disable all enhancements"; },
when: () => Boolean(pipe() && pipe().allOff && pipe().setAllOff),
run: async () => { const on = !pipe().allOff(); await pipe().setAllOff(on); say(on ? "All enhancements disabled: runs are plain" : "Enhancements enabled again"); } }),
offer(app, { id: "guidance-off", optIn: true, icon: "◌", get label() { return pipe().guidanceOff?.() ? "Enable guidance enhancements" : "Disable all guidance enhancements"; },
when: () => Boolean(pipe() && pipe().guidanceOff && pipe().setGuidanceOff),
run: async () => { const on = !pipe().guidanceOff(); await pipe().setGuidanceOff(on); say(on ? "Guidance enhancements disabled" : "Guidance enhancements enabled again"); } }),
offer(app, { id: "camera-off", optIn: true, icon: "◎", get label() { return pipe().cameraOff?.() ? "Enable camera move enhancements" : "Disable camera move enhancements"; },
when: () => Boolean(pipe() && pipe().cameraOff && pipe().setCameraOff && pipe().modulesById?.().conditioning_shot_camera),
run: async () => { const on = !pipe().cameraOff(); await pipe().setCameraOff(on); say(on ? "Camera move enhancements disabled" : "Camera move enhancements enabled again"); } }),
offer(app, { id: "quick-ab", optIn: true, label: "Quick A/B", icon: "⇄",
when: () => Boolean(app.runner && app.runner.quickAB && pipe() && pipe().setAllOff),
run: () => app.runner.quickAB() }),
offer(app, { id: "clear-refs", optIn: true, label: "Clear references", icon: "⌧",
when: () => Boolean(p() && p().project && allReferences(p().project).size),
run: async () => { p().edit(clearReferences); say("References cleared for every scene"); } }),
offer(app, { id: "fix-seed", optIn: true, icon: "⚄", get label() { return p().pref("random_seed", true) === false ? "Fresh seed each run" : "Fix the seed"; },
when: () => Boolean(p() && p().pref),
run: () => { const fix = p().pref("random_seed", true) !== false; p().setPref("random_seed", !fix); say(fix ? "Seed fixed: the seed node's own number is used" : "A fresh seed on every Generate"); } }),
];
const onDown = (e) => {
if (e.button !== 1 || wheel || app.dom.dialogOpen()) return; // nothing behind a dialog
e.preventDefault();
const prefs = readPrefs(app), on = (a) => prefs.enabled ? prefs.enabled.includes(a.id) : !a.optIn;
const items = (app.actions || []).filter((a) => on(a) && (!a.when || a.when())).slice(0, 12).map((a) => ({ id: a.id, label: a.label, icon: a.icon }));
if (!items.length) return;
const shared = { items, onClose: () => { wheel = null; },
onPick: (item) => { const a = (app.actions || []).find((x) => x.id === item.id); if (a) Promise.resolve().then(() => a.run()).catch((err) => c.toast.warn({ text: err.message })); } };
wheel = prefs.shape === "half" ? c.wheel.half({ ...shared, edge: prefs.side, at: e.clientY }) : c.wheel.picker({ ...shared, x: e.clientX, y: e.clientY });
};
app.editorSettings.push(() => {
const prefs = readPrefs(app), set = (patch) => writePrefs(app, { ...readPrefs(app), ...patch });
const offered = (app.actions || []).map((a) => ({ value: a.id, label: `${a.icon || ""} ${a.label}`.trim() }));
return c.region.stack({ gap: "sm", children: [c.label.section({ text: "Picker wheel" }),
c.hint.default({ text: "Hold the middle mouse button anywhere, flick toward an action and let go." }),
c.segmented.md({ label: "Shape", value: prefs.shape, onChange: (v) => set({ shape: v }), options: [{ value: "full", label: "Full wheel" }, { value: "half", label: "Half wheel" }] }),
c.segmented.md({ label: "A half wheel opens", value: prefs.side, onChange: (v) => set({ side: v }), options: [{ value: "left", label: "Left" }, { value: "right", label: "Right" }] }),
c.checklist.default({ label: "On the wheel", items: offered, values: prefs.enabled || (app.actions || []).filter((a) => !a.optIn).map((a) => a.id), onChange: (v) => set({ enabled: v }) })] });
});
const offDown = app.dom.onPage("mousedown", onDown, true);
return () => { offDown(); if (wheel) wheel.close && wheel.close(); offs.concat(toggles).forEach((f) => f()); };
},
};