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:
Bret Mogilefsky
2026-06-23 11:03:01 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent a8ad02739a
commit af2949677a
257 changed files with 2389 additions and 2293 deletions
+15 -15
View File
@@ -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
+6 -6
View File
@@ -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).
+2 -2
View File
@@ -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:589604 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/.
+67 -67
View File
@@ -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()
+2 -2
View File
@@ -147,7 +147,7 @@
<p class="text-xs text-gray-500 mb-2">
Upload an MP4 or WebM (&le;50&nbsp;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 -1
View File
@@ -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