mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
a8ad02739a
commit
af2949677a
+20
-20
@@ -15,7 +15,7 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
||||
|
||||
from safepath import safe_join
|
||||
|
||||
log = logging.getLogger("slopsmith.plugins")
|
||||
log = logging.getLogger("feedBack.plugins")
|
||||
|
||||
|
||||
PLUGINS_DIR = Path(__file__).parent
|
||||
@@ -43,7 +43,7 @@ PLUGINS_LOCK = threading.RLock()
|
||||
# registry mutation (the pending seed, _graduate, _mark_failed) re-checks it
|
||||
# under the lock before touching LOADED_PLUGINS / PENDING_PLUGINS. This keeps a
|
||||
# still-running loader from an EARLIER pass — e.g. a "reload plugins" action,
|
||||
# SLOPSMITH_SYNC_STARTUP hot-reload, or test teardown re-invoking load_plugins()
|
||||
# FEEDBACK_SYNC_STARTUP hot-reload, or test teardown re-invoking load_plugins()
|
||||
# while the first pass's background install thread is mid-flight — from
|
||||
# repopulating or duplicating entries after a NEWER pass has already cleared the
|
||||
# registries. Only the latest pass is allowed to publish.
|
||||
@@ -512,7 +512,7 @@ def _capability_warnings(manifest: dict, plugin_id: str) -> tuple[dict, list[dic
|
||||
if isinstance(declaration.get("provider_policy"), dict):
|
||||
clean["provider_policy"] = declaration["provider_policy"]
|
||||
# Declarative per-instance control descriptors a consuming host renders
|
||||
# generically (slopsmith#849). Domain-agnostic: validated for any
|
||||
# generically (feedBack#849). Domain-agnostic: validated for any
|
||||
# capability here and surfaced via /api/plugins; each domain defines how
|
||||
# a value is applied (visualization is the first consumer).
|
||||
if clean_settings:
|
||||
@@ -567,7 +567,7 @@ def _load_plugin_sibling(plugin_id: str, plugin_dir: Path, name: str):
|
||||
import precedence). Mirrors the routes-loading pattern in
|
||||
`load_plugins()` and shares its `sys.modules` cache, so two plugins
|
||||
that each ship `extractor.py` get distinct cached modules instead
|
||||
of stomping each other through `sys.path`. See slopsmith#33."""
|
||||
of stomping each other through `sys.path`. See feedBack#33."""
|
||||
if not isinstance(plugin_id, str) or not plugin_id:
|
||||
raise ValueError(
|
||||
f"load_sibling: plugin_id must be a non-empty string, got {plugin_id!r}"
|
||||
@@ -613,7 +613,7 @@ def _load_plugin_sibling(plugin_id: str, plugin_dir: Path, name: str):
|
||||
# sys.modules entry — same key load_sibling produces
|
||||
# `setdefault` is atomic under the GIL so two threads racing to
|
||||
# create the parent can't overwrite each other's registration.
|
||||
# Spotted by codex/Copilot reviews on PRs for slopsmith#33.
|
||||
# Spotted by codex/Copilot reviews on PRs for feedBack#33.
|
||||
import types
|
||||
new_parent = types.ModuleType(parent_name)
|
||||
new_parent.__path__ = [str(plugin_dir)]
|
||||
@@ -641,14 +641,14 @@ def _warn_on_module_collisions(plugin_specs):
|
||||
"""Scan top-level importable modules across all plugins about to
|
||||
be loaded. Print a warning for any module name shipped by 2+
|
||||
plugins, since bare `import <name>` from those plugins will hit
|
||||
the sys.path-based cache and cross-load (slopsmith#33).
|
||||
the sys.path-based cache and cross-load (feedBack#33).
|
||||
|
||||
Both top-level `.py` files AND top-level packages (directories
|
||||
containing `__init__.py`) are scanned — the same collision
|
||||
pattern applies to either, e.g. one plugin's `extractor.py` vs
|
||||
another plugin's `extractor/__init__.py` both produce a shared
|
||||
`sys.modules['extractor']` entry. Spotted by codex review on
|
||||
PR for slopsmith#33.
|
||||
PR for feedBack#33.
|
||||
|
||||
`routes.py` itself is excluded because the loader already
|
||||
namespaces it as `plugin_{id}_routes`. Top-level dunder files
|
||||
@@ -663,7 +663,7 @@ def _warn_on_module_collisions(plugin_specs):
|
||||
# — that intra-plugin layout is supported by load_sibling
|
||||
# (package form wins, matching CPython precedence) and shouldn't
|
||||
# trip a cross-plugin collision warning. Spotted by codex review
|
||||
# on PR for slopsmith#33.
|
||||
# on PR for feedBack#33.
|
||||
by_name: dict[str, dict[str, set[str]]] = {}
|
||||
for plugin_id, plugin_dir in plugin_specs:
|
||||
try:
|
||||
@@ -700,7 +700,7 @@ def _warn_on_module_collisions(plugin_specs):
|
||||
log.warning(
|
||||
"Module-name collision: %r (%s) is shipped by %d plugins (%s). "
|
||||
"Bare `import %s` may load the wrong file. "
|
||||
"Migrate to context['load_sibling']('%s') — see CLAUDE.md (slopsmith#33).",
|
||||
"Migrate to context['load_sibling']('%s') — see CLAUDE.md (feedBack#33).",
|
||||
name, kind_label, len(by_plugin), ids_quoted, name, name,
|
||||
)
|
||||
|
||||
@@ -745,7 +745,7 @@ def _is_valid_tour_manifest(val) -> bool:
|
||||
def _normalize_export_paths(settings_field, plugin_id: str) -> list[str]:
|
||||
"""Validate and normalize a plugin's `settings.server_files` manifest
|
||||
list into clean POSIX-style relpaths suitable for the settings
|
||||
export/import bundle (slopsmith#113).
|
||||
export/import bundle (feedBack#113).
|
||||
|
||||
Each entry must be a non-empty string with no absolute prefix and
|
||||
no `..` segment. A trailing `/` denotes a directory (recurse on
|
||||
@@ -1118,7 +1118,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
|
||||
# Collect plugin directories — user plugins first so they override built-in
|
||||
plugin_dirs = []
|
||||
user_plugins_dir = os.environ.get("SLOPSMITH_PLUGINS_DIR")
|
||||
user_plugins_dir = os.environ.get("FEEDBACK_PLUGINS_DIR") or os.environ.get("SLOPSMITH_PLUGINS_DIR")
|
||||
if user_plugins_dir:
|
||||
user_path = Path(user_plugins_dir)
|
||||
if user_path.is_dir() and user_path != PLUGINS_DIR:
|
||||
@@ -1179,7 +1179,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
)
|
||||
|
||||
# Two-pass discovery so we can warn about cross-plugin module-name
|
||||
# collisions BEFORE any plugin's setup runs (slopsmith#33). The
|
||||
# collisions BEFORE any plugin's setup runs (feedBack#33). The
|
||||
# first pass collects (plugin_id, plugin_dir, manifest) tuples in
|
||||
# load order; the second pass actually executes each plugin's
|
||||
# setup with a per-plugin context.
|
||||
@@ -1231,7 +1231,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
kept_is_bundled = _is_bundled(kept[1], kept[2]) if kept else False
|
||||
if this_is_bundled and not kept_is_bundled:
|
||||
# The incoming copy is the canonical bundled plugin; the
|
||||
# already-kept copy is user-installed (SLOPSMITH_PLUGINS_DIR
|
||||
# already-kept copy is user-installed (FEEDBACK_PLUGINS_DIR
|
||||
# or cloned directly into plugins/). Bundled always wins —
|
||||
# evict the user copy and fall through to register the
|
||||
# bundled version instead.
|
||||
@@ -1537,7 +1537,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
continue
|
||||
|
||||
# Add plugin directory to sys.path so the plugin's bare
|
||||
# `import sibling` keeps working during the slopsmith#33
|
||||
# `import sibling` keeps working during the feedBack#33
|
||||
# transition. New plugins should prefer
|
||||
# `context['load_sibling']('sibling')` instead — see
|
||||
# CLAUDE.md / Plugin System / Backend routes.
|
||||
@@ -1556,13 +1556,13 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
# bijectively encoded by _safe_plugin_id_for_module_name:
|
||||
# `_` -> `_5f_`, `.` -> `_2e_`) so two plugins shipping the
|
||||
# same filename get distinct cached modules. See
|
||||
# slopsmith#33.
|
||||
# feedBack#33.
|
||||
plugin_context = dict(context)
|
||||
plugin_context["load_sibling"] = (
|
||||
lambda name, _pid=plugin_id, _pdir=plugin_dir:
|
||||
_load_plugin_sibling(_pid, _pdir, name)
|
||||
)
|
||||
plugin_context["log"] = logging.getLogger(f"slopsmith.plugin.{plugin_id}")
|
||||
plugin_context["log"] = logging.getLogger(f"feedBack.plugin.{plugin_id}")
|
||||
if callable(plugin_context.get("register_library_provider")):
|
||||
_register_library_provider = plugin_context["register_library_provider"]
|
||||
|
||||
@@ -1709,9 +1709,9 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
# Normalized list of relpaths under CONFIG_DIR that this
|
||||
# plugin opts in to settings export/import. Empty for
|
||||
# plugins that don't declare `settings.server_files`. See
|
||||
# slopsmith#113.
|
||||
# feedBack#113.
|
||||
"_export_paths": _normalize_export_paths(manifest.get("settings"), plugin_id),
|
||||
# Diagnostics opt-in (slopsmith#166): same allowlist semantics
|
||||
# Diagnostics opt-in (feedBack#166): same allowlist semantics
|
||||
# as `_export_paths` but for the troubleshooting bundle.
|
||||
"_diagnostics_paths": _normalize_diagnostics_paths(manifest.get("diagnostics"), plugin_id),
|
||||
"_diagnostics_callable_spec": _parse_diagnostics_callable(manifest.get("diagnostics"), plugin_id),
|
||||
@@ -1800,7 +1800,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
|
||||
lambda name, _pid=evicted_id, _pdir=ev_dir:
|
||||
_load_plugin_sibling(_pid, _pdir, name)
|
||||
)
|
||||
ev_context["log"] = logging.getLogger(f"slopsmith.plugin.{evicted_id}")
|
||||
ev_context["log"] = logging.getLogger(f"feedBack.plugin.{evicted_id}")
|
||||
if callable(ev_context.get("register_library_provider")):
|
||||
_ev_register_library_provider = ev_context["register_library_provider"]
|
||||
|
||||
@@ -2053,7 +2053,7 @@ def register_plugin_api(app: FastAPI):
|
||||
"category": p.get("category") if "category" in p else ((p.get("_manifest") or {}).get("category") or None),
|
||||
"icon": p.get("icon") if "icon" in p else ((p.get("_manifest") or {}).get("icon") or None),
|
||||
# `bundled` is reserved metadata flagging plugins that
|
||||
# ship with the default container image (slopsmith#160).
|
||||
# ship with the default container image (feedBack#160).
|
||||
# Surfaced in /api/plugins so the plugin-list UI can
|
||||
# render a "Bundled" badge (lock icon) next to the
|
||||
# plugin name in the settings collapsible.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
id: 'library-provider',
|
||||
selector: '#lib-provider',
|
||||
title: 'Choose a library',
|
||||
content: 'Use this menu to switch between your local library and any connected remote libraries. Slopsmith remembers the last library you picked.',
|
||||
content: 'Use this menu to switch between your local library and any connected remote libraries. FeedBack remembers the last library you picked.',
|
||||
shape: 'spotlight',
|
||||
position: 'bottom',
|
||||
waitFor: '#lib-provider'
|
||||
@@ -57,20 +57,20 @@
|
||||
|
||||
function _register() {
|
||||
try {
|
||||
window.slopsmithTour.register(PLUGIN_ID, { screens: SCREENS, buildSteps: _buildSteps });
|
||||
window.feedBackTour.register(PLUGIN_ID, { screens: SCREENS, buildSteps: _buildSteps });
|
||||
} catch (e) {
|
||||
console.warn('[app_tour_library] register failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') {
|
||||
if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
|
||||
_register();
|
||||
} else {
|
||||
// Engine inits on DOMContentLoaded after fetching /api/plugins. Plugin
|
||||
// scripts can load before or after that handler runs, so poll briefly.
|
||||
var deadline = performance.now() + 5000;
|
||||
var pollId = setInterval(function () {
|
||||
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') {
|
||||
if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
|
||||
clearInterval(pollId);
|
||||
_register();
|
||||
} else if (performance.now() > deadline) {
|
||||
@@ -93,9 +93,9 @@
|
||||
var s = document.createElement('style');
|
||||
s.id = STYLE_ID;
|
||||
s.textContent =
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-btn { bottom: 68px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-popover { bottom: 112px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-prompt { bottom: 112px; }';
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-btn { bottom: 68px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-popover { bottom: 112px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-prompt { bottom: 112px; }';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
@@ -109,8 +109,8 @@
|
||||
// Prime from whichever screen is already active.
|
||||
var active = document.querySelector('.screen.active');
|
||||
_applyNudge(active ? active.id : null);
|
||||
if (window.slopsmith && typeof window.slopsmith.on === 'function') {
|
||||
window.slopsmith.on('screen:changed', function (ev) {
|
||||
if (window.feedBack && typeof window.feedBack.on === 'function') {
|
||||
window.feedBack.on('screen:changed', function (ev) {
|
||||
_applyNudge(ev && ev.detail && ev.detail.id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"tour": [
|
||||
{
|
||||
"id": "welcome",
|
||||
"title": "Welcome to Slopsmith",
|
||||
"title": "Welcome to FeedBack",
|
||||
"content": "This is your library — every song we found in your library folder. Let's take a quick spin through the controls.",
|
||||
"shape": "bubble",
|
||||
"position": "auto"
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
function _register() {
|
||||
try {
|
||||
window.slopsmithTour.register(PLUGIN_ID, { screens: SCREENS });
|
||||
window.feedBackTour.register(PLUGIN_ID, { screens: SCREENS });
|
||||
} catch (e) {
|
||||
console.warn('[app_tour_settings] register failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') {
|
||||
if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
|
||||
_register();
|
||||
} else {
|
||||
var deadline = performance.now() + 5000;
|
||||
var pollId = setInterval(function () {
|
||||
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') {
|
||||
if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
|
||||
clearInterval(pollId);
|
||||
_register();
|
||||
} else if (performance.now() > deadline) {
|
||||
@@ -38,9 +38,9 @@
|
||||
var s = document.createElement('style');
|
||||
s.id = STYLE_ID;
|
||||
s.textContent =
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-btn { bottom: 68px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-popover { bottom: 112px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .slopsmith-tour-prompt { bottom: 112px; }';
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-btn { bottom: 68px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-popover { bottom: 112px; }' +
|
||||
'body.' + NUDGE_CLASS + ' .feedBack-tour-prompt { bottom: 112px; }';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
_ensureStyle();
|
||||
var active = document.querySelector('.screen.active');
|
||||
_applyNudge(active ? active.id : null);
|
||||
if (window.slopsmith && typeof window.slopsmith.on === 'function') {
|
||||
window.slopsmith.on('screen:changed', function (ev) {
|
||||
if (window.feedBack && typeof window.feedBack.on === 'function') {
|
||||
window.feedBack.on('screen:changed', function (ev) {
|
||||
_applyNudge(ev && ev.detail && ev.detail.id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"id": "dlc-path",
|
||||
"selector": "#dlc-path",
|
||||
"title": "Library folder",
|
||||
"content": "Point Slopsmith at your library folder. Songs here become your library. Hit Save after changing.",
|
||||
"content": "Point FeedBack at your library folder. Songs here become your library. Hit Save after changing.",
|
||||
"shape": "spotlight",
|
||||
"position": "bottom"
|
||||
},
|
||||
@@ -69,7 +69,7 @@
|
||||
"id": "about",
|
||||
"selector": "#app-version-about",
|
||||
"title": "About",
|
||||
"content": "Version, source code, and license. Slopsmith is AGPL-3.0 — if you fork it, the source has to stay open.",
|
||||
"content": "Version, source code, and license. FeedBack is AGPL-3.0 — if you fork it, the source has to stay open.",
|
||||
"shape": "spotlight",
|
||||
"position": "top"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const state = window.__slopsmithCapabilityInspector || (window.__slopsmithCapabilityInspector = {});
|
||||
const state = window.__feedBackCapabilityInspector || (window.__feedBackCapabilityInspector = {});
|
||||
state.render = render;
|
||||
if (state.installed) return;
|
||||
state.installed = true;
|
||||
@@ -126,7 +126,7 @@
|
||||
}
|
||||
|
||||
function registry() {
|
||||
return window.slopsmith && window.slopsmith.capabilities;
|
||||
return window.feedBack && window.feedBack.capabilities;
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
@@ -319,7 +319,7 @@
|
||||
lifecycle: review.lifecycle || 'plugin-defined',
|
||||
label: review.label || 'Plugin-defined',
|
||||
tone: review.tone || 'info',
|
||||
summary: review.summary || 'Declared by a plugin or test fixture rather than registered as a core Slopsmith domain.',
|
||||
summary: review.summary || 'Declared by a plugin or test fixture rather than registered as a core FeedBack domain.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1087,7 +1087,7 @@
|
||||
const groups = new Map();
|
||||
for (const expected of Array.isArray(expectedShims) ? expectedShims : []) {
|
||||
const surface = String(expected && expected.legacySurface || '');
|
||||
const eventMatch = surface.match(/^window\.slopsmith\.(emit|on):(.+)$/);
|
||||
const eventMatch = surface.match(/^window\.feedBack\.(emit|on):(.+)$/);
|
||||
const key = eventMatch ? eventMatch[2] : surface;
|
||||
const type = eventMatch ? (eventMatch[1] === 'emit' ? 'emit' : 'listener') : 'surface';
|
||||
const entry = groups.get(key) || { group: key, emit: null, listener: null, surfaces: [] };
|
||||
@@ -1461,14 +1461,14 @@
|
||||
}
|
||||
|
||||
function audioSessionSnapshot() {
|
||||
const api = window.slopsmith && window.slopsmith.audioSession;
|
||||
const api = window.feedBack && window.feedBack.audioSession;
|
||||
if (!api || typeof api.snapshot !== 'function') return null;
|
||||
try { return api.snapshot(); }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
|
||||
function playbackSnapshot() {
|
||||
const api = window.slopsmith && window.slopsmith.playback;
|
||||
const api = window.feedBack && window.feedBack.playback;
|
||||
if (!api || typeof api.snapshot !== 'function') return null;
|
||||
try { return api.snapshot({ exportMode: 'local-inspector' }); }
|
||||
catch (_) { return null; }
|
||||
@@ -1747,6 +1747,6 @@
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install);
|
||||
else install();
|
||||
window.addEventListener('slopsmith:capabilities:ready', render);
|
||||
window.addEventListener('slopsmith:capabilities:changed', scheduleRender);
|
||||
window.addEventListener('feedBack:capabilities:ready', render);
|
||||
window.addEventListener('feedBack:capabilities:changed', scheduleRender);
|
||||
})();
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
This guide tells future AI assistants where each visual element lives in `screen.js`, what controls it, and the gotchas to watch for. The goal is for small polishes (color tweaks, sizing, animation timing, add/remove a label) to land in the right place on the first try without grep spelunking.
|
||||
|
||||
The whole renderer is **one file** — `screen.js`, wrapped in an IIFE, registered as `window.slopsmithViz_highway_3d` (a slopsmith#36 setRenderer factory). No imports beyond Three.js loaded from the vendored `/static/vendor/three/three.module.min.js` (pinned r170; swapped from CDN when bundled into core).
|
||||
The whole renderer is **one file** — `screen.js`, wrapped in an IIFE, registered as `window.feedBackViz_highway_3d` (a feedBack#36 setRenderer factory). No imports beyond Three.js loaded from the vendored `/static/vendor/three/three.module.min.js` (pinned r170; swapped from CDN when bundled into core).
|
||||
|
||||
**Styling (slopsmith `styles` capability).** This plugin owns its Tailwind CSS: it ships `assets/plugin.css` and declares `"styles": "assets/plugin.css"` in `plugin.json`, so core's prebuilt `static/tailwind.min.css` no longer scans it (it's excluded from core's content globs). The frontend injects `assets/plugin.css` as a `<link>` when the renderer activates. This is the one maintainer-time build step: after you add/change a Tailwind class in `screen.js` or `settings.html`, run `bash build-tailwind.sh` (pinned `tailwindcss@3.4.19`, `corePlugins.preflight=false` — utilities only) and **bump the `version` in `plugin.json`** so the injected `<link>`'s `?v=` cache-buster fetches the fresh file. The generated `assets/plugin.css` is committed; end users never build. See [docs/plugin-styles.md](../../docs/plugin-styles.md).
|
||||
**Styling (feedBack `styles` capability).** This plugin owns its Tailwind CSS: it ships `assets/plugin.css` and declares `"styles": "assets/plugin.css"` in `plugin.json`, so core's prebuilt `static/tailwind.min.css` no longer scans it (it's excluded from core's content globs). The frontend injects `assets/plugin.css` as a `<link>` when the renderer activates. This is the one maintainer-time build step: after you add/change a Tailwind class in `screen.js` or `settings.html`, run `bash build-tailwind.sh` (pinned `tailwindcss@3.4.19`, `corePlugins.preflight=false` — utilities only) and **bump the `version` in `plugin.json`** so the injected `<link>`'s `?v=` cache-buster fetches the fresh file. The generated `assets/plugin.css` is committed; end users never build. See [docs/plugin-styles.md](../../docs/plugin-styles.md).
|
||||
|
||||
> **Navigation note:** This guide references functions by name and uses the existing banner comments (`/* ── Scene initialisation ─ */`, etc.) as section anchors. Line numbers are deliberately avoided so this stays correct as the file evolves. Use `Grep` for the function name or banner text to jump to a section.
|
||||
|
||||
@@ -15,7 +15,7 @@ The file is laid out top-to-bottom as:
|
||||
1. **Constants block** — palette (`S_COL`), scale (`SCALE`, `K`), fret/string counts, geometry sizes, camera, fog
|
||||
2. **Pure helpers** — `fretX`, `fretMid`, `dZ`, `computeBPM`
|
||||
3. **Three.js loader** — `loadThree()` (loads vendored `/static/vendor/three/three.module.min.js`, memoized)
|
||||
4. **Splitscreen helpers** — `_ssActive`, `_ssIsCanvasFocused` (read `window.slopsmithSplitscreen`)
|
||||
4. **Splitscreen helpers** — `_ssActive`, `_ssIsCanvasFocused` (read `window.feedBackSplitscreen`)
|
||||
5. **`createFactory()`** — the rest of the file is one big closure
|
||||
- Per-instance state (Three.js refs, pools, camera state, lifecycle flags)
|
||||
- `txtMat()` text-sprite cache, `pool()` factory
|
||||
@@ -63,7 +63,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
|
||||
|
||||
### Strings
|
||||
- **String colors** → `S_COL` array in the top-level constants block. Eight-element vibrant palette; index `s` is the string (0 = high E for guitar). `MAX_RENDER_STRINGS` keys off `S_COL.length`.
|
||||
- **String count for the active arrangement** → `resolveStringCount(bundle)` (top-level helper). Reads `bundle.stringCount` (slopsmith#93) with a `bass`-name fallback. Don't reintroduce `tuning.length` — see Pitfall #4.
|
||||
- **String count for the active arrangement** → `resolveStringCount(bundle)` (top-level helper). Reads `bundle.stringCount` (feedBack#93) with a `bass`-name fallback. Don't reintroduce `tuning.length` — see Pitfall #4.
|
||||
- **String thickness / gap / base Y** → `STR_THICK`, `S_BASE`, `S_GAP` constants.
|
||||
- **String-to-Y mapping (respects invert)** → the `sY(s)` arrow function inside `createFactory()`. Single source of truth for "where on Y is string s."
|
||||
- **Static string mesh creation** → `buildBoard()`, the `// Thin Line strings (glow layer)` and `// BoxGeometry strings — emissive glow ...` comment blocks. Two layers: low-opacity `Line` for soft glow, `BoxGeometry` mesh per string with its own material clone (kept in `stringLines[]` for live emissive updates).
|
||||
@@ -87,7 +87,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
|
||||
- **Technique markers** (bend, slide, hammer/pull/tap, accent, tremolo, palm-mute, pinch harmonic) → `// ── Technique labels ──` block in `drawNote()`. Most are small if-blocks using `txtMat(text, color, wide, style)` (cached sprite material; `'technique'` preset in `TXT_STYLES`). Exceptions: a **bend** draws a string-coloured chevron strength stack (`bendChevronMat`, one chevron per half-step), and **hammer-on / pull-off** draw a white ▲/▼ triangle with a string-coloured border (`triMat`) — both pinned to the gem; the bend ribbon's up→hold→down contour is driven by `bendSemisAtTime`.
|
||||
- **Open-string note** → special-cased throughout `drawNote()`: `n.f === 0`. Wider/flatter geometry, "0" label sprite, uses `openX` (the chord's open-string centroid) when supplied.
|
||||
- **Board projection ("ghost" preview)** → `// ── Board projection ──` block in `drawNote()`. Two meshes per string (`projMeshArr`, `projGlowArr`), one visible per frame for the next note. Linger window `PROJ_WIN`. Gated on the `projectionVisible` setting (BG_DEFAULTS / `h3dBgSetProjectionVisible` / the "Show note preview on the fretboard" checkbox in `settings.html`) — when off, the block is skipped and `update()`'s per-frame `m.visible = false` reset leaves the ghost hidden. **The glow has `renderOrder = -1`** which fights the strings — see Pitfall #6.
|
||||
- **Note-hit "sizzle" (slopsmith#254)** → `drawNotedetectSizzle()` (called from the `lyricsCtx` block in `draw()`, just before `drawNotedetectLabels()`). For each confirmed hit/active note (`_ndGood` in `drawNote()` pushes `{x, y, z, s, alpha, color}` onto the per-frame `_ndSizzle` array — `alpha` is the provider's clamped fade, `color` an optional palette override), it projects the note's world point through the up-to-date `cam`, sizes the burst from a fretboard-X-axis offset projection (reliable even when the note's rotated flat at the line), and twinkles a few short crackling ellipse-arc segments + tiny dots hugging the note's rectangle — re-randomised every frame, contained to ≲1.4× the note, half white / half the string colour (or the provider's `color` when given). Every dot/arc's `globalAlpha` and `shadowBlur` are scaled by the entry's `alpha`, and the per-element "off-this-frame" probability rises as `alpha` decays, so a struck-note glow visibly thins and fades. Also: `_ndGood` swaps the note's outline to `mGlow[s]` (bright string-tinted, not green). Knobs are inline: arc/dot count, base on-probability, line widths, `shadowBlur`, spread radii. Lives entirely on the 2D overlay layer — no Three.js geometry/disposal.
|
||||
- **Note-hit "sizzle" (feedBack#254)** → `drawNotedetectSizzle()` (called from the `lyricsCtx` block in `draw()`, just before `drawNotedetectLabels()`). For each confirmed hit/active note (`_ndGood` in `drawNote()` pushes `{x, y, z, s, alpha, color}` onto the per-frame `_ndSizzle` array — `alpha` is the provider's clamped fade, `color` an optional palette override), it projects the note's world point through the up-to-date `cam`, sizes the burst from a fretboard-X-axis offset projection (reliable even when the note's rotated flat at the line), and twinkles a few short crackling ellipse-arc segments + tiny dots hugging the note's rectangle — re-randomised every frame, contained to ≲1.4× the note, half white / half the string colour (or the provider's `color` when given). Every dot/arc's `globalAlpha` and `shadowBlur` are scaled by the entry's `alpha`, and the per-element "off-this-frame" probability rises as `alpha` decays, so a struck-note glow visibly thins and fades. Also: `_ndGood` swaps the note's outline to `mGlow[s]` (bright string-tinted, not green). Knobs are inline: arc/dot count, base on-probability, line widths, `shadowBlur`, spread radii. Lives entirely on the 2D overlay layer — no Three.js geometry/disposal.
|
||||
|
||||
### Chords
|
||||
- **Chord rendering loop** → `update()`, `// ── Chords ──` block. Iterates `bundle.chords`, calls `drawNote()` per chord-note, then draws the frame box, name label, and barre indicator.
|
||||
@@ -131,7 +131,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
|
||||
|
||||
## The `bundle` object
|
||||
|
||||
Every per-frame renderer call receives a `bundle` from slopsmith core. Fields used by this plugin:
|
||||
Every per-frame renderer call receives a `bundle` from feedBack core. Fields used by this plugin:
|
||||
|
||||
- `currentTime` — playback time in seconds (drives `dt` for everything)
|
||||
- `notes`, `chords`, `beats`, `sections` — chart arrays (already difficulty-filtered by core)
|
||||
@@ -141,9 +141,9 @@ Every per-frame renderer call receives a `bundle` from slopsmith core. Fields us
|
||||
- `lyricsVisible` — gate for lyrics overlay
|
||||
- `renderScale` — pixel-ratio multiplier from the user's quality setting
|
||||
- `songInfo.arrangement` — only field of `songInfo` this plugin reads, used as the bass-name fallback in `resolveStringCount()`
|
||||
- `stringCount` — slopsmith#93; always prefer this over deriving from tuning/arrangement
|
||||
- `stringCount` — feedBack#93; always prefer this over deriving from tuning/arrangement
|
||||
- `lefty` — display flag consumed by this renderer from `bundle.lefty`. Captured into `_leftyCached` before each frame so `xFret()`, `xFretMid()`, `boardSpanX()`, board geometry, note placement, and the camera shoulder offset mirror the fret axis for left-handed mode. A runtime lefty flip rebuilds board state and mirrors `curX`/`tgtX` plus the lookahead camera X cache so the camera does not drift across the neck.
|
||||
- `getNoteState(note, chartTime)` — slopsmith#254; per-note judgment from a scorer (note_detect). Captured each frame into `_ndGetNoteState` at the top of `update()` and consulted in `drawNote()` AFTER the event-driven `_ndHitMarks`/`_ndMissMarks` lookup AND over the proximity-based `hit` heuristic, both of which it overrides when it has a verdict: `'hit'`/`'active'` → `mGlow[s]` outline (bright string-tinted, *not* green) + `mGlow[s]` body + `mGlow[s]` sustain trail + a queue entry for `drawNotedetectSizzle` (so a held sustain keeps glowing/sparkling as long as the provider keeps returning `'active'`); `'miss'` → `mMissOutline` and `_showHit = false` (suppresses the bright body even if the note is near the line). Called with the note's chart time (`n.t`), which is how note_detect keys its `noteResults` map — *not* `now`. Returns null on cores without the API or songs with no scorer — then the event path / `hit` heuristic drive feedback for older note_detect builds. **notedetect ≥1.13 object verdicts additionally carry `{ points, mult, popKey }`** (game-scoring layer): `points` is the note's awarded score, `mult` the multiplier tier it landed at, and `popKey` a dedup key — chord members all return the chord-level judgment's key so a chord pops once, not once per gem. Consumed by the score-pop spawn in `drawNote()` (see Score FX below); all three are absent on older notedetect builds, so guard with `!== undefined`.
|
||||
- `getNoteState(note, chartTime)` — feedBack#254; per-note judgment from a scorer (note_detect). Captured each frame into `_ndGetNoteState` at the top of `update()` and consulted in `drawNote()` AFTER the event-driven `_ndHitMarks`/`_ndMissMarks` lookup AND over the proximity-based `hit` heuristic, both of which it overrides when it has a verdict: `'hit'`/`'active'` → `mGlow[s]` outline (bright string-tinted, *not* green) + `mGlow[s]` body + `mGlow[s]` sustain trail + a queue entry for `drawNotedetectSizzle` (so a held sustain keeps glowing/sparkling as long as the provider keeps returning `'active'`); `'miss'` → `mMissOutline` and `_showHit = false` (suppresses the bright body even if the note is near the line). Called with the note's chart time (`n.t`), which is how note_detect keys its `noteResults` map — *not* `now`. Returns null on cores without the API or songs with no scorer — then the event path / `hit` heuristic drive feedback for older note_detect builds. **notedetect ≥1.13 object verdicts additionally carry `{ points, mult, popKey }`** (game-scoring layer): `points` is the note's awarded score, `mult` the multiplier tier it landed at, and `popKey` a dedup key — chord members all return the chord-level judgment's key so a chord pops once, not once per gem. Consumed by the score-pop spawn in `drawNote()` (see Score FX below); all three are absent on older notedetect builds, so guard with `!== undefined`.
|
||||
|
||||
`tuning` and `capo` aren't consumed by this plugin.
|
||||
|
||||
@@ -151,11 +151,11 @@ Every per-frame renderer call receives a `bundle` from slopsmith core. Fields us
|
||||
|
||||
- **"+N" score pops** → `_fxSpawnPop()` from `drawNote()` (just after the provider verdict-override block), drawn by `drawScoreFx()` (called from the `lyricsCtx` block in `draw()`, right after `drawNotedetectLabels()`). Fixed 24-slot pool (`_fxPops`), deduped per `popKey` via the TTL'd `_fxSeen` map (pruned in `drawScoreFx`). Pops rise/fade over 700 ms; font size scales with the multiplier tier.
|
||||
- **Session FX** → `notedetect:fx` events (`{ fxType: 'multiplier'|'milestone'|'streakBreak', ... }`). notedetect dispatches each detail object twice in the same task: on `window` (unscoped, first) and as a bubbling CustomEvent from its per-panel instanceRoot (scoped, second). The listener (`_fxOnFx`, bound with the other notedetect listeners) treats element-targeted copies as authoritative — accepted only when their root lives in this panel's container — and **defers the window copy by a task** (`setTimeout 0`): if the element copy (same detail reference) arrived meanwhile it's dropped as a duplicate, otherwise it's the compat fallback for a detector whose root isn't in the DOM. This keeps splitscreen panels from rendering each other's FX even for the first event of a session. Effects: milestone → particle burst from a 4-slot Float32Array pool (`_fxBursts`), multiplier tier-up → expanding ring pulse at the strike-line centre, streak break → brief red wash.
|
||||
- **Skin palette** → `_fxResolvePalette()` reads `localStorage['slopsmith_notedetect_skin']` (`neon`/`esports`/`metal` → `_FX_PALETTES`) at listener-bind time and on the `notedetect:skin` bus event. The display fonts are document-loaded by notedetect's stylesheet, so the overlay canvas can reference the family names directly.
|
||||
- **Skin palette** → `_fxResolvePalette()` reads `localStorage['feedBack_notedetect_skin']` (`neon`/`esports`/`metal` → `_FX_PALETTES`) at listener-bind time and on the `notedetect:skin` bus event. The display fonts are document-loaded by notedetect's stylesheet, so the overlay canvas can reference the family names directly.
|
||||
- Everything lives on the 2D overlay layer — no Three.js geometry, no `txtMat()` cache traffic, nothing to dispose; `teardown()` deactivates the pools and removes both listeners.
|
||||
- **This block is the reference implementation for other renderer plugins** (drum highway, piano, custom highways) that want score pops / session FX: copy the `_fxOnFx` dedup+scoping listener, the `popKey`-keyed seen-map (cleared on backward seek), and the `_FX_PALETTES` skin mapping. The full consumer contract (events, payloads, provider verdict fields, theming variables) is documented in slopsmith-plugin-notedetect's `CLAUDE.md`.
|
||||
- **This block is the reference implementation for other renderer plugins** (drum highway, piano, custom highways) that want score pops / session FX: copy the `_fxOnFx` dedup+scoping listener, the `popKey`-keyed seen-map (cleared on backward seek), and the `_FX_PALETTES` skin mapping. The full consumer contract (events, payloads, provider verdict fields, theming variables) is documented in feedBack-plugin-notedetect's `CLAUDE.md`.
|
||||
|
||||
If you need a bundle field that isn't here yet, check `_makeBundle()` in `static/highway.js` in the **slopsmith core repo** — this is the plugin repo, `static/highway.js` is not here. The full path in the parent slopsmith checkout is `slopsmith/static/highway.js`.
|
||||
If you need a bundle field that isn't here yet, check `_makeBundle()` in `static/highway.js` in the **feedBack core repo** — this is the plugin repo, `static/highway.js` is not here. The full path in the parent feedBack checkout is `feedBack/static/highway.js`.
|
||||
|
||||
## Per-string state arrays
|
||||
|
||||
@@ -185,7 +185,7 @@ If a pool's mesh has per-instance state (its own material clone, its own texture
|
||||
1. **Adding a new pool? Reset it.** The reset block at the top of `update()` is easy to miss when adding a new pool elsewhere.
|
||||
2. **`txtMat()` is cache-keyed by `(style, text, color, wide)`.** Calling it with a numeric `text` works (it's coerced via `String(...)`), but new label content creates a new texture forever. Don't generate dynamic per-frame text (e.g. interpolated values) through `txtMat()` or you'll leak GPU memory. For static labels that change occasionally (chord names, fret numbers), the cache is fine. The `style` arg picks a preset from the `TXT_STYLES` table — see "Tweaking text-sprite styling" below.
|
||||
3. **Disposal in `teardown()` matters.** Three.js doesn't garbage-collect GPU resources. Every `material.dispose()`, `geometry.dispose()`, `map.dispose()`, and `ren.dispose()` call there is load-bearing. `teardown()` is called from `init()` (when re-initing), `destroy()` (setRenderer swap or `highway.stop()`), and on init failure.
|
||||
4. **Don't use `tuning.length` for string count.** `bundle.tuning` (and `arr.tuning` server-side) is always 6 elements even for bass — slopsmith pre-fills the array with zeros for unused strings. Use `bundle.stringCount` (slopsmith#93), with `/bass/i.test(arrangement)` as the only acceptable fallback. There's a comment in `resolveStringCount()` documenting this.
|
||||
4. **Don't use `tuning.length` for string count.** `bundle.tuning` (and `arr.tuning` server-side) is always 6 elements even for bass — feedBack pre-fills the array with zeros for unused strings. Use `bundle.stringCount` (feedBack#93), with `/bass/i.test(arrangement)` as the only acceptable fallback. There's a comment in `resolveStringCount()` documenting this.
|
||||
5. **lyricsCanvas DOM order.** The 2D overlay canvas is appended to `wrap` AFTER `ren.domElement` and given `z-index:1`. This is the empirically-correct order — earlier versions had it before the WebGL canvas, which broke in splitscreen panels with `position:relative; overflow:hidden`. Don't reorder without testing both modes.
|
||||
6. **Projection glow `renderOrder = -1`** in `initScene()`. This is a known-suboptimal setting — it forces the glow to draw before the strings in the transparent queue, so the string visibly cuts through the preview. Removing the line lets natural Z-sort layer it correctly. Plus the projection's world-Y matches the string Y, which after perspective projection puts the preview slightly screen-lower than the string; bumping `projY = y + NH * 0.4` recenters it. (Both fixes live on the `fix/preview-stacking` branch.)
|
||||
7. **`renderOrder` on transparent objects is sticky.** Three.js sorts the transparent queue by `renderOrder` first, then back-to-front. A stray `m.renderOrder = -1` on something will pull it under everything regardless of Z. When in doubt, leave `renderOrder` at the default 0 and rely on Z position.
|
||||
@@ -231,21 +231,21 @@ Style fields:
|
||||
|
||||
## Lifecycle (setRenderer contract)
|
||||
|
||||
Per slopsmith#36, the factory returns `{ init, draw, resize, destroy }`:
|
||||
Per feedBack#36, the factory returns `{ init, draw, resize, destroy }`:
|
||||
|
||||
- **`init(canvas, bundle)`** tears down any prior state, sets `highwayCanvas`, lazily loads Three.js, runs `initScene()`, calls `applySize()` (with a `retrySize` rAF loop fallback if the canvas isn't laid out yet).
|
||||
- **`draw(bundle)`** is gated on `_isReady`. Re-resolves `nStr` / inverted / renderScale, then `update(bundle) → camUpdate(bundle) → ren.render → 2D overlays`. The `_lastHwW/_lastHwH` check at the top auto-resizes when the splitscreen plugin bypasses `resize()`.
|
||||
- **`resize(w, h)`** is gated on `_isReady`. Just calls `applySize()`.
|
||||
- **`destroy()`** is idempotent. Sets flags, runs `teardown()`, drops `highwayCanvas`. Tolerates being called on an instance that's been destroyed and re-init'd already (resets `_lastHwW/H`, `_diagChord`, etc.).
|
||||
|
||||
The factory **returns a fresh instance per call**, so splitscreen's per-panel `setRenderer(slopsmithViz_highway_3d())` gets independent state per panel — important because the chord diagram, projection meshes, etc. are all per-instance.
|
||||
The factory **returns a fresh instance per call**, so splitscreen's per-panel `setRenderer(feedBackViz_highway_3d())` gets independent state per panel — important because the chord diagram, projection meshes, etc. are all per-instance.
|
||||
|
||||
## Branching / PR conventions
|
||||
|
||||
- Feature branches off `main`, descriptive name (e.g. `fix/preview-stacking`, `feat/palette-picker`).
|
||||
- PR target: target the contributor's own fork by default unless they ask otherwise; confirm before opening a PR upstream. Run `git remote -v` in this directory to see the remotes that are configured locally.
|
||||
- Commit messages: short imperative subject, optional body explaining *why*. Don't summarize the diff — the diff already does that.
|
||||
- This plugin is bundled **in-tree** at `plugins/highway_3d/` inside the `got-feedback/feedback` repository (not a gitlink/submodule). It ships with the default container image. Changes go through the normal slopsmith PR process — no separate upstream repo to sync.
|
||||
- This plugin is bundled **in-tree** at `plugins/highway_3d/` inside the `got-feedback/feedBack` repository (not a gitlink/submodule). It ships with the default container image. Changes go through the normal feedBack PR process — no separate upstream repo to sync.
|
||||
|
||||
## When in doubt
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 3D Highway
|
||||
|
||||
A 3D note highway visualization for [Slopsmith](https://github.com/got-feedback/feedback) — an alternative to the default 2D highway, with a sense of depth and perspective inspired by stage views in modern rhythm games.
|
||||
A 3D note highway visualization for [FeedBack](https://github.com/got-feedback/feedBack) — an alternative to the default 2D highway, with a sense of depth and perspective inspired by stage views in modern rhythm games.
|
||||
|
||||
## What you get
|
||||
|
||||
@@ -19,15 +19,15 @@ A 3D note highway visualization for [Slopsmith](https://github.com/got-feedback/
|
||||
|
||||
## Install
|
||||
|
||||
3D Highway ships **bundled** with Slopsmith — no separate installation needed. Pick **3D Highway** from the visualization picker in the player.
|
||||
3D Highway ships **bundled** with FeedBack — no separate installation needed. Pick **3D Highway** from the visualization picker in the player.
|
||||
|
||||
> **Note:** The bundled version is preferred over any user-installed copy with the same plugin ID. If you have an old `slopsmith-plugin-3dhighway` clone on disk (from before 3D Highway was promoted to core), it will be ignored at startup — a warning in the server log names the path of the discarded copy. You can safely delete the stale clone.
|
||||
> **Note:** The bundled version is preferred over any user-installed copy with the same plugin ID. If you have an old `feedBack-plugin-3dhighway` clone on disk (from before 3D Highway was promoted to core), it will be ignored at startup — a warning in the server log names the path of the discarded copy. You can safely delete the stale clone.
|
||||
>
|
||||
> **Fallback:** In the unlikely event that the bundled copy fails to load its routes (e.g., a broken bundled release), Slopsmith will automatically fall back to your user-installed copy and show a yellow "Fallback" badge in the Settings panel. Check the server startup log for the root cause in that case.
|
||||
> **Fallback:** In the unlikely event that the bundled copy fails to load its routes (e.g., a broken bundled release), FeedBack will automatically fall back to your user-installed copy and show a yellow "Fallback" badge in the Settings panel. Check the server startup log for the root cause in that case.
|
||||
|
||||
## Settings
|
||||
|
||||
Most of the visual controls (background style, intensity, audio reactivity, color palette) live on Slopsmith's **Settings** screen under the *3D Highway* section.
|
||||
Most of the visual controls (background style, intensity, audio reactivity, color palette) live on FeedBack's **Settings** screen under the *3D Highway* section.
|
||||
|
||||
## Contributing / development
|
||||
|
||||
@@ -35,4 +35,4 @@ For maintainers and AI assistants working on the codebase, see [`CLAUDE.md`](CLA
|
||||
|
||||
### Perf bench (`?h3dbench=1`)
|
||||
|
||||
Append `?h3dbench=1` to the player URL to enable opt-in `console.log` reporting of `update()` self-time, broken into six segments — `frame` (everything between `pbBeg(0)` at the top of `update()` and `pbEnd(0)` at the bottom; excludes the trailing `pbReportTick()` logging that fires after `pbEnd(0)`), `state` (per-frame state-derivation loop), `next` (next-note-by-string lookahead), `mat` (per-string material writes), `noteDraw` (single-note draw loop), `chordDraw` (chord draw loop). Reported every 5 seconds with p50 / p95 / max per segment and frame count, so before/after numbers on a target chart are reproducible (slopsmith#226). Off-by-default; the bench helpers (`pbBeg` / `pbEnd` / `pbReportTick`) are bound to a shared empty-function literal when the renderer instance is created (each `createHighway()` panel re-checks the flag), so the hot-path call sites are no-ops with negligible overhead (typically JIT-inlined).
|
||||
Append `?h3dbench=1` to the player URL to enable opt-in `console.log` reporting of `update()` self-time, broken into six segments — `frame` (everything between `pbBeg(0)` at the top of `update()` and `pbEnd(0)` at the bottom; excludes the trailing `pbReportTick()` logging that fires after `pbEnd(0)`), `state` (per-frame state-derivation loop), `next` (next-note-by-string lookahead), `mat` (per-string material writes), `noteDraw` (single-note draw loop), `chordDraw` (chord draw loop). Reported every 5 seconds with p50 / p95 / max per segment and frame count, so before/after numbers on a target chart are reproducible (feedBack#226). Off-by-default; the bench helpers (`pbBeg` / `pbEnd` / `pbReportTick`) are bound to a shared empty-function literal when the renderer instance is created (each `createHighway()` panel re-checks the flag), so the hot-path call sites are no-ops with negligible overhead (typically JIT-inlined).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Plugin-registered FastAPI routes for the 3dhighway visualization plugin.
|
||||
|
||||
Registered by slopsmith core via plugin.json's "routes" field — the
|
||||
Registered by feedBack core via plugin.json's "routes" field — the
|
||||
loader at plugins/__init__.py:589–604 imports this module and calls
|
||||
setup(app, context). context["config_dir"] points at the slopsmith
|
||||
setup(app, context). context["config_dir"] points at the feedBack
|
||||
data directory; we namespace user uploads under
|
||||
{config_dir}/plugin_uploads/highway_3d/.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Visual layer from joel's prototype (vibrant palette, glowing strings,
|
||||
// fret heat, dynamic lane, chord frame-boxes, per-note connector labels,
|
||||
// board projection, outline+core note meshes) adapted into the
|
||||
// slopsmithViz setRenderer contract (slopsmith#36) so it works in the
|
||||
// feedBackViz setRenderer contract (feedBack#36) so it works in the
|
||||
// main player and per-panel in splitscreen without any architectural
|
||||
// changes.
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
// high E=purple); Neon pushes saturation harder; Pastel desaturates
|
||||
// for long-session comfort; Colorblind (high contrast) is derived from
|
||||
// the chart format's built-in colorblind-mode palette, but this preset
|
||||
// intentionally keeps some entries tuned for slopsmith rather than
|
||||
// intentionally keeps some entries tuned for feedBack rather than
|
||||
// reproducing every original hex value verbatim. The chart-format base
|
||||
// values came from community reverse-engineering of the original chart
|
||||
// files; do not treat the tuned values below as the exact original
|
||||
// palette.
|
||||
// In slopsmith's index convention s=0 is the low E (thickest) and
|
||||
// In feedBack's index convention s=0 is the low E (thickest) and
|
||||
// s=5 is the high E (thinnest), matching the chart format's native string
|
||||
// indexing. Per-index ordering is preserved across all palettes so
|
||||
// switching between them never reassigns a string to a different
|
||||
@@ -128,10 +128,10 @@
|
||||
const MAX_RENDER_STRINGS = S_COL.length;
|
||||
|
||||
// Resolve the string count for the active arrangement. Prefer
|
||||
// bundle.stringCount (exposed by slopsmith core since #93 — derived
|
||||
// bundle.stringCount (exposed by feedBack core since #93 — derived
|
||||
// from notes/chords/tuning, so it works for 5-string bass, 7- and
|
||||
// 8-string guitar, etc.). Fall back to arrangement-name detection
|
||||
// for older slopsmith cores that don't emit the field. Clamp to the
|
||||
// for older feedBack cores that don't emit the field. Clamp to the
|
||||
// palette size so a malformed bundle or a 12-string chart doesn't
|
||||
// index past the per-string material arrays.
|
||||
function resolveStringCount(bundle) {
|
||||
@@ -231,7 +231,7 @@
|
||||
const AHEAD = 3.0;
|
||||
const BEHIND = 0.5;
|
||||
// How long a note/chord-frame stays renderable past the hit line while a
|
||||
// note-state provider (slopsmith#254) is attached. The provider's
|
||||
// note-state provider (feedBack#254) is attached. The provider's
|
||||
// hit/miss verdict is asynchronous — the engine-side verifier reports it
|
||||
// ~0.35-0.5 s after the line — so the default ~50 ms note linger /
|
||||
// ~0.48 s chord linger lapses before the tint can apply. Drives both
|
||||
@@ -664,7 +664,7 @@
|
||||
/** Arpeggio rim accent and lane tint. */
|
||||
const ARPEGGIO_RIM_BLUE_HEX = 0x454BB6;
|
||||
/** Post-hit chord-frame rim tints driven by the note-state provider
|
||||
* (slopsmith#254). Applied only to the teal frame during the linger
|
||||
* (feedBack#254). Applied only to the teal frame during the linger
|
||||
* fade (chDt <= 0) when a scorer is attached.
|
||||
* Matches the gem hit/miss colours so chord frame and note body
|
||||
* give a consistent signal:
|
||||
@@ -922,7 +922,7 @@
|
||||
* ====================================================================== */
|
||||
|
||||
function _ssActive() {
|
||||
const ss = window.slopsmithSplitscreen;
|
||||
const ss = window.feedBackSplitscreen;
|
||||
if (!ss || typeof ss.isActive !== 'function' || !ss.isActive()) return false;
|
||||
return typeof ss.isCanvasFocused === 'function'
|
||||
&& typeof ss.onFocusChange === 'function'
|
||||
@@ -930,7 +930,7 @@
|
||||
}
|
||||
|
||||
function _ssIsCanvasFocused(highwayCanvas) {
|
||||
const ss = window.slopsmithSplitscreen;
|
||||
const ss = window.feedBackSplitscreen;
|
||||
if (!_ssActive()) return true;
|
||||
return !!(ss && typeof ss.isCanvasFocused === 'function' &&
|
||||
ss.isCanvasFocused(highwayCanvas));
|
||||
@@ -941,7 +941,7 @@
|
||||
*
|
||||
* Audio-reactive ambient scenery in the fog band beyond the highway.
|
||||
* Module-level singletons share an AudioContext + AnalyserNode tap on
|
||||
* the slopsmith core <audio id="audio"> element across all panel
|
||||
* the feedBack core <audio id="audio"> element across all panel
|
||||
* instances; per-panel settings live in localStorage with a global
|
||||
* fallback so settings.html drives a single default while per-panel
|
||||
* overrides (h3d_bg_panel<idx>_*) can be set for splitscreen layouts.
|
||||
@@ -981,7 +981,7 @@
|
||||
const key = `${outcome}:${status}:${reason}`;
|
||||
if (_bgBridgeKeys.get(bridgeId) === key) return;
|
||||
_bgBridgeKeys.set(bridgeId, key);
|
||||
const session = window.slopsmith && window.slopsmith.audioSession;
|
||||
const session = window.feedBack && window.feedBack.audioSession;
|
||||
if (!session || typeof session.recordBridgeHit !== 'function') return;
|
||||
try {
|
||||
session.recordBridgeHit({
|
||||
@@ -998,14 +998,14 @@
|
||||
|
||||
function _bgGetAnalyser() {
|
||||
// Prefer the stems plugin's side-chain analyser when a sloppak is
|
||||
// loaded. As of slopsmith-plugin-stems 0.5.0 (sample-locked playback)
|
||||
// loaded. As of feedBack-plugin-stems 0.5.0 (sample-locked playback)
|
||||
// the #audio element is a silent virtual transport on sloppaks, so
|
||||
// tapping it sees only silence; the stems mix is exposed at
|
||||
// window.slopsmith.stems.getAnalyser() instead. The stems plugin
|
||||
// window.feedBack.stems.getAnalyser() instead. The stems plugin
|
||||
// creates and destroys that AnalyserNode per song, so we re-check
|
||||
// each call and key the cache on its identity — when the node
|
||||
// changes (song switch), the cache is replaced automatically.
|
||||
const stemsApi = window.slopsmith && window.slopsmith.stems;
|
||||
const stemsApi = window.feedBack && window.feedBack.stems;
|
||||
const stemsAnalyser = (stemsApi && typeof stemsApi.getAnalyser === 'function')
|
||||
? stemsApi.getAnalyser() : null;
|
||||
if (stemsAnalyser) {
|
||||
@@ -1023,7 +1023,7 @@
|
||||
freq: new Uint8Array(Math.max(BG_FREQ_BINS, stemsAnalyser.frequencyBinCount)),
|
||||
source: 'stems',
|
||||
};
|
||||
_bgRecordAudioBridge('audio-mix.analyser', 'window.slopsmith.stems.getAnalyser', 'handled', '', 'stems');
|
||||
_bgRecordAudioBridge('audio-mix.analyser', 'window.feedBack.stems.getAnalyser', 'handled', '', 'stems');
|
||||
}
|
||||
return _bgAudio;
|
||||
}
|
||||
@@ -1352,7 +1352,7 @@
|
||||
const FRET_NUMBER_GHOST_SCOPE_IDS = ['chords', 'all'];
|
||||
|
||||
function _bgPanelKey(canvas) {
|
||||
const ss = window.slopsmithSplitscreen;
|
||||
const ss = window.feedBackSplitscreen;
|
||||
const idx = (ss && typeof ss.panelIndexFor === 'function') ? ss.panelIndexFor(canvas) : null;
|
||||
return (idx == null) ? 'main' : 'panel' + idx;
|
||||
}
|
||||
@@ -2324,7 +2324,7 @@
|
||||
// never sees a tainted canvas. Setting
|
||||
// `crossOrigin = "anonymous"` would also strip
|
||||
// cookies from the fetch, which would 401 against
|
||||
// any cookie-protected slopsmith deployment. If
|
||||
// any cookie-protected feedBack deployment. If
|
||||
// this ever needs to fetch cross-origin, switch
|
||||
// to `use-credentials` AND have the server send
|
||||
// the matching CORS headers.
|
||||
@@ -2470,7 +2470,7 @@
|
||||
let _nextInstanceId = 0;
|
||||
|
||||
/* ======================================================================
|
||||
* Factory — slopsmith#36 setRenderer contract
|
||||
* Factory — feedBack#36 setRenderer contract
|
||||
* ====================================================================== */
|
||||
|
||||
function createFactory() {
|
||||
@@ -2479,8 +2479,8 @@
|
||||
// ── Per-instance Three.js state ───────────────────────────────────
|
||||
let scene = null, cam = null, ren = null;
|
||||
let wrap = null;
|
||||
// highway:visibility listener (slopsmith#246). Hides the .h3d-wrap
|
||||
// overlay when slopsmith's canvas is display:none'd (splitscreen
|
||||
// highway:visibility listener (feedBack#246). Hides the .h3d-wrap
|
||||
// overlay when feedBack's canvas is display:none'd (splitscreen
|
||||
// case). Without this, the wrap is a *sibling* of #highway so
|
||||
// hiding #highway leaves the WebGL scene painting full-screen.
|
||||
// Bound in initScene after wrap creation, unbound in destroy().
|
||||
@@ -2842,9 +2842,9 @@
|
||||
|
||||
// Notedetect feedback (issue #9). Per-panel mark queues populated
|
||||
// by two event sources: (a) legacy `notedetect:hit` /
|
||||
// `notedetect:miss` window CustomEvents, and (b) Slopsmith
|
||||
// `notedetect:miss` window CustomEvents, and (b) FeedBack
|
||||
// event-bus `note:hit` / `note:miss` events (subscribed in
|
||||
// initScene() when window.slopsmith exposes both `on` and `off`).
|
||||
// initScene() when window.feedBack exposes both `on` and `off`).
|
||||
// Both sources feed the same _ndPushMark() helper which dedupes
|
||||
// dual emissions. drawNote looks up its (s, f, t) against these
|
||||
// arrays each frame and swaps the outline material when a match
|
||||
@@ -2901,7 +2901,7 @@
|
||||
// no longer reads it — pruning lives once per frame so
|
||||
// drawNote's hot path is just the bounded (s, f, t) match.
|
||||
let _ndFrameNowMs = 0;
|
||||
// slopsmith#254 — core's per-note judgment provider, captured
|
||||
// feedBack#254 — core's per-note judgment provider, captured
|
||||
// from `bundle.getNoteState` at the top of each update(). When
|
||||
// present it's authoritative over the event-driven marks above:
|
||||
// 'hit'/'active' → bright string-tinted outline (mGlow[s]) +
|
||||
@@ -2912,7 +2912,7 @@
|
||||
// with no scorer registered. Older note_detect builds that only
|
||||
// emit notedetect:hit/miss events still work via _ndHitMarks.
|
||||
let _ndGetNoteState = null;
|
||||
let _ndHasProvider = false; // true iff a note-state provider is registered (slopsmith#254)
|
||||
let _ndHasProvider = false; // true iff a note-state provider is registered (feedBack#254)
|
||||
// Sustain verdict latch — persists a provider's hit/miss verdict for the
|
||||
// full duration of a sustained note. Once hitGlowDuration expires the
|
||||
// provider stops returning state; the latch re-injects the last verdict
|
||||
@@ -2976,7 +2976,7 @@
|
||||
let _fxPalette = _FX_PALETTES.neon;
|
||||
function _fxResolvePalette() {
|
||||
let skin = null;
|
||||
try { skin = localStorage.getItem('slopsmith_notedetect_skin'); } catch (e) {}
|
||||
try { skin = localStorage.getItem('feedBack_notedetect_skin'); } catch (e) {}
|
||||
_fxPalette = _FX_PALETTES[skin] || _FX_PALETTES.neon;
|
||||
}
|
||||
function _fxSpawnPop(popKey, points, mult, x, y, z) {
|
||||
@@ -3380,7 +3380,7 @@
|
||||
|
||||
function _unsubscribeFocus() {
|
||||
if (!_focusSubscribed) return;
|
||||
const ss = window.slopsmithSplitscreen;
|
||||
const ss = window.feedBackSplitscreen;
|
||||
if (ss && typeof ss.offFocusChange === 'function') ss.offFocusChange(_onFocusChange);
|
||||
_focusSubscribed = false;
|
||||
}
|
||||
@@ -4087,7 +4087,7 @@
|
||||
}
|
||||
|
||||
// ── Object pool ────────────────────────────────────────────────────
|
||||
// ── Opt-in perf bench harness (slopsmith#226) ──────────────────────
|
||||
// ── Opt-in perf bench harness (feedBack#226) ──────────────────────
|
||||
// Enable with `?h3dbench=1` on the player URL. Aggregates per-segment
|
||||
// timings of update() into a console.log every _PB_REPORT_MS.
|
||||
//
|
||||
@@ -5073,21 +5073,21 @@
|
||||
wrap.setAttribute('data-h3d-primary', '');
|
||||
highwayCanvas.parentNode.insertBefore(wrap, highwayCanvas.nextSibling);
|
||||
|
||||
// Subscribe to highway:visibility (slopsmith#246) so the
|
||||
// .h3d-wrap overlay hides in sync with the slopsmith canvas.
|
||||
// Subscribe to highway:visibility (feedBack#246) so the
|
||||
// .h3d-wrap overlay hides in sync with the feedBack canvas.
|
||||
// The wrap is a sibling of #highway, so display:none on
|
||||
// #highway leaves us painting full-screen otherwise.
|
||||
// Guarded lazy bind: tolerate hosts that don't yet expose
|
||||
// slopsmith.on/off (older slopsmith versions, headless
|
||||
// feedBack.on/off (older feedBack versions, headless
|
||||
// tests).
|
||||
if (window.slopsmith
|
||||
&& typeof window.slopsmith.on === 'function'
|
||||
&& typeof window.slopsmith.off === 'function') {
|
||||
if (window.feedBack
|
||||
&& typeof window.feedBack.on === 'function'
|
||||
&& typeof window.feedBack.off === 'function') {
|
||||
_visibilityHandler = (e) => {
|
||||
if (!wrap) return;
|
||||
// Filter by canvas identity (splitscreen-safe).
|
||||
// Each createHighway() instance emits its own
|
||||
// visibility events on the shared slopsmith bus —
|
||||
// visibility events on the shared feedBack bus —
|
||||
// without this gate, one hidden panel would also
|
||||
// hide every other panel's 3D overlay.
|
||||
if (!e || !e.detail || e.detail.canvas !== highwayCanvas) return;
|
||||
@@ -5095,7 +5095,7 @@
|
||||
wrap.style.display = v === false ? 'none' : '';
|
||||
};
|
||||
try {
|
||||
window.slopsmith.on('highway:visibility', _visibilityHandler);
|
||||
window.feedBack.on('highway:visibility', _visibilityHandler);
|
||||
} catch (e) {
|
||||
_visibilityHandler = null;
|
||||
}
|
||||
@@ -5116,7 +5116,7 @@
|
||||
}
|
||||
};
|
||||
try {
|
||||
window.slopsmith.on('highway:canvas-replaced', _canvasReplacedHandler);
|
||||
window.feedBack.on('highway:canvas-replaced', _canvasReplacedHandler);
|
||||
} catch (e) {
|
||||
_canvasReplacedHandler = null;
|
||||
}
|
||||
@@ -6170,7 +6170,7 @@
|
||||
return _sp;
|
||||
});
|
||||
|
||||
// ── Pre-warm pools (slopsmith#226) ─────────────────────────────
|
||||
// ── Pre-warm pools (feedBack#226) ─────────────────────────────
|
||||
// Dense 7/8-string charts can outrun the lazy-grow path in the
|
||||
// first 1-2s of playback, stalling those frames with `new T.Mesh`
|
||||
// allocations *and* growing noteG forever (the pool only hides on
|
||||
@@ -6459,13 +6459,13 @@
|
||||
_ndOnMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); };
|
||||
window.addEventListener('notedetect:hit', _ndOnHit);
|
||||
window.addEventListener('notedetect:miss', _ndOnMiss);
|
||||
if (window.slopsmith &&
|
||||
typeof window.slopsmith.on === 'function' &&
|
||||
typeof window.slopsmith.off === 'function') {
|
||||
if (window.feedBack &&
|
||||
typeof window.feedBack.on === 'function' &&
|
||||
typeof window.feedBack.off === 'function') {
|
||||
_ndOnBusHit = (e) => { _ndHitMarks = _ndPushMark(_ndHitMarks, e.detail); };
|
||||
_ndOnBusMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); };
|
||||
window.slopsmith.on('note:hit', _ndOnBusHit);
|
||||
window.slopsmith.on('note:miss', _ndOnBusMiss);
|
||||
window.feedBack.on('note:hit', _ndOnBusHit);
|
||||
window.feedBack.on('note:miss', _ndOnBusMiss);
|
||||
}
|
||||
|
||||
// Score FX (notedetect ≥1.13). notedetect dispatches each fx
|
||||
@@ -6499,10 +6499,10 @@
|
||||
}, 0);
|
||||
};
|
||||
window.addEventListener('notedetect:fx', _fxOnFx);
|
||||
if (window.slopsmith && typeof window.slopsmith.on === 'function'
|
||||
&& typeof window.slopsmith.off === 'function') {
|
||||
if (window.feedBack && typeof window.feedBack.on === 'function'
|
||||
&& typeof window.feedBack.off === 'function') {
|
||||
_fxOnSkin = () => _fxResolvePalette();
|
||||
window.slopsmith.on('notedetect:skin', _fxOnSkin);
|
||||
window.feedBack.on('notedetect:skin', _fxOnSkin);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -8315,7 +8315,7 @@
|
||||
function smoothNow(bundle) {
|
||||
const raw = bundle.currentTime;
|
||||
const p = performance.now();
|
||||
// Host pause signal (slopsmith core's bundle.isPlaying): when the
|
||||
// Host pause signal (feedBack core's bundle.isPlaying): when the
|
||||
// chart clock isn't advancing (paused / stalled / mid-seek), don't
|
||||
// extrapolate forward against a frozen audio sample — that creeps
|
||||
// the highway ahead by up to the interp cap and then snaps back
|
||||
@@ -8437,7 +8437,7 @@
|
||||
if (_ndMissMarks[_pi].expiresAt <= _ndFrameNowMs) _ndMissMarks.splice(_pi, 1);
|
||||
}
|
||||
}
|
||||
// slopsmith#254 — capture core's per-note judgment provider for
|
||||
// feedBack#254 — capture core's per-note judgment provider for
|
||||
// this frame's drawNote() calls (held-sustain glow + lit gems).
|
||||
// bundle.getNoteState is ALWAYS present (the core stub returns
|
||||
// null when no provider is registered), so its existence isn't
|
||||
@@ -9140,10 +9140,10 @@
|
||||
{
|
||||
const si = bundle.songInfo;
|
||||
// bundle.songInfo has no filename field (the WS song_info message
|
||||
// never includes it). Use window.slopsmith.currentSong.filename
|
||||
// never includes it). Use window.feedBack.currentSong.filename
|
||||
// — set by highway.js from the WS URL — combined with the
|
||||
// arrangement index as a reliable per-song-arrangement key.
|
||||
const currentSong = window.slopsmith && window.slopsmith.currentSong;
|
||||
const currentSong = window.feedBack && window.feedBack.currentSong;
|
||||
const key = currentSong ? currentSong.filename + '\0' + (si ? (si.arrangement_index ?? '') : '') : null;
|
||||
if (key !== null && key !== _songKey) {
|
||||
_songKey = key;
|
||||
@@ -9729,7 +9729,7 @@
|
||||
// lingering past that point.
|
||||
chordTailHoldS = Math.min(CHORD_HWY_LINGER_S, Math.max(cjNext.t - ch.t, 1e-3));
|
||||
}
|
||||
// slopsmith#254 — engine verdicts land ~0.4 s after the
|
||||
// feedBack#254 — engine verdicts land ~0.4 s after the
|
||||
// chord crosses; on a fast different-voicing sequence
|
||||
// the clip above can shrink the rim's draw life below
|
||||
// that, so the green/red latch is set but the rim isn't
|
||||
@@ -10042,7 +10042,7 @@
|
||||
// Used for the mute X lines so hit/miss feedback only shows on
|
||||
// the outer borders of the framebox, not inside the X pattern.
|
||||
const baseRimHex = rimHex;
|
||||
// slopsmith#254 — once the chord crosses the hit
|
||||
// feedBack#254 — once the chord crosses the hit
|
||||
// line, tint the teal frame by the note-state
|
||||
// provider verdict: green on a clean grab, red on a
|
||||
// miss. The verdict is async (the engine verifier
|
||||
@@ -11723,7 +11723,7 @@
|
||||
const effectiveProjWin = _rawGap > 0 ? Math.min(0.6, Math.max(0.05, _rawGap)) : 0.6;
|
||||
const projFactorG = Math.max(0, Math.min(1, 1 - Math.max(dt, 0) / effectiveProjWin));
|
||||
const inGhostWin = n.f > 0 && isNextOnString && dt > -ghostHold && dt < effectiveProjWin && projFactorG > 0.001;
|
||||
// slopsmith#254 — query the provider once per note, before both !skipBody
|
||||
// feedBack#254 — query the provider once per note, before both !skipBody
|
||||
// blocks, so _showHit can be a const and _ndGood is available for the
|
||||
// sustain trail (which renders even when skipBody=true for slide targets).
|
||||
let _ndGood = false; // true when provider confirms hit/active
|
||||
@@ -11898,7 +11898,7 @@
|
||||
const rimXY = n.ac ? ACCENT_RIM_XY_SCALE_MUL : 1;
|
||||
const rimZ = n.ac ? ACCENT_RIM_Z_SCALE_MUL : 1;
|
||||
|
||||
// slopsmith#254 — apply outline + lateral face-fill overrides from provider verdict.
|
||||
// feedBack#254 — apply outline + lateral face-fill overrides from provider verdict.
|
||||
// hit/active → green outline (mHitBright[s]) + green lateral faces;
|
||||
// miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent.
|
||||
if (_ndCs) {
|
||||
@@ -13019,15 +13019,15 @@
|
||||
if (_ndOnHit) { window.removeEventListener('notedetect:hit', _ndOnHit); _ndOnHit = null; }
|
||||
if (_ndOnMiss) { window.removeEventListener('notedetect:miss', _ndOnMiss); _ndOnMiss = null; }
|
||||
if (_fxOnFx) { window.removeEventListener('notedetect:fx', _fxOnFx); _fxOnFx = null; }
|
||||
if (window.slopsmith && typeof window.slopsmith.off === 'function') {
|
||||
if (_fxOnSkin) { try { window.slopsmith.off('notedetect:skin', _fxOnSkin); } catch (e) {} _fxOnSkin = null; }
|
||||
if (_ndOnBusHit) window.slopsmith.off('note:hit', _ndOnBusHit);
|
||||
if (_ndOnBusMiss) window.slopsmith.off('note:miss', _ndOnBusMiss);
|
||||
if (window.feedBack && typeof window.feedBack.off === 'function') {
|
||||
if (_fxOnSkin) { try { window.feedBack.off('notedetect:skin', _fxOnSkin); } catch (e) {} _fxOnSkin = null; }
|
||||
if (_ndOnBusHit) window.feedBack.off('note:hit', _ndOnBusHit);
|
||||
if (_ndOnBusMiss) window.feedBack.off('note:miss', _ndOnBusMiss);
|
||||
if (_visibilityHandler) {
|
||||
try { window.slopsmith.off('highway:visibility', _visibilityHandler); } catch (e) {}
|
||||
try { window.feedBack.off('highway:visibility', _visibilityHandler); } catch (e) {}
|
||||
}
|
||||
if (_canvasReplacedHandler) {
|
||||
try { window.slopsmith.off('highway:canvas-replaced', _canvasReplacedHandler); } catch (e) {}
|
||||
try { window.feedBack.off('highway:canvas-replaced', _canvasReplacedHandler); } catch (e) {}
|
||||
}
|
||||
}
|
||||
_ndOnBusHit = _ndOnBusMiss = null;
|
||||
@@ -13261,11 +13261,11 @@
|
||||
_bgReactiveOptOut = !!(bundle && bundle.bgReactive === false);
|
||||
|
||||
if (_ssActive()) {
|
||||
window.slopsmithSplitscreen.onFocusChange(_onFocusChange);
|
||||
window.feedBackSplitscreen.onFocusChange(_onFocusChange);
|
||||
_focusSubscribed = true;
|
||||
}
|
||||
|
||||
// Async-ready contract (slopsmith#36 readyPromise). Resolves
|
||||
// Async-ready contract (feedBack#36 readyPromise). Resolves
|
||||
// when Three.js loaded + scene initialised (_isReady = true).
|
||||
// Rejects on any async failure so highway.js can revert.
|
||||
let _resolveReady, _rejectReady;
|
||||
@@ -13571,11 +13571,11 @@
|
||||
};
|
||||
}
|
||||
|
||||
window.slopsmithViz_highway_3d = createFactory;
|
||||
window.feedBackViz_highway_3d = createFactory;
|
||||
// Per-panel control descriptors (splitscreen). The palette selector was
|
||||
// removed — per-string colors are set via the core "Highway String Colors"
|
||||
// UI, which drives both highways by named string.
|
||||
window.slopsmithViz_highway_3d.panelControls = [
|
||||
window.feedBackViz_highway_3d.panelControls = [
|
||||
{
|
||||
key: 'cameraSmoothing',
|
||||
label: 'Camera smoothing (X-pan)',
|
||||
@@ -13618,8 +13618,8 @@
|
||||
// are matched by the piano plugin instead.
|
||||
// _canRun3D() in app.js still gates Auto from
|
||||
// picking us on machines without WebGL2.
|
||||
window.slopsmithViz_highway_3d.contextType = 'webgl2';
|
||||
window.slopsmithViz_highway_3d.__test = {
|
||||
window.feedBackViz_highway_3d.contextType = 'webgl2';
|
||||
window.feedBackViz_highway_3d.__test = {
|
||||
getAnalyserForBridgeTest: _bgGetAnalyser,
|
||||
readBandsForBridgeTest: _bgReadBands,
|
||||
resetAnalyserBridgeForTest() { _bgBridgeKeys.clear(); _bgAudio = null; _bgAudioCore = null; _bgAudioFailedAt = 0; },
|
||||
@@ -13630,12 +13630,12 @@
|
||||
// sloppaks). Word boundaries (\b) keep us from accidentally matching
|
||||
// arrangements that merely contain these as substrings (e.g. a
|
||||
// "BasslineKeys" arrangement would otherwise match `bass`).
|
||||
window.slopsmithViz_highway_3d.matchesArrangement = function (songInfo) {
|
||||
window.feedBackViz_highway_3d.matchesArrangement = function (songInfo) {
|
||||
const arr = (songInfo && songInfo.arrangement) || '';
|
||||
return /\b(?:lead|rhythm|bass|combo|guitar)\b/i.test(arr);
|
||||
};
|
||||
|
||||
// No imperative register() call needed: slopsmith#272 introduced the
|
||||
// No imperative register() call needed: feedBack#272 introduced the
|
||||
// consolidated tour menu, which discovers this plugin's tour automatically
|
||||
// via /api/plugins (has_tour:true from plugin.json's tour field) and
|
||||
// gates relevance on whether highway_3d is the active viz. A register()
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
Upload an MP4 or WebM (≤50 MB). Plays muted, looped
|
||||
in the fog band when the style above is set to
|
||||
<em>Custom video</em>. Bytes stay on the slopsmith server,
|
||||
<em>Custom video</em>. Bytes stay on the feedBack server,
|
||||
not in the browser.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -725,7 +725,7 @@
|
||||
// localStorage values are stored as UTF-16 (two bytes per
|
||||
// character), so the on-disk footprint is ~2.67× the raw
|
||||
// file size. localStorage quotas are typically 5 MB per
|
||||
// origin and slopsmith already uses some of that for other
|
||||
// origin and feedBack already uses some of that for other
|
||||
// settings, so a 1.5 MB raw limit (≈4 MB on disk) leaves
|
||||
// safe headroom; the read-back verification below catches
|
||||
// remaining edge cases where the write still gets refused.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tailwind build config for the 3D Highway plugin's OWN stylesheet.
|
||||
*
|
||||
* Slopsmith serves Tailwind as a prebuilt stylesheet and core only scans core
|
||||
* FeedBack serves Tailwind as a prebuilt stylesheet and core only scans core
|
||||
* source at build time (constitution Principle II — no Play CDN / runtime JIT).
|
||||
* This plugin owns its utilities so it styles correctly even when core's build
|
||||
* didn't scan it (it's excluded from core's content globs). It uses arbitrary
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.slopsmith = window.slopsmith || {};
|
||||
if (window.slopsmithInputSetup && window.slopsmithInputSetup.version === 1) return;
|
||||
window.feedBack = window.feedBack || {};
|
||||
if (window.feedBackInputSetup && window.feedBackInputSetup.version === 1) return;
|
||||
|
||||
const capabilities = window.slopsmith.capabilities;
|
||||
const capabilities = window.feedBack.capabilities;
|
||||
const DONE_KEY = (inst) => `input_setup.done.${inst}`;
|
||||
const INSTRUMENTS = {
|
||||
guitar: { label: 'Guitar', mode: 'audio' },
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
// The Web-MIDI source provider now ships built-in with the core midi-input
|
||||
// domain (static/capabilities/midi-input.js), so input_setup is a pure
|
||||
// consumer — it just discovers/selects/opens through `window.slopsmith.midiInput`.
|
||||
// consumer — it just discovers/selects/opens through `window.feedBack.midiInput`.
|
||||
|
||||
// ── audio-input helper (guitar/bass device context) ─────────────────────
|
||||
async function _audioSources() {
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
// Keys/drums: pick a MIDI device via midi-input and confirm a live hit.
|
||||
async function renderMidiPanel(inst) {
|
||||
const mi = window.slopsmith.midiInput;
|
||||
const mi = window.feedBack.midiInput;
|
||||
// Availability is the midi-input DOMAIN being present, not the
|
||||
// Web-MIDI browser API — the domain coordinates providers (the
|
||||
// built-in Web-MIDI one, plus any native/desktop adapter), so
|
||||
@@ -249,7 +249,7 @@
|
||||
// Show every source the midi-input domain surfaces — not just
|
||||
// the built-in Web-MIDI provider — so a native/desktop MIDI
|
||||
// adapter registered with the domain is selectable too.
|
||||
const sources = window.slopsmith.midiInput.listSources() || [];
|
||||
const sources = window.feedBack.midiInput.listSources() || [];
|
||||
if (!sources.length) { testEl && (testEl.textContent = ''); wrap.classList.remove('hidden'); select.innerHTML = '<option>No MIDI devices found</option>'; select.disabled = true; return; }
|
||||
wrap.classList.remove('hidden');
|
||||
select.disabled = false;
|
||||
@@ -327,7 +327,7 @@
|
||||
return _runWizard({ host, instruments: instruments || [] }).then((r) => { overlay.remove(); return r; });
|
||||
}
|
||||
|
||||
window.slopsmithInputSetup = {
|
||||
window.feedBackInputSetup = {
|
||||
version: 1,
|
||||
mount,
|
||||
launch,
|
||||
|
||||
+12
-12
@@ -1,16 +1,16 @@
|
||||
# slopsmith-plugin-minigames
|
||||
# feedBack-plugin-minigames
|
||||
|
||||
The minigame framework for [Slopsmith](https://github.com/got-feedback/feedback).
|
||||
The minigame framework for [FeedBack](https://github.com/got-feedback/feedBack).
|
||||
|
||||
This plugin provides:
|
||||
|
||||
- A **Minigames hub** screen that discovers every installed minigame plugin and lists them as tiles with leaderboards.
|
||||
- A **shared profile** (XP, level, unlocks, totals) that aggregates runs across every minigame.
|
||||
- A JS **SDK** exposed at `window.slopsmithMinigames` that minigame plugins use to access scoring, HUD primitives, run persistence, and a scheduler — so individual minigames do not need their own DSP or backend.
|
||||
- A JS **SDK** exposed at `window.feedBackMinigames` that minigame plugins use to access scoring, HUD primitives, run persistence, and a scheduler — so individual minigames do not need their own DSP or backend.
|
||||
|
||||
## Writing a minigame
|
||||
|
||||
A minigame is a standard Slopsmith plugin that:
|
||||
A minigame is a standard FeedBack plugin that:
|
||||
|
||||
1. Adds a `minigame` block to its `plugin.json`:
|
||||
|
||||
@@ -33,7 +33,7 @@ A minigame is a standard Slopsmith plugin that:
|
||||
2. On script load, registers itself with the SDK using the safe late-binding
|
||||
pattern (minigame plugins may load before the SDK; the pending queue
|
||||
handles both orderings — the SDK drains it on init, and the
|
||||
`slopsmith-minigames-ready` event is an alternative for plugins that prefer
|
||||
`feedBack-minigames-ready` event is an alternative for plugins that prefer
|
||||
event-driven registration):
|
||||
|
||||
> **Important:** `spec.id` must exactly match the `id` field in `plugin.json`.
|
||||
@@ -51,20 +51,20 @@ A minigame is a standard Slopsmith plugin that:
|
||||
stop: () => { /* tear down */ },
|
||||
};
|
||||
|
||||
if (window.slopsmithMinigames) {
|
||||
window.slopsmithMinigames.register(spec);
|
||||
if (window.feedBackMinigames) {
|
||||
window.feedBackMinigames.register(spec);
|
||||
} else {
|
||||
(window.__slopsmithMinigamesPending = window.__slopsmithMinigamesPending || []).push(spec);
|
||||
(window.__feedBackMinigamesPending = window.__feedBackMinigamesPending || []).push(spec);
|
||||
}
|
||||
```
|
||||
|
||||
3. Calls `window.slopsmithMinigames.end({ score, durationMs, modifiers, meta })` when the run ends.
|
||||
3. Calls `window.feedBackMinigames.end({ score, durationMs, modifiers, meta })` when the run ends.
|
||||
|
||||
See [`slopsmith-plugin-flappy-bend`](https://github.com/got-feedback/feedback-plugin-flappy-bend) for a working example.
|
||||
See [`feedBack-plugin-flappy-bend`](https://github.com/got-feedback/feedBack-plugin-flappy-bend) for a working example.
|
||||
|
||||
## SDK reference
|
||||
|
||||
`window.slopsmithMinigames` exposes:
|
||||
`window.feedBackMinigames` exposes:
|
||||
|
||||
- `register(spec)` — declare a minigame
|
||||
- `start(gameId, opts)` / `end(result)` — lifecycle
|
||||
@@ -77,4 +77,4 @@ See [`slopsmith-plugin-flappy-bend`](https://github.com/got-feedback/feedback-pl
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `slopsmith-plugin-notedetect` >= 1.10.0 — required for discrete/chord scoring modes (continuous mode is self-contained).
|
||||
- `feedBack-plugin-notedetect` >= 1.10.0 — required for discrete/chord scoring modes (continuous mode is self-contained).
|
||||
|
||||
@@ -36,7 +36,7 @@ _state = {
|
||||
"db_path": None,
|
||||
"profile_path": None,
|
||||
"plugins_dir_resolver": None,
|
||||
"log": logging.getLogger("slopsmith.plugin.minigames"),
|
||||
"log": logging.getLogger("feedBack.plugin.minigames"),
|
||||
# fee[dB]ack v0.3.0 unified XP: when running inside core these point at the
|
||||
# single core XP store (server.py plugin_context). XP then flows to ONE
|
||||
# store the profile badge reads. Absent when the plugin runs standalone,
|
||||
@@ -282,7 +282,7 @@ def _list_minigame_plugins(force_refresh: bool = False) -> list:
|
||||
"version": data.get("version"),
|
||||
}
|
||||
# Deduplicate by plugin_id: first entry wins (resolver returns
|
||||
# SLOPSMITH_PLUGINS_DIR before the bundled siblings, so an explicit
|
||||
# FEEDBACK_PLUGINS_DIR before the bundled siblings, so an explicit
|
||||
# override takes precedence over the in-tree snapshot — same winner
|
||||
# selection as the core plugin loader).
|
||||
if plugin_id not in seen_ids:
|
||||
@@ -328,7 +328,7 @@ def setup(app, context):
|
||||
|
||||
# The plugin loader doesn't currently expose a list-other-plugins helper,
|
||||
# so derive the plugin directories from environment + conventions:
|
||||
# 1. SLOPSMITH_PLUGINS_DIR env var (explicit override)
|
||||
# 1. FEEDBACK_PLUGINS_DIR env var (explicit override)
|
||||
# 2. The directory that contains this plugin (plugin_self.parent) —
|
||||
# covers the common case where all plugins live in one flat dir.
|
||||
# 3. plugin_self.parent.parent / "plugins" — covers the layout where
|
||||
@@ -336,7 +336,7 @@ def setup(app, context):
|
||||
# Duplicates are removed via a seen-set keyed on resolved paths.
|
||||
def _resolve_plugin_dirs():
|
||||
roots = []
|
||||
env_dir = os.environ.get("SLOPSMITH_PLUGINS_DIR")
|
||||
env_dir = os.environ.get("FEEDBACK_PLUGINS_DIR") or os.environ.get("SLOPSMITH_PLUGINS_DIR")
|
||||
if env_dir:
|
||||
roots.append(Path(env_dir))
|
||||
# Built-in plugins/ next to server.py (one level above this file's
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<!-- In-game container — only visible while a minigame is running.
|
||||
z-[60] sits above the Slopsmith navbar (z-50) so the game owns the
|
||||
z-[60] sits above the FeedBack navbar (z-50) so the game owns the
|
||||
viewport during a run; the stage's own Quit button is the exit. -->
|
||||
<div id="mg-stage" class="hidden fixed inset-0 z-[60] bg-fb-bg/95 flex flex-col"
|
||||
role="region" aria-labelledby="mg-stage-title">
|
||||
|
||||
+23
-23
@@ -1,20 +1,20 @@
|
||||
// slopsmith-plugin-minigames — SDK + hub controller.
|
||||
// feedBack-plugin-minigames — SDK + hub controller.
|
||||
//
|
||||
// This file does two things:
|
||||
// 1) Publishes window.slopsmithMinigames — the SDK that individual
|
||||
// 1) Publishes window.feedBackMinigames — the SDK that individual
|
||||
// minigame plugins call (register, start/end, scoring, ui, persistence).
|
||||
// 2) Mounts a hub UI in screen.html that lists every registered minigame
|
||||
// and the shared profile/leaderboards.
|
||||
//
|
||||
// Plugin load order is alphabetical, so minigame plugins (e.g. flappy_bend)
|
||||
// load BEFORE this script. They should register via a tiny shim that queues
|
||||
// to `window.__slopsmithMinigamesPending` if the SDK isn't up yet — we drain
|
||||
// the queue on init and also fire `slopsmith-minigames-ready` once ready.
|
||||
// to `window.__feedBackMinigamesPending` if the SDK isn't up yet — we drain
|
||||
// the queue on init and also fire `feedBack-minigames-ready` once ready.
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.slopsmithMinigames && window.slopsmithMinigames.__alive) {
|
||||
if (window.feedBackMinigames && window.feedBackMinigames.__alive) {
|
||||
return; // hot-reload guard
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
const yinD = new Float32Array(yinHalfN);
|
||||
const yinCmnd = new Float32Array(yinHalfN);
|
||||
let ringWrite = 0;
|
||||
// Desktop-engine bridge path. On slopsmith-desktop the native JUCE engine
|
||||
// Desktop-engine bridge path. On feedBack-desktop the native JUCE engine
|
||||
// owns the input device (often an exclusive ASIO device the browser's
|
||||
// getUserMedia can't see), so a renderer getUserMedia stream lands on the
|
||||
// wrong/silent Windows-default device. When the bridge is present we pull
|
||||
@@ -347,7 +347,7 @@
|
||||
// Prefer the desktop engine bridge (correct, user-configured input device);
|
||||
// fall back to getUserMedia on the web build or a downlevel addon.
|
||||
function start() {
|
||||
const audio = window.slopsmithDesktop && window.slopsmithDesktop.audio;
|
||||
const audio = window.feedBackDesktop && window.feedBackDesktop.audio;
|
||||
if (audio && typeof audio.getRawAudioFrame === 'function') {
|
||||
startBridge(audio);
|
||||
} else {
|
||||
@@ -360,7 +360,7 @@
|
||||
}
|
||||
|
||||
// ── scoring.createDiscrete / createChord ──────────────────────────────
|
||||
// Both wrap window.createNoteDetector from slopsmith-plugin-notedetect.
|
||||
// Both wrap window.createNoteDetector from feedBack-plugin-notedetect.
|
||||
// For v1 they are thin event re-emitters — minigames using them must
|
||||
// run alongside a chart (createNoteDetector needs a highway). Chart-free
|
||||
// discrete scoring is out of scope until the scoring-core extraction
|
||||
@@ -369,7 +369,7 @@
|
||||
const handlers = { hit: [], miss: [], end: [] };
|
||||
const fn = window.createNoteDetector;
|
||||
if (typeof fn !== 'function') {
|
||||
console.warn('[minigames] window.createNoteDetector unavailable — install slopsmith-plugin-notedetect for discrete/chord scoring.');
|
||||
console.warn('[minigames] window.createNoteDetector unavailable — install feedBack-plugin-notedetect for discrete/chord scoring.');
|
||||
let _unavailStopped = false;
|
||||
return {
|
||||
on(event, cb) { (handlers[event] || (handlers[event] = [])).push(cb); return this; },
|
||||
@@ -736,8 +736,8 @@
|
||||
container,
|
||||
modifiers,
|
||||
// Convenience pass-through for the SDK so games don't have to
|
||||
// touch window.slopsmithMinigames inside their start handler.
|
||||
sdk: window.slopsmithMinigames,
|
||||
// touch window.feedBackMinigames inside their start handler.
|
||||
sdk: window.feedBackMinigames,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[minigames] minigame start() threw:', e);
|
||||
@@ -895,7 +895,7 @@
|
||||
tile.setAttribute('aria-label', title);
|
||||
const stats = perGame[spec.id] || { runs: 0, best_score: 0 };
|
||||
// Thumbnails are served via the minigame plugin's own asset route
|
||||
// (the Slopsmith plugin loader only serves manifest-declared files,
|
||||
// (the FeedBack plugin loader only serves manifest-declared files,
|
||||
// so each minigame that ships extra assets must expose /assets/).
|
||||
// Thumbnails are served by the minigame plugin's own /assets/ route;
|
||||
// not every plugin ships one, so fall back to the placeholder on 404.
|
||||
@@ -957,15 +957,15 @@
|
||||
listRegistered: () => Array.from(registered.values()),
|
||||
};
|
||||
|
||||
window.slopsmithMinigames = sdk;
|
||||
window.feedBackMinigames = sdk;
|
||||
// Drain queue of plugins that loaded before us.
|
||||
(window.__slopsmithMinigamesPending || []).forEach(register);
|
||||
window.__slopsmithMinigamesPending = null;
|
||||
window.dispatchEvent(new CustomEvent('slopsmith-minigames-ready'));
|
||||
(window.__feedBackMinigamesPending || []).forEach(register);
|
||||
window.__feedBackMinigamesPending = null;
|
||||
window.dispatchEvent(new CustomEvent('feedBack-minigames-ready'));
|
||||
|
||||
// ── Wire hub render to screen lifecycle ───────────────────────────────
|
||||
// Slopsmith mounts plugin screens with id "plugin-<plugin_id>" and
|
||||
// routes there via showScreen() / window.slopsmith.navigate().
|
||||
// FeedBack mounts plugin screens with id "plugin-<plugin_id>" and
|
||||
// routes there via showScreen() / window.feedBack.navigate().
|
||||
const SCREEN_ID = `plugin-${PLUGIN_ID}`;
|
||||
// Non-scoring teardown: called when navigation happens mid-run so that
|
||||
// microphone streams, timers, and stage DOM are cleaned up without submitting
|
||||
@@ -993,8 +993,8 @@
|
||||
console.info('[minigames] active session torn down (reason=' + reason + ')');
|
||||
}
|
||||
|
||||
if (window.slopsmith && typeof window.slopsmith.on === 'function') {
|
||||
window.slopsmith.on('screen:changed', (e) => {
|
||||
if (window.feedBack && typeof window.feedBack.on === 'function') {
|
||||
window.feedBack.on('screen:changed', (e) => {
|
||||
const id = e && e.detail && e.detail.id;
|
||||
if (id === SCREEN_ID) {
|
||||
renderHub();
|
||||
@@ -1035,8 +1035,8 @@
|
||||
function installNavLink() {
|
||||
const navigateToHub = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
if (window.slopsmith && typeof window.slopsmith.navigate === 'function') {
|
||||
window.slopsmith.navigate(SCREEN_ID);
|
||||
if (window.feedBack && typeof window.feedBack.navigate === 'function') {
|
||||
window.feedBack.navigate(SCREEN_ID);
|
||||
} else if (typeof window.showScreen === 'function') {
|
||||
window.showScreen(SCREEN_ID);
|
||||
}
|
||||
@@ -1078,7 +1078,7 @@
|
||||
}
|
||||
}
|
||||
installNavLink();
|
||||
// The slopsmith plugin loader rebuilds the dropdown when plugins
|
||||
// The feedBack plugin loader rebuilds the dropdown when plugins
|
||||
// hot-reload — re-install on a short delay then settle.
|
||||
setTimeout(installNavLink, 250);
|
||||
setTimeout(installNavLink, 1500);
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
if (!btn || !status) return;
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Wipe all minigame XP, unlocks, and run history? This cannot be undone.')) return;
|
||||
if (!window.slopsmithMinigames?.resetProfile) {
|
||||
if (!window.feedBackMinigames?.resetProfile) {
|
||||
status.textContent = 'Minigames SDK not loaded — reload the page and try again.';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
status.textContent = 'Wiping…';
|
||||
try {
|
||||
await window.slopsmithMinigames.resetProfile();
|
||||
await window.feedBackMinigames.resetProfile();
|
||||
status.textContent = 'Profile reset.';
|
||||
} catch (e) {
|
||||
status.textContent = 'Failed: ' + String(e?.message || e);
|
||||
|
||||
+14
-14
@@ -1,22 +1,22 @@
|
||||
# Slopsmith Tuner Plugin
|
||||
# FeedBack Tuner Plugin
|
||||
|
||||
<img width="290" height="362" alt="grafik" src="https://github.com/user-attachments/assets/879440e9-b680-481b-9091-ddfa73319078" />
|
||||
|
||||
|
||||
A real-time guitar and bass tuner plugin for [Slopsmith](https://github.com/got-feedback/feedback).
|
||||
A real-time guitar and bass tuner plugin for [FeedBack](https://github.com/got-feedback/feedBack).
|
||||
|
||||
This plugin adds a floating "Tuner" button to the Slopsmith interface, providing a high-accuracy chromatic tuner with support for multiple presets, custom tunings, and automatic song tuning detection.
|
||||
This plugin adds a floating "Tuner" button to the FeedBack interface, providing a high-accuracy chromatic tuner with support for multiple presets, custom tunings, and automatic song tuning detection.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Pitch Detection**: Uses the YIN algorithm for robust and accurate frequency tracking.
|
||||
- **Multiple Presets**: Includes common guitar and bass tunings (Standard, Drop D, DADGAD, Open G, etc.).
|
||||
- **Automatic Song Tuning**: Detects and selects the correct tuning for the currently playing song in the Slopsmith player.
|
||||
- **Automatic Song Tuning**: Detects and selects the correct tuning for the currently playing song in the FeedBack player.
|
||||
- **Manual & Auto Tracking**: Automatically estimates the closest string or allows manual selection for focused tuning.
|
||||
- **Visual Feedback**: Large cents-deviation gauge, frequency display, and color-coded indicators.
|
||||
- **Custom Tunings**: Add your own tunings via note names (e.g., E2, A2) or Hz frequencies in the settings.
|
||||
- **Audio Device Selection**: Choose specific input devices and channels (Mono, Left, Right) for professional interfaces.
|
||||
- **Themable UI**: Styled with Tailwind CSS to match your Slopsmith theme.
|
||||
- **Themable UI**: Styled with Tailwind CSS to match your FeedBack theme.
|
||||
- **Visualizations**: Pick from different visualizations to suit your needs (Currently: Default, Strobe, Analogue Gauge, Mace Fx III, and Toilet Tuner)
|
||||
|
||||
## Available Visualizations
|
||||
@@ -34,18 +34,18 @@ This plugin adds a floating "Tuner" button to the Slopsmith interface, providing
|
||||
## Installation
|
||||
|
||||
### Download a Release
|
||||
1. Download one of the [Releases](https://github.com/OmikronApex/slopsmith-plugin-tuner/releases)
|
||||
1. Download one of the [Releases](https://github.com/OmikronApex/feedBack-plugin-tuner/releases)
|
||||
2. Extract it to your plugins folder
|
||||
3. Restart Slopsmith
|
||||
3. Restart FeedBack
|
||||
|
||||
### Update Manager
|
||||
The plugin is listed in the official plugin repository, so it can also be installed directly via the [Update Manager](https://github.com/masc0t/slopsmith-update-manager)
|
||||
The plugin is listed in the official plugin repository, so it can also be installed directly via the [Update Manager](https://github.com/masc0t/feedBack-update-manager)
|
||||
|
||||
### Git
|
||||
```bash
|
||||
cd /path/to/slopsmith/plugins
|
||||
git clone https://github.com/OmikronApex/slopsmith-plugin-tuner.git tuner
|
||||
# Restart Slopsmith (or restart your docker container)
|
||||
cd /path/to/feedBack/plugins
|
||||
git clone https://github.com/OmikronApex/feedBack-plugin-tuner.git tuner
|
||||
# Restart FeedBack (or restart your docker container)
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
@@ -70,7 +70,7 @@ Click the ⚙️ icon in the tuner window to access:
|
||||
|
||||
|
||||
### Plugin Manager
|
||||
Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -> Tuner):
|
||||
Access advanced settings via the FeedBack Plugin Manager (Settings -> Plugins -> Tuner):
|
||||
- **Floating Button**: Toggle the visibility of the tuner button on the main interface.
|
||||
- **Tuning Visibility**: Toggle which built-in tunings appear in your menu.
|
||||
- **Custom Tunings**: Define your own tuning presets by entering a name and a list of notes/frequencies.
|
||||
@@ -82,7 +82,7 @@ Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -
|
||||
## Changelog
|
||||
|
||||
### [1.3.1] - 2026-06-04
|
||||
- JUCE bridge audio input: when running inside Slopsmith Desktop the tuner taps the engine's raw audio stream (`getRawAudioFrame`) and runs its own tuning-optimised YIN over it, falling back to the browser microphone pipeline otherwise.
|
||||
- JUCE bridge audio input: when running inside FeedBack Desktop the tuner taps the engine's raw audio stream (`getRawAudioFrame`) and runs its own tuning-optimised YIN over it, falling back to the browser microphone pipeline otherwise.
|
||||
- Fixed octave-low / sub-harmonic pitch errors (canonical YIN absolute-threshold selection) and added octave-aware nearest-string matching.
|
||||
- "Free Tune" is now remembered as your last tuning, so it persists across sessions instead of resetting to a preset each time.
|
||||
- Relocated visualization SVG assets to `visualization/assets/`, served via the dedicated `/api/plugins/tuner/viz-assets/` route (supersedes the 1.3.0 note about the root `assets/` directory).
|
||||
@@ -93,7 +93,7 @@ Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -
|
||||
- Added CHEF MT-3 visualization: inspired by the BOSS TU-3, featuring a 90° curved glass gauge arc, 51 tick marks, red 7-segment display, and rubber mode/brightness buttons.
|
||||
- Refactored `screen.js` into focused modules: audio pipeline extracted to `utils/audio.js`, UI layer extracted to `utils/ui.js` (shared-state factory pattern). `screen.js` reduced from ~1060 to ~300 lines.
|
||||
- Normalised `DEFAULT_TUNINGS` keys to instrument keys (`guitar-6`, `bass-4`, etc.) — removes the internal group-name lookup table.
|
||||
- Added plugin stylesheet (`assets/plugin.css`) via the Slopsmith styles contract, ensuring arbitrary Tailwind classes render correctly for runtime-installed users.
|
||||
- Added plugin stylesheet (`assets/plugin.css`) via the FeedBack styles contract, ensuring arbitrary Tailwind classes render correctly for runtime-installed users.
|
||||
- Moved SVG assets (`Bathroom.svg`, `Plunger.svg`, `Toiletbowl.svg`) to the root `assets/` directory; removed the now-redundant custom asset route from `routes.py`.
|
||||
- Moved Toilet Tuner to the end of the visualization picker list.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def _migrate_custom_tuning(name: str, value) -> dict:
|
||||
def setup(app: FastAPI, context: dict):
|
||||
config_dir = Path(context["config_dir"])
|
||||
config_file = config_dir / "tuner.json"
|
||||
log = context.get("log") or logging.getLogger("slopsmith.plugin.tuner")
|
||||
log = context.get("log") or logging.getLogger("feedBack.plugin.tuner")
|
||||
|
||||
def _read() -> dict:
|
||||
defaults = {
|
||||
|
||||
+25
-25
@@ -1,7 +1,7 @@
|
||||
// Guitar/Bass Tuner Plugin for Slopsmith
|
||||
// Guitar/Bass Tuner Plugin for FeedBack
|
||||
(function() {
|
||||
'use strict';
|
||||
const _TUNER_STORAGE_KEY = 'slopsmith_tuner_settings';
|
||||
const _TUNER_STORAGE_KEY = 'feedBack_tuner_settings';
|
||||
|
||||
// ── Player sync state ─────────────────────────────────────────────
|
||||
let _onScreenChanged = null;
|
||||
@@ -104,18 +104,18 @@
|
||||
|
||||
function _tuningIdentityKey(songInfo) {
|
||||
if (!songInfo || !Array.isArray(songInfo.tuning) || !songInfo.tuning.length) return null;
|
||||
const ctx = (typeof window.slopsmith?.songTuningContext === 'function')
|
||||
? window.slopsmith.songTuningContext(songInfo)
|
||||
const ctx = (typeof window.feedBack?.songTuningContext === 'function')
|
||||
? window.feedBack.songTuningContext(songInfo)
|
||||
: {
|
||||
stringCount: songInfo.stringCount,
|
||||
arrangement: songInfo.arrangement,
|
||||
arrangement_smart_name: songInfo.arrangement_smart_name,
|
||||
};
|
||||
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function')
|
||||
? window.slopsmith.isBassArrangement(ctx)
|
||||
const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
|
||||
? window.feedBack.isBassArrangement(ctx)
|
||||
: (songInfo.arrangement || '').toLowerCase().includes('bass');
|
||||
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function')
|
||||
? window.slopsmith.effectiveStringCount(songInfo.tuning, ctx)
|
||||
const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
|
||||
? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
|
||||
: (songInfo.stringCount || songInfo.tuning.length);
|
||||
if (!sc || sc <= 0) return null;
|
||||
const offsets = songInfo.tuning.slice(0, sc);
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
function _autoOpenSessionKey(songInfo) {
|
||||
if (!songInfo) return '';
|
||||
const cur = window.slopsmith?.currentSong;
|
||||
const cur = window.feedBack?.currentSong;
|
||||
const filename = (cur && cur.filename) || songInfo.filename || songInfo.title || 'unknown';
|
||||
const arr = (cur && cur.arrangementIndex != null)
|
||||
? cur.arrangementIndex
|
||||
@@ -142,7 +142,7 @@
|
||||
async function _maybeAutoOpenOnTuningChange() {
|
||||
if (!document.getElementById('player')?.classList.contains('active')) return;
|
||||
|
||||
const songInfo = window.highway?.getSongInfo?.() || window.slopsmith?.currentSong;
|
||||
const songInfo = window.highway?.getSongInfo?.() || window.feedBack?.currentSong;
|
||||
if (!songInfo) return;
|
||||
|
||||
const tuningKey = _tuningIdentityKey(songInfo);
|
||||
@@ -181,11 +181,11 @@
|
||||
}
|
||||
|
||||
function _installAutoOpenListeners() {
|
||||
if (_onAutoOpenSongLoading || !window.slopsmith?.on) return;
|
||||
if (_onAutoOpenSongLoading || !window.feedBack?.on) return;
|
||||
_onAutoOpenSongLoading = _onAutoOpenSongLoadingHandler;
|
||||
_onAutoOpenSongReady = () => { _maybeAutoOpenOnTuningChange(); };
|
||||
window.slopsmith.on('song:loading', _onAutoOpenSongLoading);
|
||||
window.slopsmith.on('song:ready', _onAutoOpenSongReady);
|
||||
window.feedBack.on('song:loading', _onAutoOpenSongLoading);
|
||||
window.feedBack.on('song:ready', _onAutoOpenSongReady);
|
||||
}
|
||||
|
||||
// ── Player sync helpers ───────────────────────────────────────────
|
||||
@@ -196,18 +196,18 @@
|
||||
|| (onPlayer && songInfo?.tuning?.length);
|
||||
if (songInfo?.tuning?.length && wantCurrent) {
|
||||
_state.selectedTuningName = '_current';
|
||||
const ctx = (typeof window.slopsmith?.songTuningContext === 'function')
|
||||
? window.slopsmith.songTuningContext(songInfo)
|
||||
const ctx = (typeof window.feedBack?.songTuningContext === 'function')
|
||||
? window.feedBack.songTuningContext(songInfo)
|
||||
: {
|
||||
stringCount: songInfo.stringCount,
|
||||
arrangement: songInfo.arrangement,
|
||||
arrangement_smart_name: songInfo.arrangement_smart_name,
|
||||
};
|
||||
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function')
|
||||
? window.slopsmith.isBassArrangement(ctx)
|
||||
const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
|
||||
? window.feedBack.isBassArrangement(ctx)
|
||||
: (songInfo.arrangement || '').toLowerCase().includes('bass');
|
||||
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function')
|
||||
? window.slopsmith.effectiveStringCount(songInfo.tuning, ctx)
|
||||
const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
|
||||
? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
|
||||
: (songInfo.stringCount || songInfo.tuning.length);
|
||||
_state.currentSongOffsets = songInfo.tuning.slice(0, sc);
|
||||
_state.currentSongIsBass = isBass;
|
||||
@@ -378,14 +378,14 @@
|
||||
_outsideClickClose = () => { if (_state.enabled) disable(); };
|
||||
setTimeout(() => { if (_outsideClickClose) document.addEventListener('click', _outsideClickClose, { once: true }); }, 0);
|
||||
|
||||
if (window.slopsmith && !_onScreenChanged) {
|
||||
if (window.feedBack && !_onScreenChanged) {
|
||||
_onScreenChanged = () => { disable(); };
|
||||
_onSongReady = () => {
|
||||
_tunerUIApi.renderTuningOptions();
|
||||
if (_state.selectedTuningName === '_current') _syncCurrentTuning();
|
||||
};
|
||||
window.slopsmith.on('screen:changed', _onScreenChanged);
|
||||
window.slopsmith.on('song:ready', _onSongReady);
|
||||
window.feedBack.on('screen:changed', _onScreenChanged);
|
||||
window.feedBack.on('song:ready', _onSongReady);
|
||||
}
|
||||
|
||||
_state.uiContainer?.querySelector('.tuner-mic-error')?.remove();
|
||||
@@ -413,8 +413,8 @@
|
||||
if (_outsideClickClose) { document.removeEventListener('click', _outsideClickClose); _outsideClickClose = null; }
|
||||
if (_state.activeViz) { _state.activeViz.destroy(); _state.activeViz = null; }
|
||||
if (_state.uiContainer) { _state.uiContainer.classList.add('hidden'); _state.uiContainer.classList.remove('flex'); }
|
||||
if (_onScreenChanged) { window.slopsmith?.off('screen:changed', _onScreenChanged); _onScreenChanged = null; }
|
||||
if (_onSongReady) { window.slopsmith?.off('song:ready', _onSongReady); _onSongReady = null; }
|
||||
if (_onScreenChanged) { window.feedBack?.off('screen:changed', _onScreenChanged); _onScreenChanged = null; }
|
||||
if (_onSongReady) { window.feedBack?.off('song:ready', _onSongReady); _onSongReady = null; }
|
||||
if (window._tunerAudio) window._tunerAudio.stop();
|
||||
if (_state.vizContainer) _state.vizContainer.innerHTML = '';
|
||||
if (window.tuner?.updateButtons) window.tuner.updateButtons();
|
||||
@@ -426,7 +426,7 @@
|
||||
).catch(e => console.warn('Tuner: badge audio resume failed:', e && e.message ? e.message : e));
|
||||
}
|
||||
if (wasEnabled && onPlayer) {
|
||||
const songInfo = window.highway?.getSongInfo?.() || window.slopsmith?.currentSong;
|
||||
const songInfo = window.highway?.getSongInfo?.() || window.feedBack?.currentSong;
|
||||
if (songInfo) _autoOpenDismissedSessionKey = _autoOpenSessionKey(songInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (window.slopsmithDesktop && window.slopsmithDesktop.isDesktop) {
|
||||
if (window.feedBackDesktop && window.feedBackDesktop.isDesktop) {
|
||||
document.currentScript.insertAdjacentHTML('beforebegin', `
|
||||
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
|
||||
<div>
|
||||
@@ -129,7 +129,7 @@
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
if (window._tunerReloadConfig) window._tunerReloadConfig();
|
||||
if (opts && opts.tuningsChanged) window.slopsmith?.emit('tunings:updated');
|
||||
if (opts && opts.tuningsChanged) window.feedBack?.emit('tunings:updated');
|
||||
} catch (e) { console.error('Tuner settings: save failed', e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
async function _tryBridgeStart(audioInputMode, myGen) {
|
||||
if (audioInputMode === 'browser') return false;
|
||||
var desktop = (typeof window !== 'undefined') ? window.slopsmithDesktop : null;
|
||||
var desktop = (typeof window !== 'undefined') ? window.feedBackDesktop : null;
|
||||
if (!desktop || !desktop.isDesktop || !desktop.audio
|
||||
|| typeof desktop.audio.isAvailable !== 'function') return false;
|
||||
|
||||
|
||||
+21
-21
@@ -149,7 +149,7 @@ window._tunerUI = function(state, actions) {
|
||||
if (state.tuningSelect) state.tuningSelect.value = name;
|
||||
renderStringNotes();
|
||||
actions.saveConfig();
|
||||
window.slopsmith?.emit('tunings:updated');
|
||||
window.feedBack?.emit('tunings:updated');
|
||||
} catch (e) {
|
||||
console.error('Tuner: Failed to save custom tuning', e);
|
||||
}
|
||||
@@ -204,18 +204,18 @@ window._tunerUI = function(state, actions) {
|
||||
if (isPlayer && typeof window.highway?.getSongInfo === 'function') {
|
||||
const info = window.highway.getSongInfo();
|
||||
if (info && info.tuning) {
|
||||
const ctx = (typeof window.slopsmith?.songTuningContext === 'function')
|
||||
? window.slopsmith.songTuningContext(info)
|
||||
const ctx = (typeof window.feedBack?.songTuningContext === 'function')
|
||||
? window.feedBack.songTuningContext(info)
|
||||
: {
|
||||
stringCount: info.stringCount,
|
||||
arrangement: info.arrangement,
|
||||
arrangement_smart_name: info.arrangement_smart_name,
|
||||
};
|
||||
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function')
|
||||
? window.slopsmith.isBassArrangement(ctx)
|
||||
const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
|
||||
? window.feedBack.isBassArrangement(ctx)
|
||||
: (info.arrangement || '').toLowerCase().includes('bass');
|
||||
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function')
|
||||
? window.slopsmith.effectiveStringCount(info.tuning, ctx)
|
||||
const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
|
||||
? window.feedBack.effectiveStringCount(info.tuning, ctx)
|
||||
: (info.stringCount || info.tuning.length);
|
||||
const sliced = info.tuning.slice(0, sc);
|
||||
const freqs = window._tunerUtils.offsetsToFreqs(sliced, isBass);
|
||||
@@ -340,8 +340,8 @@ window._tunerUI = function(state, actions) {
|
||||
_lastAutoTargetFreq = null;
|
||||
if (state.activeViz) state.activeViz.update(null, 0, 0, vizMode, null, referencePitch);
|
||||
_syncStringHighlight(state.manualTargetFreq);
|
||||
if (window.slopsmith && window.slopsmith.emit) {
|
||||
window.slopsmith.emit('tuner:frame', { note: null, cents: 0, freq: 0, hasSignal: false });
|
||||
if (window.feedBack && window.feedBack.emit) {
|
||||
window.feedBack.emit('tuner:frame', { note: null, cents: 0, freq: 0, hasSignal: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -384,8 +384,8 @@ window._tunerUI = function(state, actions) {
|
||||
if (state.activeViz) state.activeViz.update(note, cents, displayFreq, vizMode, targetFreq, referencePitch, state.useFlats);
|
||||
if (state.freeTune) _syncStringHighlight(null);
|
||||
else _syncActiveStringFromFreq(targetFreq, isManual);
|
||||
if (window.slopsmith && window.slopsmith.emit) {
|
||||
window.slopsmith.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
|
||||
if (window.feedBack && window.feedBack.emit) {
|
||||
window.feedBack.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ window._tunerUI = function(state, actions) {
|
||||
const btn = document.getElementById('tuner-toggle-btn');
|
||||
if (!btn) return;
|
||||
const isPlayer = document.querySelector('.screen.active')?.id === 'player';
|
||||
if (!state.showFloatingButton || isPlayer || window.slopsmith?.isPlaying) {
|
||||
if (!state.showFloatingButton || isPlayer || window.feedBack?.isPlaying) {
|
||||
btn.classList.add('hidden');
|
||||
} else {
|
||||
btn.classList.remove('hidden');
|
||||
@@ -687,16 +687,16 @@ window._tunerUI = function(state, actions) {
|
||||
};
|
||||
const handleStop = () => updateFloatingButtonVisibility();
|
||||
|
||||
if (window.slopsmith) {
|
||||
window.slopsmith.on('song:play', handlePlay);
|
||||
window.slopsmith.on('song:pause', handleStop);
|
||||
window.slopsmith.on('song:ended', handleStop);
|
||||
window.slopsmith.on('screen:changed', (e) => {
|
||||
if (window.feedBack) {
|
||||
window.feedBack.on('song:play', handlePlay);
|
||||
window.feedBack.on('song:pause', handleStop);
|
||||
window.feedBack.on('song:ended', handleStop);
|
||||
window.feedBack.on('screen:changed', (e) => {
|
||||
if (e.detail.id === 'player') { handlePlay(); injectPlayerButton(); }
|
||||
else handleStop();
|
||||
});
|
||||
|
||||
if (window.slopsmith.isPlaying || document.querySelector('.screen.active')?.id === 'player') {
|
||||
if (window.feedBack.isPlaying || document.querySelector('.screen.active')?.id === 'player') {
|
||||
handlePlay();
|
||||
if (document.querySelector('.screen.active')?.id === 'player') injectPlayerButton();
|
||||
} else {
|
||||
@@ -710,10 +710,10 @@ window._tunerUI = function(state, actions) {
|
||||
// popover). The legacy `button:last-child` anchor resolves to a NESTED
|
||||
// transport button in v3 and would throw on insertBefore; the slot is
|
||||
// always present in v3, so that anchor is only used in the classic UI.
|
||||
const isV3 = !!(window.slopsmith && window.slopsmith.uiVersion === 'v3');
|
||||
const isV3 = !!(window.feedBack && window.feedBack.uiVersion === 'v3');
|
||||
let slot = null;
|
||||
if (isV3 && window.slopsmith.ui && typeof window.slopsmith.ui.playerControlSlot === 'function') {
|
||||
try { const _s = window.slopsmith.ui.playerControlSlot(); if (_s instanceof Element) slot = _s; }
|
||||
if (isV3 && window.feedBack.ui && typeof window.feedBack.ui.playerControlSlot === 'function') {
|
||||
try { const _s = window.feedBack.ui.playerControlSlot(); if (_s instanceof Element) slot = _s; }
|
||||
catch (_e) { /* host slot API failure → fall back to legacy container */ }
|
||||
}
|
||||
const controls = slot || document.getElementById('player-controls');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Analogue gauge tuner visualization for the Slopsmith tuner plugin.
|
||||
* Analogue gauge tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Contract: window['_tunerViz_analogue-gauge'](container) → { update(note, cents, freq), destroy() }
|
||||
* - note: string | null (null = no signal)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* CHEF MT-3 tuner visualization for the Slopsmith tuner plugin.
|
||||
* CHEF MT-3 tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Inspired by classic chromatic pedal tuners:
|
||||
* - Shiny black rectangular panel with chamfered edges and corner screws
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Default (gauge) tuner visualization for the Slopsmith tuner plugin.
|
||||
* Default (gauge) tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Contract: window._tunerViz_default(container) → { update(note, cents, freq), destroy() }
|
||||
* - note: string | null (null = no signal)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Mace Fx III style tuner visualization for the Slopsmith tuner plugin.
|
||||
* Mace Fx III style tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Inspired by hardware rack tuner displays:
|
||||
* - Dark navy LCD background
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Strobe tuner visualization for the Slopsmith tuner plugin.
|
||||
* Strobe tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Contract: window._tunerViz_strobe(container) → { update(note, cents, freq), destroy() }
|
||||
* - note: string | null (null = no signal)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Toilet Tuner visualization for the Slopsmith tuner plugin.
|
||||
* Toilet Tuner visualization for the FeedBack tuner plugin.
|
||||
*
|
||||
* Bathroom scene background; plunger slides left/right over the bowl based on
|
||||
* cents deviation; dips into bowl when in tune (±2 cents); wall calendar shows
|
||||
|
||||
Reference in New Issue
Block a user