diff --git a/README.md b/README.md new file mode 100644 index 0000000..efa4d32 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Factshift Web Client + +This repository contains the front‑end code for **app.factshift.com**. The project is built with [Vite](https://vitejs.dev/) and uses a variety of client‑side libraries to power a real‑time stream interface. + +## Installation + +Use `yarn` to install dependencies: + +```bash +yarn install +``` + +## Development + +Start a local development server: + +```bash +yarn start +``` + +## Building + +Create a production build: + +```bash +yarn build +``` + +Preview the built files locally: + +```bash +yarn serve +``` + +## Initialization + +For details on how the initialization pipeline works, see [docs/init-pipeline.md](docs/init-pipeline.md). + +## QA + +Quality assurance checklists live under [docs/qa](docs/qa). See the +[Ace Editor Styling QA Checklist](docs/qa/ace-editor-style.md) for +testing the new code editor styles. + diff --git a/docs/api/modes.md b/docs/api/modes.md new file mode 100644 index 0000000..0ac3b53 --- /dev/null +++ b/docs/api/modes.md @@ -0,0 +1,39 @@ +# Stream Mode Web APIs + +This document outlines the WebSocket message formats understood by each stream mode. + +## Boon +Messages are JSON objects describing a "boon" node. +Example sent from the client: +```json +{ + "id": 123, + "name": "Example Boon", + "x": 0, + "y": 0, + "z": 0, + "boonhonk": {"description": "text", "level": 1, "is_active": true}, + "image_id": null +} +``` +The server echoes a similar structure when broadcasting boon updates. + +## Boof +Boof messages contain textual content. Clients may send plain text or a JSON object `{ "content": "text" }` and receive a similar structure from the server. + +## Focal +Used to update the focal point of the simulation. +```json +{ + "type": "focal-point", + "position": {"x": 10, "y": 20}, + "bounds": {"x1": 0, "y1": 0, "x2": 20, "y2": 40} +} +``` + +## Passive +Passive mode receives streamed updates but does not send data. Any JSON object broadcast by the server will be displayed. + +## Other Modes +Bane, Bone, Bonk, Honk and Lore share the same pattern as Passive mode. They may be extended later with specific message formats. + diff --git a/docs/init-pipeline.md b/docs/init-pipeline.md new file mode 100644 index 0000000..efb34a9 --- /dev/null +++ b/docs/init-pipeline.md @@ -0,0 +1,44 @@ +# Initialization Pipeline + +The bootstrap folder exposes a small pipeline utility for organizing startup routines. + +```javascript +import { addInitSteps, runInitPipeline, InitPhase } from '../src/js/bootstrap/init-pipeline'; +import { stepsById } from '../src/js/bootstrap/init-steps'; + +addInitSteps([ + { ...stepsById.analytics, phase: InitPhase.BOOT }, + { ...stepsById.parameters, phase: InitPhase.BOOT }, + { ...stepsById.ui, phase: InitPhase.UI, dependsOn: [stepsById.parameters] }, +]); + +await runInitPipeline(); +``` + +`addInitSteps` accepts either an object mapping step ids to functions or an array of step objects. Each step object has the shape `{id, init, priority?, dependsOn?, phase?}`. The `dependsOn` list may contain step ids or references to other step objects. Steps are sorted before execution first by their phase, then by priority and finally their declared dependencies. + +### Example with Priorities and Dependencies + +```javascript +addInitSteps([ + { id: 'a', init: initA, priority: 0 }, + { id: 'b', init: initB, priority: 10 }, + { id: 'c', init: initC, dependsOn: ['a', 'b'] }, +]); +``` + +Here `c` runs after `a` and `b` despite not specifying a priority. Lower priority values run earlier. + +Duplicate step ids are not allowed and will throw an error when added. The +pipeline also detects circular dependencies between steps and throws an error if +a cycle is found during sorting. + +### Phases + +Steps can optionally specify a `phase` that groups them into broad buckets of work. +Phases are defined in the exported `InitPhase` enum with the values `BOOT`, `UI` and `DEFERRED`. +The sorter runs all `boot` phase steps before `ui` steps, followed by `deferred` steps. +Any step without a known phase is treated as `deferred`. + +Phases are useful for keeping lightweight initialization logic (`boot`) separate +from user interface startup (`ui`) or work that can be postponed (`deferred`). diff --git a/docs/qa/README.md b/docs/qa/README.md new file mode 100644 index 0000000..8e9c207 --- /dev/null +++ b/docs/qa/README.md @@ -0,0 +1,5 @@ +# QA Documentation + +This folder contains checklists and notes for quality assurance testers. + +- [Ace Editor Styling QA Checklist](ace-editor-style.md) diff --git a/docs/qa/ace-editor-style.md b/docs/qa/ace-editor-style.md new file mode 100644 index 0000000..b8d9bf4 --- /dev/null +++ b/docs/qa/ace-editor-style.md @@ -0,0 +1,11 @@ +# Ace Editor Styling QA Checklist + +This document lists the steps for verifying that Ace editor instances are styled consistently throughout the app. + +1. Navigate to any feature that displays a code editor. +2. Confirm that the editor is inside a container with the `.code-editor` class. +3. Verify the container has a thin border and uses the same background color as the main menu. +4. Ensure code text appears in a monospace font and is legible. +5. Resize the editor to check that width and height remain consistent. + +Report any styling deviations to the development team. diff --git a/docs/websocket-notes.md b/docs/websocket-notes.md new file mode 100644 index 0000000..907601a --- /dev/null +++ b/docs/websocket-notes.md @@ -0,0 +1,11 @@ +# Stream Container Notes + +This interface exposes nine narrative modes: **Boon**, **Bane**, **Bone**, **Bonk**, **Honk**, **Boof**, **Lore**, **Focal**, and **Passive**. + +Each mode's settings are stored locally in `localStorage` and may be transmitted to a backend when the `Send to Backend` button is pressed. To hook in real-time updates, listen to stream messages of the form `{type: '-settings', data: {...}}` and update the UI using the `handleStreamMessage` method of each handler. + +The container uses a `DataManager` abstraction to source data either from static arrays or from a live WebSocket feed. Extend `modeConfig` in `stream-container.js` to point a mode at a live URL when the backend is available. + +`LiveDataSource` now exposes a `close()` method. `ModeHandler.cleanup()` invokes this when present so switching modes or stream strategies cleans up the previous connection and callbacks. + +For detailed message formats see [Stream Mode Web APIs](api/modes.md). diff --git a/docs/websocket-options-sample.html b/docs/websocket-options-sample.html new file mode 100644 index 0000000..4289fce --- /dev/null +++ b/docs/websocket-options-sample.html @@ -0,0 +1,12 @@ + + + + WebSocket Options Panel Sample + + + +
+ +
+ + diff --git a/public/v0.0.1/service-worker.js b/public/v0.0.1/service-worker.js index 11630c0..0c6b7d4 100644 --- a/public/v0.0.1/service-worker.js +++ b/public/v0.0.1/service-worker.js @@ -1,4 +1,4 @@ -const appVersion = 'v0.0.2-alpha'; +const appVersion = 'v0.0.1'; const cacheName = `serviceworker@${appVersion}`; const staticAssets = [ diff --git a/public/v0.0.2-alpha/service-worker.js b/public/v0.0.2-alpha/service-worker.js index ea5cf9b..6fd960a 100644 --- a/public/v0.0.2-alpha/service-worker.js +++ b/public/v0.0.2-alpha/service-worker.js @@ -1,4 +1,4 @@ -const appVersion = 'v0.0.1'; +const appVersion = 'v0.0.2-alpha'; const cacheName = `serviceworker@${appVersion}`; const staticAssets = diff --git a/src/_spwashi@/scripts/env/augmentations.js b/src/_spwashi@/scripts/env/augmentations.js index 557f278..c078e97 100644 --- a/src/_spwashi@/scripts/env/augmentations.js +++ b/src/_spwashi@/scripts/env/augmentations.js @@ -1,4 +1,4 @@ -import {boonConcept, boonNode} from "../../../js/modes/spw/commands/boon"; +import {boonConcept, boonNode} from "../../../js/modes/input/spw/commands/boon"; boonConcept['@node'] = (index, batchSize) => { const currentDayIndex = (new Date()).getDay(); diff --git a/src/_spwashi@/scripts/loop/body.js b/src/_spwashi@/scripts/loop/body.js index dec95bc..ff97284 100644 --- a/src/_spwashi@/scripts/loop/body.js +++ b/src/_spwashi@/scripts/loop/body.js @@ -1,7 +1,7 @@ import {mapNodes, pushNode} from "../../../js/simulation/nodes/data/operate"; -import {clearActiveNodes} from "../../../js/init/hotkeys/handlers/clear-active-nodes"; +import {clearActiveNodes} from "../../../js/ui/hotkeys/handlers/clear-active-nodes"; import {gameState} from "../state/state"; -import {processSpwInput} from "../../../js/modes/spw/process-spw-input"; +import {processSpwInput} from "../../../js/modes/input/spw/process-spw-input"; import {pushLink} from "../../../js/simulation/edges/data/pushLink"; import {mainLoop} from "./head"; diff --git a/src/_spwashi@/scripts/spwashi.js b/src/_spwashi@/scripts/spwashi.js index 2704b0c..6ef54f6 100644 --- a/src/_spwashi@/scripts/spwashi.js +++ b/src/_spwashi@/scripts/spwashi.js @@ -1,7 +1,7 @@ -import "../../stylesheets/main.scss"; +import "../../js/ui/styles/main.scss"; import "../styles/_spwashi@.scss"; -import {app} from '../../js' -import {processSpwInput} from "../../js/modes/spw/process-spw-input"; +import {app} from '../../js/main.js' +import {processSpwInput} from "../../js/modes/input/spw/process-spw-input"; import {mainLoop} from "./loop/head"; import {gameState} from "./state/state"; import "./env/augmentations"; diff --git a/src/index.html b/src/index.html index 1501b4b..4593840 100644 --- a/src/index.html +++ b/src/index.html @@ -4,10 +4,10 @@ - - + + @@ -15,9 +15,9 @@

<concept>

-
+
- +
diff --git a/src/js/init/callbacks/initCallbacks.js b/src/js/bootstrap/callbacks/initCallbacks.js similarity index 100% rename from src/js/init/callbacks/initCallbacks.js rename to src/js/bootstrap/callbacks/initCallbacks.js diff --git a/src/js/bootstrap/hydrate-ui.js b/src/js/bootstrap/hydrate-ui.js new file mode 100644 index 0000000..1aefac1 --- /dev/null +++ b/src/js/bootstrap/hydrate-ui.js @@ -0,0 +1,13 @@ +import {initFocalSquare} from "../ui/components/focal-point"; +import {initH1} from "../ui/components/h1"; +import {initUi} from "./ui"; +import {initStreamContainer} from "../ui/components/stream-container"; +import {initStreamConfig} from "../ui/components/stream-config"; + +export function hydrateUi(mode = window.spwashi.initialMode) { + initFocalSquare(); + initH1(); + initUi(mode); + initStreamContainer(); + initStreamConfig(); +} diff --git a/src/js/bootstrap/init-pipeline.js b/src/js/bootstrap/init-pipeline.js new file mode 100644 index 0000000..51134f5 --- /dev/null +++ b/src/js/bootstrap/init-pipeline.js @@ -0,0 +1,91 @@ +export const InitPhase = { + BOOT: 'boot', + UI: 'ui', + DEFERRED: 'deferred', +}; + +const initSteps = []; +const map = new Map(); + +export function addInitStep(step) { + if (!step) return; + const { id, init, priority = 0, phase, dependsOn = [] } = step; + if (!id || typeof init !== 'function') { + throw new Error('init step requires {id, init}'); + } + if (map.has(id)) { + throw new Error(`init step with id "${id}" already exists`); + } + const entry = { id, init, priority, phase, dependsOn }; + map.set(id, entry); + initSteps.push(entry); +} + +export function addInitSteps(steps) { + if (!steps) return; + if (Array.isArray(steps)) { + steps.forEach(step => { + if (Array.isArray(step)) { + const [id, init] = step; + addInitStep({ id, init }); + } else { + addInitStep(step); + } + }); + } else { + Object.entries(steps).forEach(([id, init]) => addInitStep({ id, init })); + } +} + +export function sortSteps(steps) { + const stepsMap = new Map(steps.map(s => [s.id, s])); + const phaseOrder = [InitPhase.BOOT, InitPhase.UI, InitPhase.DEFERRED]; + const phaseRank = phase => { + const idx = phaseOrder.indexOf(phase); + return idx === -1 ? phaseOrder.length : idx; + }; + const ordered = []; + const visited = new Set(); + const visiting = new Set(); + const path = []; + + function visit(step) { + if (visited.has(step.id)) return; + if (visiting.has(step.id)) { + const cycleStart = path.indexOf(step.id); + const cyclePath = path.slice(cycleStart).concat(step.id).join(' -> '); + throw new Error(`circular dependency detected: ${cyclePath}`); + } + visiting.add(step.id); + path.push(step.id); + (step.dependsOn || []).forEach(dep => { + const depId = typeof dep === 'string' ? dep : dep.id; + const depStep = stepsMap.get(depId); + if (depStep) visit(depStep); + }); + visiting.delete(step.id); + path.pop(); + visited.add(step.id); + ordered.push(step); + } + + const sorted = [...steps].sort((a, b) => { + const phaseDiff = phaseRank(a.phase) - phaseRank(b.phase); + if (phaseDiff !== 0) return phaseDiff; + return (a.priority ?? 0) - (b.priority ?? 0); + }); + sorted.forEach(visit); + return ordered; +} + +export async function runInitPipeline() { + const ordered = sortSteps(initSteps); + for (const { init } of ordered) { + await init(); + } +} + +export function clearInitPipeline() { + initSteps.length = 0; + map.clear(); +} diff --git a/src/js/bootstrap/init-steps/index.js b/src/js/bootstrap/init-steps/index.js new file mode 100644 index 0000000..a3d1039 --- /dev/null +++ b/src/js/bootstrap/init-steps/index.js @@ -0,0 +1,67 @@ +import { InitPhase } from '../init-pipeline'; +import { initAnalytics } from '../meta/analytics'; +import { initParameters } from '../bootstrap/parameters/init'; +import { initRoot } from '../bootstrap/root'; +import { initSite } from '../bootstrap/site'; +import { loadParameters } from '../bootstrap/parameters/read'; +import { initSvgEvents } from '../simulation/events'; +import { simulationElements } from '../simulation/basic'; +import { hydrateUi } from '../bootstrap/hydrate-ui'; + +export const analyticsStep = { + id: 'analytics', + init: initAnalytics, + priority: 0, + phase: InitPhase.BOOT, +}; +export const parametersStep = { + id: 'parameters', + init: initParameters, + priority: 1, + phase: InitPhase.BOOT, +}; +export const rootStep = { + id: 'root', + init: initRoot, + priority: 2, + phase: InitPhase.BOOT, +}; +export const siteStep = { + id: 'site', + init: initSite, + priority: 3, + phase: InitPhase.BOOT, +}; +export const queryParamsStep = { + id: 'queryParams', + init: () => loadParameters(new URLSearchParams(window.location.search)), + priority: 4, + phase: InitPhase.BOOT, + dependsOn: [parametersStep], +}; +export const svgStep = { + id: 'svg', + init: () => initSvgEvents(simulationElements.svg), + priority: 5, + phase: InitPhase.UI, + dependsOn: [rootStep], +}; +export const uiStep = { + id: 'ui', + init: () => hydrateUi(window.spwashi.initialMode), + priority: 6, + phase: InitPhase.UI, + dependsOn: [svgStep, parametersStep], +}; + +export const stepsById = { + analytics: analyticsStep, + parameters: parametersStep, + root: rootStep, + site: siteStep, + queryParams: queryParamsStep, + svg: svgStep, + ui: uiStep, +}; + +export const defaultInitSteps = Object.values(stepsById); diff --git a/src/js/init/listeners/initDocumentMousedown.js b/src/js/bootstrap/listeners/initDocumentMousedown.js similarity index 82% rename from src/js/init/listeners/initDocumentMousedown.js rename to src/js/bootstrap/listeners/initDocumentMousedown.js index 66abc45..beae84c 100644 --- a/src/js/init/listeners/initDocumentMousedown.js +++ b/src/js/bootstrap/listeners/initDocumentMousedown.js @@ -1,4 +1,4 @@ -import {initFocalSquare} from "../../ui/focal-point"; +import {initFocalSquare} from "../../ui/components/focal-point"; export function initDocumentMousedown() { document.body.addEventListener('mousedown', (e) => { diff --git a/src/js/init/listeners/initListeners.js b/src/js/bootstrap/listeners/initListeners.js similarity index 97% rename from src/js/init/listeners/initListeners.js rename to src/js/bootstrap/listeners/initListeners.js index 32c545c..779fd7b 100644 --- a/src/js/init/listeners/initListeners.js +++ b/src/js/bootstrap/listeners/initListeners.js @@ -1,5 +1,5 @@ import { initDocumentMousedown } from "./initDocumentMousedown"; -import { attachFocalPointToElementPosition, focalPoint, initFocalSquare, updateFocalPoint } from "../../ui/focal-point"; +import { attachFocalPointToElementPosition, focalPoint, initFocalSquare, updateFocalPoint } from "../../ui/components/focal-point"; import { onReflexModeStart } from "../../modes/reflex/mode-reflex"; import { onColorModeStart } from "../../modes/dataindex/mode-dataindex"; diff --git a/src/js/init/parameters/_.js b/src/js/bootstrap/parameters/_.js similarity index 100% rename from src/js/init/parameters/_.js rename to src/js/bootstrap/parameters/_.js diff --git a/src/js/init/parameters/init.js b/src/js/bootstrap/parameters/init.js similarity index 100% rename from src/js/init/parameters/init.js rename to src/js/bootstrap/parameters/init.js diff --git a/src/js/init/parameters/options/alpha.js b/src/js/bootstrap/parameters/options/alpha.js similarity index 100% rename from src/js/init/parameters/options/alpha.js rename to src/js/bootstrap/parameters/options/alpha.js diff --git a/src/js/init/parameters/options/alphaDecay.js b/src/js/bootstrap/parameters/options/alphaDecay.js similarity index 100% rename from src/js/init/parameters/options/alphaDecay.js rename to src/js/bootstrap/parameters/options/alphaDecay.js diff --git a/src/js/init/parameters/options/alphaTarget.js b/src/js/bootstrap/parameters/options/alphaTarget.js similarity index 100% rename from src/js/init/parameters/options/alphaTarget.js rename to src/js/bootstrap/parameters/options/alphaTarget.js diff --git a/src/js/init/parameters/options/boundingBox.js b/src/js/bootstrap/parameters/options/boundingBox.js similarity index 100% rename from src/js/init/parameters/options/boundingBox.js rename to src/js/bootstrap/parameters/options/boundingBox.js diff --git a/src/js/init/parameters/options/center.js b/src/js/bootstrap/parameters/options/center.js similarity index 100% rename from src/js/init/parameters/options/center.js rename to src/js/bootstrap/parameters/options/center.js diff --git a/src/js/init/parameters/options/centerStrength.js b/src/js/bootstrap/parameters/options/centerStrength.js similarity index 100% rename from src/js/init/parameters/options/centerStrength.js rename to src/js/bootstrap/parameters/options/centerStrength.js diff --git a/src/js/init/parameters/options/charge.js b/src/js/bootstrap/parameters/options/charge.js similarity index 100% rename from src/js/init/parameters/options/charge.js rename to src/js/bootstrap/parameters/options/charge.js diff --git a/src/js/init/parameters/options/dataindex.js b/src/js/bootstrap/parameters/options/dataindex.js similarity index 92% rename from src/js/init/parameters/options/dataindex.js rename to src/js/bootstrap/parameters/options/dataindex.js index 700f384..d6aed32 100644 --- a/src/js/init/parameters/options/dataindex.js +++ b/src/js/bootstrap/parameters/options/dataindex.js @@ -1,4 +1,4 @@ -import {getDataIndexForNumber, setDocumentDataIndex} from "../../../modes/dataindex/util"; +import {getDataIndexForNumber, setDocumentDataIndex} from "../../../modes/input/dataindex/util"; export function dataindex(searchParameters) { if (searchParameters.has('dataindex')) { diff --git a/src/js/init/parameters/options/debug.js b/src/js/bootstrap/parameters/options/debug.js similarity index 100% rename from src/js/init/parameters/options/debug.js rename to src/js/bootstrap/parameters/options/debug.js diff --git a/src/js/init/parameters/options/defaultRadius.js b/src/js/bootstrap/parameters/options/defaultRadius.js similarity index 100% rename from src/js/init/parameters/options/defaultRadius.js rename to src/js/bootstrap/parameters/options/defaultRadius.js diff --git a/src/js/init/parameters/options/display.js b/src/js/bootstrap/parameters/options/display.js similarity index 100% rename from src/js/init/parameters/options/display.js rename to src/js/bootstrap/parameters/options/display.js diff --git a/src/js/init/parameters/options/doFetch.js b/src/js/bootstrap/parameters/options/doFetch.js similarity index 100% rename from src/js/init/parameters/options/doFetch.js rename to src/js/bootstrap/parameters/options/doFetch.js diff --git a/src/js/init/parameters/options/fontSize.js b/src/js/bootstrap/parameters/options/fontSize.js similarity index 100% rename from src/js/init/parameters/options/fontSize.js rename to src/js/bootstrap/parameters/options/fontSize.js diff --git a/src/js/init/parameters/options/fx.js b/src/js/bootstrap/parameters/options/fx.js similarity index 100% rename from src/js/init/parameters/options/fx.js rename to src/js/bootstrap/parameters/options/fx.js diff --git a/src/js/init/parameters/options/fy.js b/src/js/bootstrap/parameters/options/fy.js similarity index 100% rename from src/js/init/parameters/options/fy.js rename to src/js/bootstrap/parameters/options/fy.js diff --git a/src/js/init/parameters/options/height.js b/src/js/bootstrap/parameters/options/height.js similarity index 100% rename from src/js/init/parameters/options/height.js rename to src/js/bootstrap/parameters/options/height.js diff --git a/src/js/init/parameters/options/intent.js b/src/js/bootstrap/parameters/options/intent.js similarity index 100% rename from src/js/init/parameters/options/intent.js rename to src/js/bootstrap/parameters/options/intent.js diff --git a/src/js/init/parameters/options/linkStrength.js b/src/js/bootstrap/parameters/options/linkStrength.js similarity index 100% rename from src/js/init/parameters/options/linkStrength.js rename to src/js/bootstrap/parameters/options/linkStrength.js diff --git a/src/js/init/parameters/options/linkStyle.js b/src/js/bootstrap/parameters/options/linkStyle.js similarity index 100% rename from src/js/init/parameters/options/linkStyle.js rename to src/js/bootstrap/parameters/options/linkStyle.js diff --git a/src/js/init/parameters/options/mode.js b/src/js/bootstrap/parameters/options/mode.js similarity index 84% rename from src/js/init/parameters/options/mode.js rename to src/js/bootstrap/parameters/options/mode.js index 4b94155..9315791 100644 --- a/src/js/init/parameters/options/mode.js +++ b/src/js/bootstrap/parameters/options/mode.js @@ -1,4 +1,4 @@ -import {setDocumentMode} from "../../../modes"; +import {setDocumentMode} from "../../../modes/input"; export function mode(searchParameters) { let mode; diff --git a/src/js/init/parameters/options/nodeCount.js b/src/js/bootstrap/parameters/options/nodeCount.js similarity index 100% rename from src/js/init/parameters/options/nodeCount.js rename to src/js/bootstrap/parameters/options/nodeCount.js diff --git a/src/js/init/parameters/options/perspective.js b/src/js/bootstrap/parameters/options/perspective.js similarity index 100% rename from src/js/init/parameters/options/perspective.js rename to src/js/bootstrap/parameters/options/perspective.js diff --git a/src/js/init/parameters/options/r.js b/src/js/bootstrap/parameters/options/r.js similarity index 100% rename from src/js/init/parameters/options/r.js rename to src/js/bootstrap/parameters/options/r.js diff --git a/src/js/init/parameters/options/reset.js b/src/js/bootstrap/parameters/options/reset.js similarity index 100% rename from src/js/init/parameters/options/reset.js rename to src/js/bootstrap/parameters/options/reset.js diff --git a/src/js/init/parameters/options/size.js b/src/js/bootstrap/parameters/options/size.js similarity index 100% rename from src/js/init/parameters/options/size.js rename to src/js/bootstrap/parameters/options/size.js diff --git a/src/js/init/parameters/options/story.js b/src/js/bootstrap/parameters/options/story.js similarity index 100% rename from src/js/init/parameters/options/story.js rename to src/js/bootstrap/parameters/options/story.js diff --git a/src/js/init/parameters/options/superpower.js b/src/js/bootstrap/parameters/options/superpower.js similarity index 100% rename from src/js/init/parameters/options/superpower.js rename to src/js/bootstrap/parameters/options/superpower.js diff --git a/src/js/init/parameters/options/title.js b/src/js/bootstrap/parameters/options/title.js similarity index 100% rename from src/js/init/parameters/options/title.js rename to src/js/bootstrap/parameters/options/title.js diff --git a/src/js/init/parameters/options/velocityDecay.js b/src/js/bootstrap/parameters/options/velocityDecay.js similarity index 100% rename from src/js/init/parameters/options/velocityDecay.js rename to src/js/bootstrap/parameters/options/velocityDecay.js diff --git a/src/js/init/parameters/options/width.js b/src/js/bootstrap/parameters/options/width.js similarity index 100% rename from src/js/init/parameters/options/width.js rename to src/js/bootstrap/parameters/options/width.js diff --git a/src/js/init/parameters/options/zoom.js b/src/js/bootstrap/parameters/options/zoom.js similarity index 100% rename from src/js/init/parameters/options/zoom.js rename to src/js/bootstrap/parameters/options/zoom.js diff --git a/src/js/init/parameters/read.js b/src/js/bootstrap/parameters/read.js similarity index 99% rename from src/js/init/parameters/read.js rename to src/js/bootstrap/parameters/read.js index 205894a..af441fd 100644 --- a/src/js/init/parameters/read.js +++ b/src/js/bootstrap/parameters/read.js @@ -4,4 +4,4 @@ export function loadParameters(searchParameters) { window.spwashi.featuredIdentity = /\/identity\/([a-zA-Z\d]+)/.exec(window.location.href)?.[1] || searchParameters.get('identity'); window.spwashi.parameterKey = `spwashi.parameters#${window.spwashi.featuredIdentity}`; parameterList.forEach(fn => fn(searchParameters)); -} \ No newline at end of file +} diff --git a/src/js/init/root.js b/src/js/bootstrap/root.js similarity index 92% rename from src/js/init/root.js rename to src/js/bootstrap/root.js index e1cf62f..510b7f8 100644 --- a/src/js/init/root.js +++ b/src/js/bootstrap/root.js @@ -1,9 +1,9 @@ import {initSimulationRoot} from "../simulation/simulation"; import {initCallbacks} from "./callbacks/initCallbacks"; import {initListeners} from "./listeners/initListeners"; -import {pushHelpTopics} from "../modes/spw/commands/help"; -import {setDocumentMode} from "../modes"; -import {processSpwInput} from "../modes/spw/process-spw-input"; +import {pushHelpTopics} from "../modes/input/spw/commands/help"; +import {setDocumentMode} from "../modes/input"; +import {processSpwInput} from "../modes/input/spw/process-spw-input"; export function initRoot() { initSimulationRoot(); diff --git a/src/js/init/site.js b/src/js/bootstrap/site.js similarity index 100% rename from src/js/init/site.js rename to src/js/bootstrap/site.js diff --git a/src/js/init/ui.js b/src/js/bootstrap/ui.js similarity index 85% rename from src/js/init/ui.js rename to src/js/bootstrap/ui.js index 1040c41..8b97824 100644 --- a/src/js/init/ui.js +++ b/src/js/bootstrap/ui.js @@ -5,9 +5,9 @@ import {initializeStoryMode} from "../modes/story/mode-story"; import {initializeModeSelection} from "../modes"; import {initializeDataindexMode} from "../modes/dataindex/mode-dataindex"; import {initKeystrokes} from "./hotkeys/_"; -import {initializeForceSimulationControls} from "../ui/simulation-controls"; +import {initializeForceSimulationControls} from "../ui/components/simulation-controls"; import {initializeSpwParseField} from "../modes/spw/mode-spw"; -import {initEnableSoundsButton} from "../ui/enable-sounds-button"; +import {initEnableSoundsButton} from "../ui/components/enable-sounds-button"; export function initUi(mode) { diff --git a/src/js/config/mode-docs.js b/src/js/config/mode-docs.js new file mode 100644 index 0000000..a62c83b --- /dev/null +++ b/src/js/config/mode-docs.js @@ -0,0 +1,11 @@ +export const MODE_DOCS = { + boon: { file: 'docs/api/modes.md#boon' }, + bane: { file: 'docs/api/modes.md#bane' }, + bone: { file: 'docs/api/modes.md#bone' }, + bonk: { file: 'docs/api/modes.md#bonk' }, + honk: { file: 'docs/api/modes.md#honk' }, + boof: { url: 'https://example.com/boof-api', file: 'docs/api/modes.md#boof' }, + lore: { file: 'docs/api/modes.md#lore' }, + focal: { file: 'docs/api/modes.md#focal' }, + passive: { file: 'docs/api/modes.md#passive' }, +}; diff --git a/src/js/index.js b/src/js/index.js deleted file mode 100644 index aa0adc6..0000000 --- a/src/js/index.js +++ /dev/null @@ -1,65 +0,0 @@ -import {initFocalSquare} from "./ui/focal-point"; -import {initH1} from "./ui/h1"; -import {initUi} from "./init/ui"; -import {initRoot} from "./init/root"; -import {initSvgEvents} from "./simulation/events"; -import {simulationElements} from "./simulation/basic"; -import {initParameters} from "./init/parameters/init"; -import {loadParameters} from "./init/parameters/read"; -import {initSite} from "./init/site"; -import {initAnalytics} from "./meta/analytics"; -import {initWebSocketContainer} from "./ui/websocket-container"; - -const versions = { - 'v0.0.1': { - assetPath: 'v0.0.1', - }, - 'v0.0.2': { - assetPath: 'v0.0.2-alpha' - } -} - -async function registerServiceWorker(version = 'v0.0.2') { - if (!('serviceWorker' in navigator)) { - return; - } - - const {assetPath} = versions[version]; - - try { - const registration = await navigator.serviceWorker.register(`/${assetPath}/service-worker.js`); - console.log('Service Worker registered with scope:', registration.scope); - } catch (e) { - console.log('Service Worker registration failed:', e); - } -} - -export async function app() { - let serviceWorkerRegistered = registerServiceWorker(); - - window.spwashi = {}; - - initAnalytics(); - initParameters(); - initRoot(); - initSite(); - - // initialize context-sensitive parameters - loadParameters(new URLSearchParams(window.location.search)); - - // primary interactive elements - initSvgEvents(simulationElements.svg); - initFocalSquare(); - initH1(); - - // progressive enhancement - initUi(window.spwashi.initialMode); - initWebSocketContainer(); - - return Promise.all([serviceWorkerRegistered]) - .then(() => { - setTimeout(() => { - console.log('app is ready'); - }, 1000); - }); -} \ No newline at end of file diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..f5f2981 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,43 @@ +import { addInitSteps, runInitPipeline } from "./bootstrap/init-pipeline"; +import { defaultInitSteps } from "./bootstrap/init-steps"; + +const versions = { + 'v0.0.1': { + assetPath: 'v0.0.1', + }, + 'v0.0.2': { + assetPath: 'v0.0.2-alpha' + } +} + +async function registerServiceWorker(version = 'v0.0.2') { + if (!('serviceWorker' in navigator)) { + return; + } + + const {assetPath} = versions[version]; + + try { + const registration = await navigator.serviceWorker.register(`/${assetPath}/service-worker.js`); + console.log('Service Worker registered with scope:', registration.scope); + } catch (e) { + console.log('Service Worker registration failed:', e); + } +} + +export async function app() { + let serviceWorkerRegistered = registerServiceWorker(); + + window.spwashi = {}; + + addInitSteps(defaultInitSteps); + + await runInitPipeline(); + + return Promise.all([serviceWorkerRegistered]) + .then(() => { + setTimeout(() => { + console.log('app is ready'); + }, 1000); + }); +} diff --git a/src/js/modes/dataindex/mode-dataindex.js b/src/js/modes/input/dataindex/mode-dataindex.js similarity index 100% rename from src/js/modes/dataindex/mode-dataindex.js rename to src/js/modes/input/dataindex/mode-dataindex.js diff --git a/src/js/modes/dataindex/util.js b/src/js/modes/input/dataindex/util.js similarity index 100% rename from src/js/modes/dataindex/util.js rename to src/js/modes/input/dataindex/util.js diff --git a/src/js/modes/direct/mode-direct.js b/src/js/modes/input/direct/mode-direct.js similarity index 100% rename from src/js/modes/direct/mode-direct.js rename to src/js/modes/input/direct/mode-direct.js diff --git a/src/js/modes/index.js b/src/js/modes/input/index.js similarity index 100% rename from src/js/modes/index.js rename to src/js/modes/input/index.js diff --git a/src/js/modes/mapfilter/mode-mapfilter.js b/src/js/modes/input/mapfilter/mode-mapfilter.js similarity index 100% rename from src/js/modes/mapfilter/mode-mapfilter.js rename to src/js/modes/input/mapfilter/mode-mapfilter.js diff --git a/src/js/modes/querystring/mode-querystring.js b/src/js/modes/input/querystring/mode-querystring.js similarity index 92% rename from src/js/modes/querystring/mode-querystring.js rename to src/js/modes/input/querystring/mode-querystring.js index e110ded..caece7a 100644 --- a/src/js/modes/querystring/mode-querystring.js +++ b/src/js/modes/input/querystring/mode-querystring.js @@ -1,4 +1,4 @@ -import {loadParameters} from "../../init/parameters/read"; +import {loadParameters} from "../../bootstrap/parameters/read"; export function initializeQuerystringMode() { const element = document.querySelector('#query-parameters .value'); diff --git a/src/js/modes/reflex/mode-reflex.js b/src/js/modes/input/reflex/mode-reflex.js similarity index 100% rename from src/js/modes/reflex/mode-reflex.js rename to src/js/modes/input/reflex/mode-reflex.js diff --git a/src/js/modes/spw/commands/_.js b/src/js/modes/input/spw/commands/_.js similarity index 100% rename from src/js/modes/spw/commands/_.js rename to src/js/modes/input/spw/commands/_.js diff --git a/src/js/modes/spw/commands/all.js b/src/js/modes/input/spw/commands/all.js similarity index 100% rename from src/js/modes/spw/commands/all.js rename to src/js/modes/input/spw/commands/all.js diff --git a/src/js/modes/spw/commands/arrange.js b/src/js/modes/input/spw/commands/arrange.js similarity index 100% rename from src/js/modes/spw/commands/arrange.js rename to src/js/modes/input/spw/commands/arrange.js diff --git a/src/js/modes/spw/commands/bane.js b/src/js/modes/input/spw/commands/bane.js similarity index 100% rename from src/js/modes/spw/commands/bane.js rename to src/js/modes/input/spw/commands/bane.js diff --git a/src/js/modes/spw/commands/bone.js b/src/js/modes/input/spw/commands/bone.js similarity index 100% rename from src/js/modes/spw/commands/bone.js rename to src/js/modes/input/spw/commands/bone.js diff --git a/src/js/modes/spw/commands/bonk.js b/src/js/modes/input/spw/commands/bonk.js similarity index 100% rename from src/js/modes/spw/commands/bonk.js rename to src/js/modes/input/spw/commands/bonk.js diff --git a/src/js/modes/spw/commands/boon.js b/src/js/modes/input/spw/commands/boon.js similarity index 100% rename from src/js/modes/spw/commands/boon.js rename to src/js/modes/input/spw/commands/boon.js diff --git a/src/js/modes/spw/commands/center.js b/src/js/modes/input/spw/commands/center.js similarity index 100% rename from src/js/modes/spw/commands/center.js rename to src/js/modes/input/spw/commands/center.js diff --git a/src/js/modes/spw/commands/clear-page-image.js b/src/js/modes/input/spw/commands/clear-page-image.js similarity index 69% rename from src/js/modes/spw/commands/clear-page-image.js rename to src/js/modes/input/spw/commands/clear-page-image.js index 4ae054e..f6e344a 100644 --- a/src/js/modes/spw/commands/clear-page-image.js +++ b/src/js/modes/input/spw/commands/clear-page-image.js @@ -1,4 +1,4 @@ -import {initPageImage} from "../../../ui/page-image"; +import {initPageImage} from "../../../ui/components/page-image"; export function runClearPageImageCommand() { { diff --git a/src/js/modes/spw/commands/clear.js b/src/js/modes/input/spw/commands/clear.js similarity index 100% rename from src/js/modes/spw/commands/clear.js rename to src/js/modes/input/spw/commands/clear.js diff --git a/src/js/modes/spw/commands/clicked.js b/src/js/modes/input/spw/commands/clicked.js similarity index 100% rename from src/js/modes/spw/commands/clicked.js rename to src/js/modes/input/spw/commands/clicked.js diff --git a/src/js/modes/spw/commands/cluster.js b/src/js/modes/input/spw/commands/cluster.js similarity index 100% rename from src/js/modes/spw/commands/cluster.js rename to src/js/modes/input/spw/commands/cluster.js diff --git a/src/js/modes/spw/commands/collision-radius.js b/src/js/modes/input/spw/commands/collision-radius.js similarity index 100% rename from src/js/modes/spw/commands/collision-radius.js rename to src/js/modes/input/spw/commands/collision-radius.js diff --git a/src/js/modes/spw/commands/default.js b/src/js/modes/input/spw/commands/default.js similarity index 100% rename from src/js/modes/spw/commands/default.js rename to src/js/modes/input/spw/commands/default.js diff --git a/src/js/modes/spw/commands/demo.js b/src/js/modes/input/spw/commands/demo.js similarity index 100% rename from src/js/modes/spw/commands/demo.js rename to src/js/modes/input/spw/commands/demo.js diff --git a/src/js/modes/spw/commands/display-nodes.js b/src/js/modes/input/spw/commands/display-nodes.js similarity index 71% rename from src/js/modes/spw/commands/display-nodes.js rename to src/js/modes/input/spw/commands/display-nodes.js index 3655e23..325e504 100644 --- a/src/js/modes/spw/commands/display-nodes.js +++ b/src/js/modes/input/spw/commands/display-nodes.js @@ -1,4 +1,4 @@ -import {loadParameters} from "../../../init/parameters/read"; +import {loadParameters} from "../../../bootstrap/parameters/read"; export function runDisplayNodesCommand() { { diff --git a/src/js/modes/spw/commands/extended-menu.js b/src/js/modes/input/spw/commands/extended-menu.js similarity index 75% rename from src/js/modes/spw/commands/extended-menu.js rename to src/js/modes/input/spw/commands/extended-menu.js index e19f2da..28f4a39 100644 --- a/src/js/modes/spw/commands/extended-menu.js +++ b/src/js/modes/input/spw/commands/extended-menu.js @@ -1,4 +1,4 @@ -import {initKeystrokes} from "../../../init/hotkeys/_"; +import {initKeystrokes} from "../../../ui/hotkeys/_"; export const extendMenu = `extended menu`.trim(); diff --git a/src/js/modes/spw/commands/forces.js b/src/js/modes/input/spw/commands/forces.js similarity index 100% rename from src/js/modes/spw/commands/forces.js rename to src/js/modes/input/spw/commands/forces.js diff --git a/src/js/modes/spw/commands/freeze.js b/src/js/modes/input/spw/commands/freeze.js similarity index 100% rename from src/js/modes/spw/commands/freeze.js rename to src/js/modes/input/spw/commands/freeze.js diff --git a/src/js/modes/spw/commands/group.js b/src/js/modes/input/spw/commands/group.js similarity index 100% rename from src/js/modes/spw/commands/group.js rename to src/js/modes/input/spw/commands/group.js diff --git a/src/js/modes/spw/commands/help.js b/src/js/modes/input/spw/commands/help.js similarity index 100% rename from src/js/modes/spw/commands/help.js rename to src/js/modes/input/spw/commands/help.js diff --git a/src/js/modes/spw/commands/home.js b/src/js/modes/input/spw/commands/home.js similarity index 100% rename from src/js/modes/spw/commands/home.js rename to src/js/modes/input/spw/commands/home.js diff --git a/src/js/modes/spw/commands/honk.js b/src/js/modes/input/spw/commands/honk.js similarity index 100% rename from src/js/modes/spw/commands/honk.js rename to src/js/modes/input/spw/commands/honk.js diff --git a/src/js/modes/spw/commands/link.js b/src/js/modes/input/spw/commands/link.js similarity index 100% rename from src/js/modes/spw/commands/link.js rename to src/js/modes/input/spw/commands/link.js diff --git a/src/js/modes/spw/commands/minimalism.js b/src/js/modes/input/spw/commands/minimalism.js similarity index 100% rename from src/js/modes/spw/commands/minimalism.js rename to src/js/modes/input/spw/commands/minimalism.js diff --git a/src/js/modes/spw/commands/no-center.js b/src/js/modes/input/spw/commands/no-center.js similarity index 100% rename from src/js/modes/spw/commands/no-center.js rename to src/js/modes/input/spw/commands/no-center.js diff --git a/src/js/modes/spw/commands/prune.js b/src/js/modes/input/spw/commands/prune.js similarity index 100% rename from src/js/modes/spw/commands/prune.js rename to src/js/modes/input/spw/commands/prune.js diff --git a/src/js/modes/input/spw/commands/save.js b/src/js/modes/input/spw/commands/save.js new file mode 100644 index 0000000..f7f10a7 --- /dev/null +++ b/src/js/modes/input/spw/commands/save.js @@ -0,0 +1,5 @@ +import {saveActiveNodes} from "../../../ui/hotkeys/handlers/save-active-nodes"; + +export function runSaveCommand() { + saveActiveNodes(); +} diff --git a/src/js/modes/spw/commands/scale.js b/src/js/modes/input/spw/commands/scale.js similarity index 100% rename from src/js/modes/spw/commands/scale.js rename to src/js/modes/input/spw/commands/scale.js diff --git a/src/js/modes/spw/commands/scatter.js b/src/js/modes/input/spw/commands/scatter.js similarity index 100% rename from src/js/modes/spw/commands/scatter.js rename to src/js/modes/input/spw/commands/scatter.js diff --git a/src/js/modes/spw/commands/selected.js b/src/js/modes/input/spw/commands/selected.js similarity index 100% rename from src/js/modes/spw/commands/selected.js rename to src/js/modes/input/spw/commands/selected.js diff --git a/src/js/modes/spw/commands/sort.js b/src/js/modes/input/spw/commands/sort.js similarity index 100% rename from src/js/modes/spw/commands/sort.js rename to src/js/modes/input/spw/commands/sort.js diff --git a/src/js/modes/spw/commands/sounds.js b/src/js/modes/input/spw/commands/sounds.js similarity index 100% rename from src/js/modes/spw/commands/sounds.js rename to src/js/modes/input/spw/commands/sounds.js diff --git a/src/js/modes/spw/commands/spwashi.js b/src/js/modes/input/spw/commands/spwashi.js similarity index 100% rename from src/js/modes/spw/commands/spwashi.js rename to src/js/modes/input/spw/commands/spwashi.js diff --git a/src/js/modes/spw/commands/title.js b/src/js/modes/input/spw/commands/title.js similarity index 100% rename from src/js/modes/spw/commands/title.js rename to src/js/modes/input/spw/commands/title.js diff --git a/src/js/modes/spw/commands/unfreeze.js b/src/js/modes/input/spw/commands/unfreeze.js similarity index 100% rename from src/js/modes/spw/commands/unfreeze.js rename to src/js/modes/input/spw/commands/unfreeze.js diff --git a/src/js/modes/spw/commands/unlink.js b/src/js/modes/input/spw/commands/unlink.js similarity index 100% rename from src/js/modes/spw/commands/unlink.js rename to src/js/modes/input/spw/commands/unlink.js diff --git a/src/js/modes/spw/execute-command.js b/src/js/modes/input/spw/execute-command.js similarity index 100% rename from src/js/modes/spw/execute-command.js rename to src/js/modes/input/spw/execute-command.js diff --git a/src/js/modes/spw/handlers/_.js b/src/js/modes/input/spw/handlers/_.js similarity index 100% rename from src/js/modes/spw/handlers/_.js rename to src/js/modes/input/spw/handlers/_.js diff --git a/src/js/modes/spw/handlers/addHandler.js b/src/js/modes/input/spw/handlers/addHandler.js similarity index 100% rename from src/js/modes/spw/handlers/addHandler.js rename to src/js/modes/input/spw/handlers/addHandler.js diff --git a/src/js/modes/spw/handlers/atHandler.js b/src/js/modes/input/spw/handlers/atHandler.js similarity index 100% rename from src/js/modes/spw/handlers/atHandler.js rename to src/js/modes/input/spw/handlers/atHandler.js diff --git a/src/js/modes/spw/handlers/boxHandler.js b/src/js/modes/input/spw/handlers/boxHandler.js similarity index 100% rename from src/js/modes/spw/handlers/boxHandler.js rename to src/js/modes/input/spw/handlers/boxHandler.js diff --git a/src/js/modes/spw/handlers/chargeHandler.js b/src/js/modes/input/spw/handlers/chargeHandler.js similarity index 100% rename from src/js/modes/spw/handlers/chargeHandler.js rename to src/js/modes/input/spw/handlers/chargeHandler.js diff --git a/src/js/modes/spw/handlers/collisionRadiusHandler.js b/src/js/modes/input/spw/handlers/collisionRadiusHandler.js similarity index 100% rename from src/js/modes/spw/handlers/collisionRadiusHandler.js rename to src/js/modes/input/spw/handlers/collisionRadiusHandler.js diff --git a/src/js/modes/spw/handlers/colorHandler.js b/src/js/modes/input/spw/handlers/colorHandler.js similarity index 100% rename from src/js/modes/spw/handlers/colorHandler.js rename to src/js/modes/input/spw/handlers/colorHandler.js diff --git a/src/js/modes/spw/handlers/fontSizeHandler.js b/src/js/modes/input/spw/handlers/fontSizeHandler.js similarity index 100% rename from src/js/modes/spw/handlers/fontSizeHandler.js rename to src/js/modes/input/spw/handlers/fontSizeHandler.js diff --git a/src/js/modes/spw/handlers/nameHandler.js b/src/js/modes/input/spw/handlers/nameHandler.js similarity index 100% rename from src/js/modes/spw/handlers/nameHandler.js rename to src/js/modes/input/spw/handlers/nameHandler.js diff --git a/src/js/modes/spw/handlers/nodeQueueHandler.js b/src/js/modes/input/spw/handlers/nodeQueueHandler.js similarity index 100% rename from src/js/modes/spw/handlers/nodeQueueHandler.js rename to src/js/modes/input/spw/handlers/nodeQueueHandler.js diff --git a/src/js/modes/spw/handlers/radiusHandler.js b/src/js/modes/input/spw/handlers/radiusHandler.js similarity index 100% rename from src/js/modes/spw/handlers/radiusHandler.js rename to src/js/modes/input/spw/handlers/radiusHandler.js diff --git a/src/js/modes/spw/handlers/selectHandler.js b/src/js/modes/input/spw/handlers/selectHandler.js similarity index 100% rename from src/js/modes/spw/handlers/selectHandler.js rename to src/js/modes/input/spw/handlers/selectHandler.js diff --git a/src/js/modes/spw/handlers/sizeHandler.js b/src/js/modes/input/spw/handlers/sizeHandler.js similarity index 100% rename from src/js/modes/spw/handlers/sizeHandler.js rename to src/js/modes/input/spw/handlers/sizeHandler.js diff --git a/src/js/modes/spw/handlers/sortHandler.js b/src/js/modes/input/spw/handlers/sortHandler.js similarity index 100% rename from src/js/modes/spw/handlers/sortHandler.js rename to src/js/modes/input/spw/handlers/sortHandler.js diff --git a/src/js/modes/spw/handlers/superpowerHandler.js b/src/js/modes/input/spw/handlers/superpowerHandler.js similarity index 100% rename from src/js/modes/spw/handlers/superpowerHandler.js rename to src/js/modes/input/spw/handlers/superpowerHandler.js diff --git a/src/js/modes/spw/handlers/urlHandler.js b/src/js/modes/input/spw/handlers/urlHandler.js similarity index 100% rename from src/js/modes/spw/handlers/urlHandler.js rename to src/js/modes/input/spw/handlers/urlHandler.js diff --git a/src/js/modes/spw/handlers/velocityDecayHandler.js b/src/js/modes/input/spw/handlers/velocityDecayHandler.js similarity index 100% rename from src/js/modes/spw/handlers/velocityDecayHandler.js rename to src/js/modes/input/spw/handlers/velocityDecayHandler.js diff --git a/src/js/modes/spw/mode-spw.js b/src/js/modes/input/spw/mode-spw.js similarity index 94% rename from src/js/modes/spw/mode-spw.js rename to src/js/modes/input/spw/mode-spw.js index a087488..f7d2652 100644 --- a/src/js/modes/spw/mode-spw.js +++ b/src/js/modes/input/spw/mode-spw.js @@ -1,10 +1,10 @@ import {CharacterCursor} from "../../vendor/spw/core/node/cursor.mjs"; import {parse} from "../../vendor/spw/parser/parse.mjs"; import {NODE_MANAGER} from "../../simulation/nodes/nodes"; -import {initFocalSquare} from "../../ui/focal-point"; +import {initFocalSquare} from "../../ui/components/focal-point"; import {setDocumentMode} from "../index"; import {initSpwParseField, processSpwInput} from "./process-spw-input"; -import {initPageImage} from "../../ui/page-image"; +import {initPageImage} from "../../ui/components/page-image"; import {mapNodes, pushNode} from "../../simulation/nodes/data/operate"; import {getDocumentDataIndex} from "../dataindex/util"; diff --git a/src/js/modes/spw/process-line.js b/src/js/modes/input/spw/process-line.js similarity index 100% rename from src/js/modes/spw/process-line.js rename to src/js/modes/input/spw/process-line.js diff --git a/src/js/modes/spw/process-spw-input.js b/src/js/modes/input/spw/process-spw-input.js similarity index 97% rename from src/js/modes/spw/process-spw-input.js rename to src/js/modes/input/spw/process-spw-input.js index 26052b1..ef7f7bc 100644 --- a/src/js/modes/spw/process-spw-input.js +++ b/src/js/modes/input/spw/process-spw-input.js @@ -1,4 +1,4 @@ -import {initPageImage, setPageImage} from "../../ui/page-image"; +import {initPageImage, setPageImage} from "../../ui/components/page-image"; import {processLine} from "./process-line"; import {processNode} from "../../simulation/nodes/data/process"; import {NODE_MANAGER} from "../../simulation/nodes/nodes"; diff --git a/src/js/modes/story/mode-story.js b/src/js/modes/input/story/mode-story.js similarity index 97% rename from src/js/modes/story/mode-story.js rename to src/js/modes/input/story/mode-story.js index 98fa4bf..e7c7e47 100644 --- a/src/js/modes/story/mode-story.js +++ b/src/js/modes/input/story/mode-story.js @@ -5,8 +5,8 @@ import {NODE_MANAGER} from "../../simulation/nodes/nodes"; import {extendMenu} from "../spw/commands/extended-menu"; import {forEachNode, pushNode} from "../../simulation/nodes/data/operate"; -import {clearActiveNodes} from "../../init/hotkeys/handlers/clear-active-nodes"; -import {loadParameters} from "../../init/parameters/read"; +import {clearActiveNodes} from "../../ui/hotkeys/handlers/clear-active-nodes"; +import {loadParameters} from "../../bootstrap/parameters/read"; const clearFxFy = d => d.fx = d.fy = undefined; const fixX = (d, i) => d.fy = 75 * (i + 1); diff --git a/src/js/modes/spw/commands/save.js b/src/js/modes/spw/commands/save.js deleted file mode 100644 index 6d2dc66..0000000 --- a/src/js/modes/spw/commands/save.js +++ /dev/null @@ -1,5 +0,0 @@ -import {saveActiveNodes} from "../../../init/hotkeys/handlers/save-active-nodes"; - -export function runSaveCommand() { - saveActiveNodes(); -} \ No newline at end of file diff --git a/src/js/modes/stream/bane.js b/src/js/modes/stream/bane.js new file mode 100644 index 0000000..0df1b54 --- /dev/null +++ b/src/js/modes/stream/bane.js @@ -0,0 +1,7 @@ +import { ConfigModeHandler } from './base.js'; +export class BaneModeHandler extends ConfigModeHandler { + constructor(container) { super(container, 'bane'); } + fallback() { + return `
Loading Bane...
`; + } +} diff --git a/src/js/modes/stream/base.js b/src/js/modes/stream/base.js new file mode 100644 index 0000000..bdc60d0 --- /dev/null +++ b/src/js/modes/stream/base.js @@ -0,0 +1,149 @@ +export class ModeHandler { + constructor(container) { + this.container = container; + } + + fallback() { + return `
Loading...
`; + } + + setupDataManager() { + if (!this.container.dataManager) return; + if (!this.container.dataManager.loaded) { + this.container.startLoading(); + Promise.resolve(this.container.dataManager.getAll()) + .then((items) => { + if (typeof this.renderItems === 'function') { + this.renderItems(items); + } + }) + .catch((err) => { + this.container.setLoadError(err); + }) + .finally(() => { + this.container.finishLoading(); + }); + } else { + Promise.resolve(this.container.dataManager.getAll()).then((items) => { + if (typeof this.renderItems === 'function') { + this.renderItems(items); + } + }); + } + if (typeof this.container.dataManager.onUpdate === 'function') { + this.updateCb = (item) => { + if (typeof this.renderItem === 'function') { + this.renderItem(item); + } + }; + this.container.dataManager.onUpdate(this.updateCb); + } + } + + render() { + return ''; + } + + setupEventListeners() { + this.setupDataManager(); + } + + handleStreamMessage(data) {} + + cleanup() { + if (this.updateCb && this.container.dataManager) { + this.container.dataManager.offUpdate(this.updateCb); + } + const src = this.container.dataManager && this.container.dataManager.source; + if (src && typeof src.close === 'function') { + src.close(); + } + } +} + +export class ConfigModeHandler extends ModeHandler { + constructor(container, mode) { + super(container); + this.mode = mode; + this.storageKey = `${mode}-settings`; + } + + getDefaults() { + return { name: '', description: '', intensity: 5, active: false, visible: true }; + } + + loadSettings() { + try { + return JSON.parse(localStorage.getItem(this.storageKey)) || this.getDefaults(); + } catch { + return this.getDefaults(); + } + } + + render() { + const vals = this.loadSettings(); + return ` +
+

${this.mode.charAt(0).toUpperCase() + this.mode.slice(1)} Settings

+ + + + + +
+ + +
+
+ `; + } + + setupEventListeners() { + this.form = this.container.shadow.querySelector(`form[data-mode="${this.mode}"]`); + if (!this.form) return; + this.nameField = this.form.querySelector('.mode-name'); + this.descField = this.form.querySelector('.mode-description'); + this.intensityField = this.form.querySelector('.mode-intensity'); + this.activeField = this.form.querySelector('.mode-active'); + this.visibleField = this.form.querySelector('.mode-visible'); + this.saveBtn = this.form.querySelector('.save-settings'); + this.sendBtn = this.form.querySelector('.send-settings'); + this.saveBound = this.saveSettings.bind(this); + this.sendBound = this.sendSettings.bind(this); + this.saveBtn.addEventListener('click', this.saveBound); + this.sendBtn.addEventListener('click', this.sendBound); + super.setupEventListeners(); + } + + getSettings() { + return { + name: this.nameField.value.trim(), + description: this.descField.value.trim(), + intensity: parseInt(this.intensityField.value, 10), + active: this.activeField.checked, + visible: this.visibleField.checked, + }; + } + + saveSettings() { + const data = this.getSettings(); + localStorage.setItem(this.storageKey, JSON.stringify(data)); + this.container.displayMessage(`${this.mode} settings saved locally`, 'success'); + } + + sendSettings() { + const data = this.getSettings(); + if (this.container.stream && this.container.stream.readyState === WebSocket.OPEN) { + this.container.stream.send(JSON.stringify({ type: `${this.mode}-settings`, data })); + this.container.displayMessage(`${this.mode} settings sent`, 'info'); + } else { + this.container.displayMessage('WebSocket not connected. Settings stored locally.', 'warning'); + } + } + + cleanup() { + if (this.saveBtn) this.saveBtn.removeEventListener('click', this.saveBound); + if (this.sendBtn) this.sendBtn.removeEventListener('click', this.sendBound); + super.cleanup(); + } +} diff --git a/src/js/modes/stream/bone.js b/src/js/modes/stream/bone.js new file mode 100644 index 0000000..92a4089 --- /dev/null +++ b/src/js/modes/stream/bone.js @@ -0,0 +1,7 @@ +import { ConfigModeHandler } from './base.js'; +export class BoneModeHandler extends ConfigModeHandler { + constructor(container) { super(container, 'bone'); } + fallback() { + return `
Loading Bone...
`; + } +} diff --git a/src/js/modes/stream/bonk.js b/src/js/modes/stream/bonk.js new file mode 100644 index 0000000..5a9285b --- /dev/null +++ b/src/js/modes/stream/bonk.js @@ -0,0 +1,7 @@ +import { ConfigModeHandler } from './base.js'; +export class BonkModeHandler extends ConfigModeHandler { + constructor(container) { super(container, 'bonk'); } + fallback() { + return `
Loading Bonk...
`; + } +} diff --git a/src/js/modes/stream/boof.js b/src/js/modes/stream/boof.js new file mode 100644 index 0000000..766a946 --- /dev/null +++ b/src/js/modes/stream/boof.js @@ -0,0 +1,58 @@ +import { ModeHandler } from './base.js'; + +export class BoofModeHandler extends ModeHandler { + fallback() { + return `
Loading Boof...
`; + } + render() { + return ` +
+ + + +
+ `; + } + + setupEventListeners() { + this.boofInputField = this.container.shadow.querySelector('.boof-input'); + this.boofButton = this.container.shadow.querySelector('.boof-submit'); + this.sendBoofMessageBound = this.sendBoofMessage.bind(this); + this.boofButton.addEventListener('click', this.sendBoofMessageBound); + super.setupEventListeners(); + } + + handleStreamMessage(data) { + if (data.content) { + this.container.displayBoof(data); + this.container.dispatchEvent(new CustomEvent('boof-received', { detail: data })); + } + } + + sendBoofMessage() { + const message = this.boofInputField.value.trim(); + if (message) { + this.container.stream.send(message); + this.boofInputField.value = ''; + this.container.displayMessage('Boof message sent', 'success'); + this.container.dispatchEvent(new CustomEvent('boof-sent', { detail: { content: message } })); + } else { + this.container.displayMessage('Cannot send an empty Boof message', 'warning'); + } + } + + cleanup() { + if (this.boofButton) { + this.boofButton.removeEventListener('click', this.sendBoofMessageBound); + } + super.cleanup(); + } + + renderItems(items) { + items.forEach((item) => this.renderItem(item)); + } + + renderItem(item) { + this.container.displayBoof(item); + } +} diff --git a/src/js/modes/stream/boon.js b/src/js/modes/stream/boon.js new file mode 100644 index 0000000..23f5178 --- /dev/null +++ b/src/js/modes/stream/boon.js @@ -0,0 +1,125 @@ +import { ModeHandler } from './base.js'; + +export class BoonModeHandler extends ModeHandler { + fallback() { + return `
Loading Boons...
`; + } + render() { + const defaultValues = { + name: 'Default Boon Name', + x: 0, + y: 0, + z: 0, + description: 'Default description for the boon', + level: 1, + isActive: true, + }; + + return ` +
+

Create Boon

+
+
+ Basic Information + + +
+
+ Position + + + + + + +
+
+ Boon Details + + + + + +
+
+ +
+ `; + } + + setupEventListeners() { + const shadow = this.container.shadow; + this.boonNameField = shadow.querySelector('.boon-name'); + this.boonXField = shadow.querySelector('.boon-x'); + this.boonYField = shadow.querySelector('.boon-y'); + this.boonZField = shadow.querySelector('.boon-z'); + this.boonDescriptionField = shadow.querySelector('.boon-description'); + this.boonLevelField = shadow.querySelector('.boon-level'); + this.boonIsActiveField = shadow.querySelector('.boon-is-active'); + this.boonButton = shadow.querySelector('.boon-submit'); + + this.sendBoonMessageBound = this.sendBoonMessage.bind(this); + this.boonButton.addEventListener('click', this.sendBoonMessageBound); + + super.setupEventListeners(); + } + + handleStreamMessage(data) { + if (data.id && data.name && data.boonhonk) { + this.container.processBoon(data); + this.container.dispatchEvent(new CustomEvent('boon-received', { detail: data })); + } + } + + sendBoonMessage() { + const boon = { + id: Date.now(), + name: this.boonNameField.value.trim(), + x: parseFloat(this.boonXField.value), + y: parseFloat(this.boonYField.value), + z: parseFloat(this.boonZField.value), + boonhonk: { + description: this.boonDescriptionField.value.trim(), + level: parseInt(this.boonLevelField.value, 10), + is_active: this.boonIsActiveField.checked, + }, + image_id: null, + }; + + if (!boon.name || isNaN(boon.x) || isNaN(boon.y) || isNaN(boon.z) || !boon.boonhonk.description || isNaN(boon.boonhonk.level)) { + this.container.displayMessage('Please fill in all Boon fields correctly.', 'warning'); + return; + } + + this.container.stream.send(JSON.stringify(boon)); + + this.boonNameField.value = ''; + this.boonXField.value = ''; + this.boonYField.value = ''; + this.boonZField.value = ''; + this.boonDescriptionField.value = ''; + this.boonLevelField.value = ''; + this.boonIsActiveField.checked = false; + + this.container.displayMessage('Boon message sent', 'success'); + this.container.dispatchEvent(new CustomEvent('boon-sent', { detail: boon })); + } + + cleanup() { + if (this.boonButton) { + this.boonButton.removeEventListener('click', this.sendBoonMessageBound); + } + super.cleanup(); + } + + renderItems(items) { + items.forEach((item) => this.renderItem(item)); + } + + renderItem(item) { + this.container.processBoon(item); + } +} diff --git a/src/js/modes/stream/focal.js b/src/js/modes/stream/focal.js new file mode 100644 index 0000000..a5de0c2 --- /dev/null +++ b/src/js/modes/stream/focal.js @@ -0,0 +1,65 @@ +import { ModeHandler } from './base.js'; +import { focalPoint, initFocalSquare } from '../focal-point.js'; + +export class FocalModeHandler extends ModeHandler { + fallback() { + return `
Loading Focal...
`; + } + render() { + return ` + + `; + } + + setupEventListeners() { + this.focalButton = this.container.shadow.querySelector('.focal-submit'); + this.setFocalPointBound = this.setFocalPoint.bind(this); + this.focalButton.addEventListener('click', this.setFocalPointBound); + initFocalSquare(); + super.setupEventListeners(); + } + + handleStreamMessage(data) { + if (data.type === 'focal-point') { + this.container.displayMessage(`Focal point updated: ${JSON.stringify(data.position)}`); + this.container.dispatchEvent(new CustomEvent('focal-point-received', { detail: data })); + } + } + + setFocalPoint() { + const bounds = { + x1: focalPoint.x - 50, + y1: focalPoint.y - 50, + x2: focalPoint.x + 50, + y2: focalPoint.y + 50, + }; + + const focalData = { + type: 'focal-point', + position: { x: focalPoint.x, y: focalPoint.y }, + bounds, + }; + + this.container.stream.send(JSON.stringify(focalData)); + this.container.displayMessage( + `Focal point sent with position (${focalData.position.x}, ${focalData.position.y}) and bounds (${bounds.x1}, ${bounds.y1}, ${bounds.x2}, ${bounds.y2})`, + 'success' + ); + this.container.dispatchEvent(new CustomEvent('focal-point-sent', { detail: focalData })); + } + + cleanup() { + if (this.focalButton) { + this.focalButton.removeEventListener('click', this.setFocalPointBound); + } + super.cleanup(); + } + + renderItems(items) { + items.forEach((item) => this.renderItem(item)); + } + + renderItem(item) { + this.container.displayMessage(`[focal] ${JSON.stringify(item)}`); + } +} diff --git a/src/js/modes/stream/honk.js b/src/js/modes/stream/honk.js new file mode 100644 index 0000000..366ac9b --- /dev/null +++ b/src/js/modes/stream/honk.js @@ -0,0 +1,7 @@ +import { ConfigModeHandler } from './base.js'; +export class HonkModeHandler extends ConfigModeHandler { + constructor(container) { super(container, 'honk'); } + fallback() { + return `
Loading Honk...
`; + } +} diff --git a/src/js/modes/stream/lore.js b/src/js/modes/stream/lore.js new file mode 100644 index 0000000..1374da9 --- /dev/null +++ b/src/js/modes/stream/lore.js @@ -0,0 +1,7 @@ +import { ConfigModeHandler } from './base.js'; +export class LoreModeHandler extends ConfigModeHandler { + constructor(container) { super(container, 'lore'); } + fallback() { + return `
Loading Lore...
`; + } +} diff --git a/src/js/modes/stream/passive.js b/src/js/modes/stream/passive.js new file mode 100644 index 0000000..1bc7e49 --- /dev/null +++ b/src/js/modes/stream/passive.js @@ -0,0 +1,18 @@ +import { ModeHandler } from './base.js'; + +export class PassiveModeHandler extends ModeHandler { + fallback() { + return `
Loading Passive...
`; + } + render() { + return `
Passive mode active. Waiting for updates…
`; + } + + renderItems(items) { + items.forEach((item) => this.renderItem(item)); + } + + renderItem(item) { + this.container.displayMessage(`[passive] ${JSON.stringify(item)}`); + } +} diff --git a/src/js/services/data-manager.js b/src/js/services/data-manager.js new file mode 100644 index 0000000..67e49e1 --- /dev/null +++ b/src/js/services/data-manager.js @@ -0,0 +1,53 @@ +// services/data-manager.js + +import { StaticDataSource, LiveDataSource } from './data-sources.js'; + +export class DataManager { + constructor({ mode, strategy = 'static', staticItems = [], wsUrl = '' }) { + if (strategy === 'live') { + this.source = new LiveDataSource(wsUrl); + } else { + this.source = new StaticDataSource(staticItems); + } + this.mode = mode; + } + + getAll() { + try { + const res = this.source.fetchAll(); + return res instanceof Promise ? res : Promise.resolve(res); + } catch (err) { + this.source.error = err; + this.source.loading = false; + return Promise.reject(err); + } + } + + getOne(id) { + return this.source.fetchOne(id); + } + + onUpdate(cb) { + if (this.source.subscribeUpdates) { + this.source.subscribeUpdates(cb); + } + } + + offUpdate(cb) { + if (this.source.unsubscribeUpdates) { + this.source.unsubscribeUpdates(cb); + } + } + + get loading() { + return this.source.loading; + } + + get error() { + return this.source.error; + } + + get loaded() { + return this.source.loaded; + } +} diff --git a/src/js/services/data-sources.js b/src/js/services/data-sources.js new file mode 100644 index 0000000..81185aa --- /dev/null +++ b/src/js/services/data-sources.js @@ -0,0 +1,110 @@ +// services/data-sources.js + +/** + * A uniform interface for all data sources. + * Methods may return a value or a Promise for that value. + */ +export class DataSource { + constructor() { + /** @type {boolean} */ + this.loading = true; + /** @type {?Error} */ + this.error = null; + /** @type {boolean} */ + this.loaded = false; + } + /** Fetch all items: may be sync or async */ + fetchAll() { throw new Error('fetchAll not implemented'); } + /** Fetch a single item by ID or index */ + fetchOne(id) { throw new Error('fetchOne not implemented'); } + /** Subscribe to live updates: callback(item) */ + subscribeUpdates(callback) {} + /** Unsubscribe from updates */ + unsubscribeUpdates(callback) {} +} + +/** Static (hardcoded) Source */ +export class StaticDataSource extends DataSource { + constructor(items = []) { + super(); + this.items = items; + } + fetchAll() { + this.loading = true; + try { + const result = this.items; + this.loading = false; + this.loaded = true; + return result; + } catch (err) { + this.error = err; + this.loading = false; + throw err; + } + } + fetchOne(id) { + return this.items.find(item => item.id === id); + } +} + +/** Live (async/WebSocket) Source */ +export class LiveDataSource extends DataSource { + constructor(wsUrl) { + super(); + this.ws = new WebSocket(wsUrl); + this.queue = []; + this.callbacks = new Set(); + + this.ws.onmessage = (event) => { + const item = JSON.parse(event.data); + this.queue.push(item); + this.callbacks.forEach(cb => cb(item)); + }; + } + + async fetchAll() { + this.loading = true; + return new Promise((resolve, reject) => { + this.ws.onopen = () => { + this.ws.send(JSON.stringify({ type: 'get-all' })); + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.type === 'all-items') { + this.loading = false; + this.loaded = true; + resolve(data.items); + } + }; + this.ws.onerror = (err) => { + this.error = err; + this.loading = false; + reject(err); + }; + }; + }); + } + + fetchOne(id) { + // For demo purposes we rely on fetchAll and filter + return this.fetchAll().then(items => items.find(item => item.id === id)); + } + + subscribeUpdates(cb) { + this.callbacks.add(cb); + } + + unsubscribeUpdates(cb) { + this.callbacks.delete(cb); + } + + close() { + if (this.ws && this.ws.readyState !== WebSocket.CLOSED) { + try { + this.ws.close(); + } catch (err) { + console.warn('Failed to close WebSocket', err); + } + } + this.callbacks.clear(); + } +} diff --git a/src/js/simulation/nodes/data/normalize.js b/src/js/simulation/nodes/data/normalize.js index 0a9eca6..9c4c7af 100644 --- a/src/js/simulation/nodes/data/normalize.js +++ b/src/js/simulation/nodes/data/normalize.js @@ -1,4 +1,4 @@ -import { getDocumentDataIndex } from "../../../modes/dataindex/util"; +import { getDocumentDataIndex } from "../../../modes/input/dataindex/util"; /** * @typedef {Object} Node diff --git a/src/js/simulation/nodes/data/process.js b/src/js/simulation/nodes/data/process.js index 7d6e0f8..9936a01 100644 --- a/src/js/simulation/nodes/data/process.js +++ b/src/js/simulation/nodes/data/process.js @@ -3,7 +3,7 @@ import md5 from "md5"; window.md5 = md5; import {getNextUrlSearchParams} from "../../../util/next-url"; -import {getDocumentDataIndex} from "../../../modes/dataindex/util"; +import {getDocumentDataIndex} from "../../../modes/input/dataindex/util"; function getLastKind(node) { return node.kind?.trim().split(' + ').reverse()[0]; diff --git a/src/js/simulation/nodes/ui/circle.js b/src/js/simulation/nodes/ui/circle.js index 35f0897..da2485b 100644 --- a/src/js/simulation/nodes/ui/circle.js +++ b/src/js/simulation/nodes/ui/circle.js @@ -2,7 +2,7 @@ import { cacheNode } from "../data/store"; import { drag } from "d3"; -import { CLICKED_NODES } from "../../../modes/spw/commands/clicked"; +import { CLICKED_NODES } from "../../../modes/input/spw/commands/clicked"; import { removeNodeEdges } from "../../edges/data/set"; import { sortNodes } from "../data/sort"; import { getAllNodes } from "../data/selectors/multiple"; diff --git a/src/js/ui/enable-sounds-button.js b/src/js/ui/components/enable-sounds-button.js similarity index 94% rename from src/js/ui/enable-sounds-button.js rename to src/js/ui/components/enable-sounds-button.js index b2a9ce2..e09a0bc 100644 --- a/src/js/ui/enable-sounds-button.js +++ b/src/js/ui/components/enable-sounds-button.js @@ -1,4 +1,4 @@ -import {runDisableSoundsCommand, runEnableSoundsCommand} from "../modes/spw/commands/sounds"; +import {runDisableSoundsCommand, runEnableSoundsCommand} from "../../modes/input/spw/commands/sounds"; export function initEnableSoundsButton() { const enableSoundsButton = document.querySelector('.enable-sounds'); diff --git a/src/js/ui/focal-point.js b/src/js/ui/components/focal-point.js similarity index 98% rename from src/js/ui/focal-point.js rename to src/js/ui/components/focal-point.js index 51b0166..c50f2c1 100644 --- a/src/js/ui/focal-point.js +++ b/src/js/ui/components/focal-point.js @@ -1,4 +1,4 @@ -import { setDocumentMode } from "../modes"; +import { setDocumentMode } from "../../modes/input"; let focalPointElement; diff --git a/src/js/ui/h1.js b/src/js/ui/components/h1.js similarity index 95% rename from src/js/ui/h1.js rename to src/js/ui/components/h1.js index c53abcc..9911437 100644 --- a/src/js/ui/h1.js +++ b/src/js/ui/components/h1.js @@ -1,9 +1,9 @@ -import {processSpwInput} from "../modes/spw/process-spw-input"; -import {setDocumentMode} from "../modes"; +import {processSpwInput} from "../../modes/input/spw/process-spw-input"; +import {setDocumentMode} from "../../modes/input"; import {parse} from "../vendor/spw/parser/parse.mjs"; import {getIdentityPath} from "../simulation/nodes/data/process"; import md5 from "md5"; -import {processPastedText} from "../init/hotkeys/handlers/pasted-text"; +import {processPastedText} from "../ui/hotkeys/handlers/pasted-text"; import {getNextUrlSearchParams} from "../util/next-url"; function getHeaderSideEffects(input, sideEffects = {}) { diff --git a/src/js/ui/hotkey-buttons.js b/src/js/ui/components/hotkey-buttons.js similarity index 94% rename from src/js/ui/hotkey-buttons.js rename to src/js/ui/components/hotkey-buttons.js index b4c55d1..ba23b43 100644 --- a/src/js/ui/hotkey-buttons.js +++ b/src/js/ui/components/hotkey-buttons.js @@ -1,4 +1,4 @@ -import {toggleHotkeyMenu} from "../init/hotkeys/handlers/toggle-hotkey-menu"; +import {toggleHotkeyMenu} from "../ui/hotkeys/handlers/toggle-hotkey-menu"; export function initHotkeyButtons() { const keystrokeOptions = document.querySelector('#keystroke-options'); diff --git a/src/js/ui/main-menu-toggle.js b/src/js/ui/components/main-menu-toggle.js similarity index 100% rename from src/js/ui/main-menu-toggle.js rename to src/js/ui/components/main-menu-toggle.js diff --git a/src/js/ui/page-image.js b/src/js/ui/components/page-image.js similarity index 100% rename from src/js/ui/page-image.js rename to src/js/ui/components/page-image.js diff --git a/src/js/ui/simulation-controls.js b/src/js/ui/components/simulation-controls.js similarity index 100% rename from src/js/ui/simulation-controls.js rename to src/js/ui/components/simulation-controls.js diff --git a/src/js/ui/components/stream-config.js b/src/js/ui/components/stream-config.js new file mode 100644 index 0000000..53ede4a --- /dev/null +++ b/src/js/ui/components/stream-config.js @@ -0,0 +1,24 @@ +export function initStreamConfig() { + const container = document.querySelector('#debug-mode-container'); + if (!container) return; + + const fieldset = document.createElement('fieldset'); + fieldset.innerHTML = ` + Stream Config + + + `; + container.appendChild(fieldset); + + const radios = fieldset.querySelectorAll('input[name="stream-strategy"]'); + radios.forEach(radio => { + radio.addEventListener('change', () => { + if (radio.checked) { + window.spwashi.streamStrategy = radio.value; + document.dispatchEvent(new CustomEvent('stream-strategy-change', { detail: { strategy: radio.value } })); + } + }); + }); + + window.spwashi.streamStrategy = 'static'; +} diff --git a/src/js/ui/components/stream-container.js b/src/js/ui/components/stream-container.js new file mode 100644 index 0000000..877e2d3 --- /dev/null +++ b/src/js/ui/components/stream-container.js @@ -0,0 +1,439 @@ +// stream-container.js + +import { NODE_MANAGER } from '../../simulation/nodes/nodes'; +import { DataManager } from '../../services/data-manager.js'; +import { MODE_DOCS } from '../config/mode-docs.js'; +import { BoofModeHandler } from '../../modes/stream/boof.js'; +import { BoonModeHandler } from '../../modes/stream/boon.js'; +import { BaneModeHandler } from '../../modes/stream/bane.js'; +import { BoneModeHandler } from '../../modes/stream/bone.js'; +import { BonkModeHandler } from '../../modes/stream/bonk.js'; +import { HonkModeHandler } from '../../modes/stream/honk.js'; +import { LoreModeHandler } from '../../modes/stream/lore.js'; +import { FocalModeHandler } from '../../modes/stream/focal.js'; +import { PassiveModeHandler } from '../../modes/stream/passive.js'; + +const BOON_ITEMS = []; +const BANE_ITEMS = []; +const BONE_ITEMS = []; +const BONK_ITEMS = []; +const HONK_ITEMS = []; +const BOOF_ITEMS = []; +const LORE_ITEMS = []; +const FOCAL_ITEMS = []; +const PASSIVE_ITEMS = []; + +const modeConfig = { + boon: { strategy: 'static', staticItems: BOON_ITEMS, allowedStreams: ['static', 'live'] }, + bane: { strategy: 'static', staticItems: BANE_ITEMS, allowedStreams: ['static'] }, + bone: { strategy: 'static', staticItems: BONE_ITEMS, allowedStreams: ['static'] }, + bonk: { strategy: 'static', staticItems: BONK_ITEMS, allowedStreams: ['static'] }, + honk: { strategy: 'static', staticItems: HONK_ITEMS, allowedStreams: ['static'] }, + boof: { strategy: 'static', staticItems: BOOF_ITEMS, allowedStreams: ['static', 'live'] }, + lore: { strategy: 'static', staticItems: LORE_ITEMS, allowedStreams: ['static'] }, + focal: { strategy: 'static', staticItems: FOCAL_ITEMS, allowedStreams: ['static', 'live'] }, + passive: { strategy: 'static', staticItems: PASSIVE_ITEMS, allowedStreams: ['static', 'live'] }, +}; +/** + * Main stream container class. + */ +class SpwashiStreamContainer extends HTMLElement { + constructor() { + super(); + this.currentMode = 'boon'; + this.loading = true; + this.error = null; + this.initDataManager(); + this.modeHandlers = { + boof: BoofModeHandler, + boon: BoonModeHandler, + bane: BaneModeHandler, + bone: BoneModeHandler, + bonk: BonkModeHandler, + honk: HonkModeHandler, + lore: LoreModeHandler, + focal: FocalModeHandler, + passive: PassiveModeHandler, + }; + /** + * Initializes the stream container and its event listeners. + */ + setupStreamContainer() { + this.shadow = this.attachShadow({ mode: 'open' }); + this.render(); // Render the initial UI + + const room = document.getElementById("title-md5").innerText; + console.log("ancient knowledge being used ... title-md5"); + try { + this.stream = new WebSocket(`ws://${window.location.host}/ws/${room}`); + this.setupStream(); + } catch (e) { + console.warn("WebSocket unavailable", e); + this.stream = { readyState: -1, send: () => {} }; + } + } + /** + * Renders the UI based on the current mode. + */ + render() { + const template = document.createElement('template'); + template.innerHTML = this.getTemplateHtml(); + // Clear the shadow DOM and append the new content + this.shadow.innerHTML = ''; + this.shadow.appendChild(template.content.cloneNode(true)); + + // Always get reference to message container + this.messageContainer = this.shadow.querySelector('.response-wrapper'); + + // Get references to mode selection elements + this.modeSelector = this.shadow.querySelector('.mode-selector select'); + this.modeSelector.value = this.currentMode; + + // Cleanup previous mode handler + if (this.currentModeHandler) { + this.currentModeHandler.cleanup(); + } + + // Initialize the current mode handler + const ModeHandlerClass = this.modeHandlers[this.currentMode]; + if (ModeHandlerClass) { + this.currentModeHandler = new ModeHandlerClass(this); + this.currentModeHandler.setupEventListeners(); + } else { + this.currentModeHandler = null; + } + + // Setup event listeners + this.setupEventListeners(); + } + + /** + * Returns the HTML template for the component. + */ + getTemplateHtml() { + const modes = ['boon', 'bane', 'bone', 'bonk', 'honk', 'boof', 'lore', 'focal', 'passive']; + const optionsHtml = modes + .map( + (mode) => + `` + ) + .join(''); + + const docInfo = MODE_DOCS[this.currentMode] || {}; + const docsLink = docInfo.url || docInfo.file || ''; + + let modeSpecificHtml = ''; + const Handler = this.modeHandlers[this.currentMode]; + if (Handler) { + const proto = Handler.prototype; + if (this.loading) { + if (typeof proto.fallback === 'function') { + modeSpecificHtml = proto.fallback(); + } + } else if (this.error) { + modeSpecificHtml = `
Failed to load data.
`; + } else { + modeSpecificHtml = proto.render(); + } + } + + return ` + +
+ + ${docsLink ? `API Docs` : ''} +
+
+ ${modeSpecificHtml} + `; + } + + /** + * Sets up stream event listeners for message handling and error reporting. + */ + setupStream() { + this.stream.onmessage = (event) => this.handleStreamMessage(event); + this.stream.onopen = () => { + this.displayMessage('Stream connection opened', 'success'); + this.dispatchEvent(new Event('stream-open')); + }; + this.stream.onclose = () => { + this.displayMessage('Stream connection closed', 'warning'); + this.dispatchEvent(new Event('stream-close')); + }; + this.stream.onerror = () => { + this.displayMessage('Stream error', 'error'); + this.dispatchEvent(new Event('stream-error')); + }; + } + + initDataManager() { + const cfg = modeConfig[this.currentMode] || modeConfig.boon; + const strategy = + (cfg.allowedStreams || [cfg.strategy]).includes(window.spwashi.streamStrategy) + ? window.spwashi.streamStrategy + : cfg.strategy; + this.dataManager = new DataManager({ mode: this.currentMode, ...cfg, strategy }); + this.loading = true; + this.error = null; + } + + /** + * Sets up event listeners for user input interactions. + */ + setupEventListeners() { + // Remove any existing event listeners to prevent duplicates + if (this.modeSelector) { + this.modeSelector.removeEventListener('change', this.handleModeChangeBound); + } + + if (this.streamStrategyChangeBound) { + document.removeEventListener('stream-strategy-change', this.streamStrategyChangeBound); + } + + // Mode selector event listener + this.handleModeChangeBound = this.handleModeChange.bind(this); + this.modeSelector.addEventListener('change', this.handleModeChangeBound); + + this.streamStrategyChangeBound = this.handleStreamStrategyChange.bind(this); + document.addEventListener('stream-strategy-change', this.streamStrategyChangeBound); + } + + /** + * Handles the mode change event. + */ + handleModeChange() { + if (this.currentModeHandler) { + this.currentModeHandler.cleanup(); + this.currentModeHandler = null; + } + if (this.dataManager && this.dataManager.source && typeof this.dataManager.source.close === 'function') { + this.dataManager.source.close(); + } + this.currentMode = this.modeSelector.value; + this.initDataManager(); + this.render(); // Re-render the UI based on the new mode + // Emit custom event + this.dispatchEvent(new CustomEvent('mode-changed', { detail: { mode: this.currentMode } })); + } + + handleStreamStrategyChange() { + if (this.currentModeHandler) { + this.currentModeHandler.cleanup(); + this.currentModeHandler = null; + } + if (this.dataManager && this.dataManager.source && typeof this.dataManager.source.close === 'function') { + this.dataManager.source.close(); + } + this.initDataManager(); + this.render(); + } + + /** + * Handles incoming stream messages, delegating to the current mode handler. + * + * @param {MessageEvent} event - The stream message event. + */ + handleStreamMessage(event) { + try { + const data = JSON.parse(event.data); + + if (this.currentModeHandler && typeof this.currentModeHandler.handleStreamMessage === 'function') { + this.currentModeHandler.handleStreamMessage(data); + } else { + // Default handling for messages not processed by mode handlers + if (data.image_id) { + this.displayMessage(`Received image with ID: ${data.image_id}`); + } else { + this.displayMessage(`Received message: ${event.data}`, 'info'); + } + } + } catch (error) { + console.error('Error processing stream message:', error); + this.displayMessage('Failed to process stream data.', 'error'); + } + } + + /** + * Processes a Boon object received from the server. + * + * @param {Object} boon - The Boon object to process. + */ + processBoon(boon) { + // Process the Boon object, e.g., add it to the NODE_MANAGER + const node = { + ...boon, + fx: boon.x, + fy: boon.y, + id: `${boon.id}`, // Ensure id is a string + callbacks: { + click: (e, d) => { + console.debug('Node clicked:', d); + }, + }, + }; + NODE_MANAGER.initNodes([node], true); + window.spwashi.reinit(); + + // Display the Boon in the message container + const boonElement = document.createElement('pre'); + boonElement.textContent = `Boon: ${boon.name}, Position: (${boon.x}, ${boon.y}, ${boon.z}), Description: ${boon.boonhonk.description}, Level: ${boon.boonhonk.level}, Active: ${boon.boonhonk.is_active}`; + this.messageContainer.appendChild(boonElement); + this.messageContainer.scrollTop = this.messageContainer.scrollHeight; + + // Emit custom event + this.dispatchEvent(new CustomEvent('boon-processed', { detail: boon })); + } + + /** + * Displays a Boof message in the message container. + * + * @param {Object} boof - The Boof message object. + */ + displayBoof(boof) { + const boofElement = document.createElement('div'); + boofElement.textContent = `Boof: ${boof.content}`; + boofElement.classList.add('boof-message'); + this.messageContainer.appendChild(boofElement); + this.messageContainer.scrollTop = this.messageContainer.scrollHeight; + } + + /** + * Displays a message in the message container. + * + * @param {string} message - The message to display. + * @param {string} [type] - Optional message type for styling (e.g., 'info', 'success', 'warning', 'error'). + */ + displayMessage(message, type = 'info') { + const messageElement = document.createElement('div'); + messageElement.textContent = message; + messageElement.classList.add('message', type); + this.messageContainer.appendChild(messageElement); + // Scroll to the bottom to show the latest message + this.messageContainer.scrollTop = this.messageContainer.scrollHeight; + } + + startLoading() { + this.loading = true; + } + + finishLoading() { + this.loading = false; + this.render(); + } + + setLoadError(err) { + this.error = err; + this.loading = false; + console.error(err); + this.render(); + } +} + +/** + * Initializes and defines the custom stream container element. +*/ +export function initStreamContainer() { + if (!customElements.get('spwashi-stream-container')) { + customElements.define('spwashi-stream-container', SpwashiStreamContainer); + } + document + .querySelectorAll('spwashi-stream-container') + .forEach((el) => { + if (typeof el.setupStreamContainer === 'function') { + el.setupStreamContainer(); + } + }); +} diff --git a/src/js/init/hotkeys/_.js b/src/js/ui/hotkeys/_.js similarity index 94% rename from src/js/init/hotkeys/_.js rename to src/js/ui/hotkeys/_.js index 4cbc501..7e8e8e3 100644 --- a/src/js/init/hotkeys/_.js +++ b/src/js/ui/hotkeys/_.js @@ -1,6 +1,6 @@ import {setDocumentMode} from "../../modes"; -import {initMainMenuToggle} from "../../ui/main-menu-toggle"; -import {initHotkeyButtons} from "../../ui/hotkey-buttons"; +import {initMainMenuToggle} from "../../ui/components/main-menu-toggle"; +import {initHotkeyButtons} from "../../ui/components/hotkey-buttons"; import {processPastedText} from "./handlers/pasted-text"; function initKeystrokeHandlers(options) { diff --git a/src/js/init/hotkeys/handlers/bonk-velocity-decay.js b/src/js/ui/hotkeys/handlers/bonk-velocity-decay.js similarity index 100% rename from src/js/init/hotkeys/handlers/bonk-velocity-decay.js rename to src/js/ui/hotkeys/handlers/bonk-velocity-decay.js diff --git a/src/js/init/hotkeys/handlers/clear-active-nodes.js b/src/js/ui/hotkeys/handlers/clear-active-nodes.js similarity index 100% rename from src/js/init/hotkeys/handlers/clear-active-nodes.js rename to src/js/ui/hotkeys/handlers/clear-active-nodes.js diff --git a/src/js/init/hotkeys/handlers/clear-cached-nodes.js b/src/js/ui/hotkeys/handlers/clear-cached-nodes.js similarity index 100% rename from src/js/init/hotkeys/handlers/clear-cached-nodes.js rename to src/js/ui/hotkeys/handlers/clear-cached-nodes.js diff --git a/src/js/init/hotkeys/handlers/clear-fixed-positions.js b/src/js/ui/hotkeys/handlers/clear-fixed-positions.js similarity index 100% rename from src/js/init/hotkeys/handlers/clear-fixed-positions.js rename to src/js/ui/hotkeys/handlers/clear-fixed-positions.js diff --git a/src/js/init/hotkeys/handlers/fix-positions.js b/src/js/ui/hotkeys/handlers/fix-positions.js similarity index 100% rename from src/js/init/hotkeys/handlers/fix-positions.js rename to src/js/ui/hotkeys/handlers/fix-positions.js diff --git a/src/js/init/hotkeys/handlers/less-nodes.js b/src/js/ui/hotkeys/handlers/less-nodes.js similarity index 100% rename from src/js/init/hotkeys/handlers/less-nodes.js rename to src/js/ui/hotkeys/handlers/less-nodes.js diff --git a/src/js/init/hotkeys/handlers/more-nodes.js b/src/js/ui/hotkeys/handlers/more-nodes.js similarity index 100% rename from src/js/init/hotkeys/handlers/more-nodes.js rename to src/js/ui/hotkeys/handlers/more-nodes.js diff --git a/src/js/init/hotkeys/handlers/pasted-text.js b/src/js/ui/hotkeys/handlers/pasted-text.js similarity index 100% rename from src/js/init/hotkeys/handlers/pasted-text.js rename to src/js/ui/hotkeys/handlers/pasted-text.js diff --git a/src/js/init/hotkeys/handlers/reset-interface.js b/src/js/ui/hotkeys/handlers/reset-interface.js similarity index 100% rename from src/js/init/hotkeys/handlers/reset-interface.js rename to src/js/ui/hotkeys/handlers/reset-interface.js diff --git a/src/js/init/hotkeys/handlers/save-active-nodes.js b/src/js/ui/hotkeys/handlers/save-active-nodes.js similarity index 100% rename from src/js/init/hotkeys/handlers/save-active-nodes.js rename to src/js/ui/hotkeys/handlers/save-active-nodes.js diff --git a/src/js/init/hotkeys/handlers/toggle-focal-point.js b/src/js/ui/hotkeys/handlers/toggle-focal-point.js similarity index 100% rename from src/js/init/hotkeys/handlers/toggle-focal-point.js rename to src/js/ui/hotkeys/handlers/toggle-focal-point.js diff --git a/src/js/init/hotkeys/handlers/toggle-hotkey-menu.js b/src/js/ui/hotkeys/handlers/toggle-hotkey-menu.js similarity index 100% rename from src/js/init/hotkeys/handlers/toggle-hotkey-menu.js rename to src/js/ui/hotkeys/handlers/toggle-hotkey-menu.js diff --git a/src/js/init/hotkeys/handlers/toggle-main-menu.js b/src/js/ui/hotkeys/handlers/toggle-main-menu.js similarity index 100% rename from src/js/init/hotkeys/handlers/toggle-main-menu.js rename to src/js/ui/hotkeys/handlers/toggle-main-menu.js diff --git a/src/js/init/hotkeys/keystrokes.js b/src/js/ui/hotkeys/keystrokes.js similarity index 100% rename from src/js/init/hotkeys/keystrokes.js rename to src/js/ui/hotkeys/keystrokes.js diff --git a/src/stylesheets/aspects/shine.scss b/src/js/ui/styles/aspects/shine.scss similarity index 100% rename from src/stylesheets/aspects/shine.scss rename to src/js/ui/styles/aspects/shine.scss diff --git a/src/stylesheets/core/card.scss b/src/js/ui/styles/core/card.scss similarity index 100% rename from src/stylesheets/core/card.scss rename to src/js/ui/styles/core/card.scss diff --git a/src/stylesheets/core/core.scss b/src/js/ui/styles/core/core.scss similarity index 100% rename from src/stylesheets/core/core.scss rename to src/js/ui/styles/core/core.scss diff --git a/src/stylesheets/core/form.scss b/src/js/ui/styles/core/form.scss similarity index 100% rename from src/stylesheets/core/form.scss rename to src/js/ui/styles/core/form.scss diff --git a/src/stylesheets/core/main-aside.scss b/src/js/ui/styles/core/main-aside.scss similarity index 100% rename from src/stylesheets/core/main-aside.scss rename to src/js/ui/styles/core/main-aside.scss diff --git a/src/stylesheets/core/root.scss b/src/js/ui/styles/core/root.scss similarity index 100% rename from src/stylesheets/core/root.scss rename to src/js/ui/styles/core/root.scss diff --git a/src/stylesheets/core/svg.scss b/src/js/ui/styles/core/svg.scss similarity index 100% rename from src/stylesheets/core/svg.scss rename to src/js/ui/styles/core/svg.scss diff --git a/src/stylesheets/experience/experience.scss b/src/js/ui/styles/experience/experience.scss similarity index 100% rename from src/stylesheets/experience/experience.scss rename to src/js/ui/styles/experience/experience.scss diff --git a/src/stylesheets/experience/sounds-enabled.scss b/src/js/ui/styles/experience/sounds-enabled.scss similarity index 100% rename from src/stylesheets/experience/sounds-enabled.scss rename to src/js/ui/styles/experience/sounds-enabled.scss diff --git a/src/stylesheets/form-factors/style.css b/src/js/ui/styles/form-factors/style.css similarity index 100% rename from src/stylesheets/form-factors/style.css rename to src/js/ui/styles/form-factors/style.css diff --git a/src/stylesheets/interaction/consent.scss b/src/js/ui/styles/interaction/consent.scss similarity index 100% rename from src/stylesheets/interaction/consent.scss rename to src/js/ui/styles/interaction/consent.scss diff --git a/src/stylesheets/interaction/focal-square.scss b/src/js/ui/styles/interaction/focal-square.scss similarity index 100% rename from src/stylesheets/interaction/focal-square.scss rename to src/js/ui/styles/interaction/focal-square.scss diff --git a/src/stylesheets/interaction/interaction-depth.scss b/src/js/ui/styles/interaction/interaction-depth.scss similarity index 100% rename from src/stylesheets/interaction/interaction-depth.scss rename to src/js/ui/styles/interaction/interaction-depth.scss diff --git a/src/stylesheets/interaction/interaction.scss b/src/js/ui/styles/interaction/interaction.scss similarity index 70% rename from src/stylesheets/interaction/interaction.scss rename to src/js/ui/styles/interaction/interaction.scss index f892030..88fd0b1 100644 --- a/src/stylesheets/interaction/interaction.scss +++ b/src/js/ui/styles/interaction/interaction.scss @@ -1,4 +1,4 @@ @import "./consent"; @import "./focal-square"; @import "./interaction-depth"; -@import "./websocket-container"; +@import "./stream-container"; diff --git a/src/js/ui/styles/interaction/stream-container.scss b/src/js/ui/styles/interaction/stream-container.scss new file mode 100644 index 0000000..12ec8f6 --- /dev/null +++ b/src/js/ui/styles/interaction/stream-container.scss @@ -0,0 +1,28 @@ +#main-stream-container { + position: fixed; + bottom: 0; + right: 0; + background: #222; + color: var(--accent-color-main); + padding: 1rem; + margin: 1rem; + line-height: 1; + font-size: 2rem; + outline: thin solid var(--accent-color-main); +} +form.mode-form { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 1rem; +} +form.mode-form h3 { + margin: 0 0 0.5rem 0; +} +form.mode-form .button-group { + display: flex; + gap: 0.5rem; +} +form.mode-form button { + flex: 1 1 auto; +} diff --git a/src/stylesheets/main.scss b/src/js/ui/styles/main.scss similarity index 92% rename from src/stylesheets/main.scss rename to src/js/ui/styles/main.scss index 5ac6071..d24804c 100644 --- a/src/stylesheets/main.scss +++ b/src/js/ui/styles/main.scss @@ -2,7 +2,7 @@ @import "core/core"; @import "experience/experience"; @import "interaction/interaction"; -@import "modes/modes"; +@import "mod../input-modes"; /** widgets **/ @import "widgets/h1"; diff --git a/src/stylesheets/modes/color-mode.scss b/src/js/ui/styles/modes/color-mode.scss similarity index 100% rename from src/stylesheets/modes/color-mode.scss rename to src/js/ui/styles/modes/color-mode.scss diff --git a/src/stylesheets/modes/data-mode.scss b/src/js/ui/styles/modes/data-mode.scss similarity index 100% rename from src/stylesheets/modes/data-mode.scss rename to src/js/ui/styles/modes/data-mode.scss diff --git a/src/stylesheets/modes/debug-mode.scss b/src/js/ui/styles/modes/debug-mode.scss similarity index 100% rename from src/stylesheets/modes/debug-mode.scss rename to src/js/ui/styles/modes/debug-mode.scss diff --git a/src/stylesheets/modes/map-filter-mode.scss b/src/js/ui/styles/modes/map-filter-mode.scss similarity index 100% rename from src/stylesheets/modes/map-filter-mode.scss rename to src/js/ui/styles/modes/map-filter-mode.scss diff --git a/src/stylesheets/modes/modes.scss b/src/js/ui/styles/modes/modes.scss similarity index 100% rename from src/stylesheets/modes/modes.scss rename to src/js/ui/styles/modes/modes.scss diff --git a/src/stylesheets/modes/node-mode.scss b/src/js/ui/styles/modes/node-mode.scss similarity index 100% rename from src/stylesheets/modes/node-mode.scss rename to src/js/ui/styles/modes/node-mode.scss diff --git a/src/stylesheets/modes/query-mode.scss b/src/js/ui/styles/modes/query-mode.scss similarity index 100% rename from src/stylesheets/modes/query-mode.scss rename to src/js/ui/styles/modes/query-mode.scss diff --git a/src/stylesheets/modes/reflex-mode.scss b/src/js/ui/styles/modes/reflex-mode.scss similarity index 100% rename from src/stylesheets/modes/reflex-mode.scss rename to src/js/ui/styles/modes/reflex-mode.scss diff --git a/src/stylesheets/modes/spw-mode.scss b/src/js/ui/styles/modes/spw-mode.scss similarity index 100% rename from src/stylesheets/modes/spw-mode.scss rename to src/js/ui/styles/modes/spw-mode.scss diff --git a/src/stylesheets/modes/story-mode.scss b/src/js/ui/styles/modes/story-mode.scss similarity index 100% rename from src/stylesheets/modes/story-mode.scss rename to src/js/ui/styles/modes/story-mode.scss diff --git a/src/stylesheets/sites/sites.scss b/src/js/ui/styles/sites/sites.scss similarity index 100% rename from src/stylesheets/sites/sites.scss rename to src/js/ui/styles/sites/sites.scss diff --git a/src/js/ui/styles/vendor/ace-edit.scss b/src/js/ui/styles/vendor/ace-edit.scss new file mode 100644 index 0000000..580c42c --- /dev/null +++ b/src/js/ui/styles/vendor/ace-edit.scss @@ -0,0 +1,12 @@ +.code-editor { + width: 20rem; + height: 15rem; + margin-bottom: 1rem; + border: thin solid var(--standard-outline); + font-family: monospace; +} + +.code-editor.ace_editor { + background: var(--mainmenu-background); + color: var(--mainmenu-text-color); +} diff --git a/src/stylesheets/widgets/h1.scss b/src/js/ui/styles/widgets/h1.scss similarity index 100% rename from src/stylesheets/widgets/h1.scss rename to src/js/ui/styles/widgets/h1.scss diff --git a/src/stylesheets/widgets/hotkey-menu.scss b/src/js/ui/styles/widgets/hotkey-menu.scss similarity index 100% rename from src/stylesheets/widgets/hotkey-menu.scss rename to src/js/ui/styles/widgets/hotkey-menu.scss diff --git a/src/stylesheets/widgets/log.scss b/src/js/ui/styles/widgets/log.scss similarity index 100% rename from src/stylesheets/widgets/log.scss rename to src/js/ui/styles/widgets/log.scss diff --git a/src/stylesheets/widgets/page-image.scss b/src/js/ui/styles/widgets/page-image.scss similarity index 100% rename from src/stylesheets/widgets/page-image.scss rename to src/js/ui/styles/widgets/page-image.scss diff --git a/src/stylesheets/widgets/title-md5.scss b/src/js/ui/styles/widgets/title-md5.scss similarity index 100% rename from src/stylesheets/widgets/title-md5.scss rename to src/js/ui/styles/widgets/title-md5.scss diff --git a/src/stylesheets/widgets/wonder-button.scss b/src/js/ui/styles/widgets/wonder-button.scss similarity index 100% rename from src/stylesheets/widgets/wonder-button.scss rename to src/js/ui/styles/widgets/wonder-button.scss diff --git a/src/js/ui/websocket-container.js b/src/js/ui/websocket-container.js deleted file mode 100644 index 3e7fc84..0000000 --- a/src/js/ui/websocket-container.js +++ /dev/null @@ -1,565 +0,0 @@ -// websocket-container.js - -import { NODE_MANAGER } from '../simulation/nodes/nodes'; -import { focalPoint, initFocalSquare } from './focal-point'; - -/** - * Base class for different modes. - */ -class ModeHandler { - constructor(container) { - this.container = container; - } - - render() { - return ''; - } - - setupEventListeners() {} - - handleWebSocketMessage(data) {} - - cleanup() {} -} - -/** - * Handler for Boof mode. - */ -class BoofModeHandler extends ModeHandler { - render() { - return ` -
- - - -
- `; - } - - setupEventListeners() { - this.boofInputField = this.container.shadow.querySelector('.boof-input'); - this.boofButton = this.container.shadow.querySelector('.boof-submit'); - this.sendBoofMessageBound = this.sendBoofMessage.bind(this); - this.boofButton.addEventListener('click', this.sendBoofMessageBound); - } - - handleWebSocketMessage(data) { - if (data.content) { - this.container.displayBoof(data); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('boof-received', { detail: data })); - } - } - - sendBoofMessage() { - const message = this.boofInputField.value.trim(); - if (message) { - this.container.ws.send(message); - this.boofInputField.value = ''; // Clear the input field after sending - this.container.displayMessage('Boof message sent', 'success'); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('boof-sent', { detail: { content: message } })); - } else { - this.container.displayMessage('Cannot send an empty Boof message', 'warning'); - } - } - - cleanup() { - if (this.boofButton) { - this.boofButton.removeEventListener('click', this.sendBoofMessageBound); - } - } -} - -/** - * Handler for Boon mode. - */ -class BoonModeHandler extends ModeHandler { - render() { - // Set default values - const defaultValues = { - name: 'Default Boon Name', - x: 0, - y: 0, - z: 0, - description: 'Default description for the boon', - level: 1, - isActive: true, - }; - - return ` -
-

Create Boon

-
-
- Basic Information - - -
-
- Position - - - - - - -
-
- Boon Details - - - - - -
-
- -
- `; - } - - setupEventListeners() { - const shadow = this.container.shadow; - this.boonNameField = shadow.querySelector('.boon-name'); - this.boonXField = shadow.querySelector('.boon-x'); - this.boonYField = shadow.querySelector('.boon-y'); - this.boonZField = shadow.querySelector('.boon-z'); - this.boonDescriptionField = shadow.querySelector('.boon-description'); - this.boonLevelField = shadow.querySelector('.boon-level'); - this.boonIsActiveField = shadow.querySelector('.boon-is-active'); - this.boonButton = shadow.querySelector('.boon-submit'); - - this.sendBoonMessageBound = this.sendBoonMessage.bind(this); - this.boonButton.addEventListener('click', this.sendBoonMessageBound); - } - - handleWebSocketMessage(data) { - if (data.id && data.name && data.boonhonk) { - this.container.processBoon(data); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('boon-received', { detail: data })); - } - } - - sendBoonMessage() { - const boon = { - id: Date.now(), - name: this.boonNameField.value.trim(), - x: parseFloat(this.boonXField.value), - y: parseFloat(this.boonYField.value), - z: parseFloat(this.boonZField.value), - boonhonk: { - description: this.boonDescriptionField.value.trim(), - level: parseInt(this.boonLevelField.value, 10), - is_active: this.boonIsActiveField.checked, - }, - image_id: null, - }; - - // Validate the boon data - if ( - !boon.name || - isNaN(boon.x) || - isNaN(boon.y) || - isNaN(boon.z) || - !boon.boonhonk.description || - isNaN(boon.boonhonk.level) - ) { - this.container.displayMessage('Please fill in all Boon fields correctly.', 'warning'); - return; - } - - // Send the Boon as a JSON string - this.container.ws.send(JSON.stringify(boon)); - - // Clear the form fields - this.boonNameField.value = ''; - this.boonXField.value = ''; - this.boonYField.value = ''; - this.boonZField.value = ''; - this.boonDescriptionField.value = ''; - this.boonLevelField.value = ''; - this.boonIsActiveField.checked = false; - - this.container.displayMessage('Boon message sent', 'success'); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('boon-sent', { detail: boon })); - } - - cleanup() { - if (this.boonButton) { - this.boonButton.removeEventListener('click', this.sendBoonMessageBound); - } - } -} - -/** - * Handler for Focal mode. - */ -class FocalModeHandler extends ModeHandler { - render() { - return ` - - `; - } - - setupEventListeners() { - this.focalButton = this.container.shadow.querySelector('.focal-submit'); - this.setFocalPointBound = this.setFocalPoint.bind(this); - this.focalButton.addEventListener('click', this.setFocalPointBound); - initFocalSquare(); - } - - handleWebSocketMessage(data) { - if (data.type === 'focal-point') { - this.container.displayMessage(`Focal point updated: ${JSON.stringify(data.position)}`); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('focal-point-received', { detail: data })); - } - } - - setFocalPoint() { - const bounds = { - x1: focalPoint.x - 50, - y1: focalPoint.y - 50, - x2: focalPoint.x + 50, - y2: focalPoint.y + 50, - }; - - const focalData = { - type: 'focal-point', - position: { x: focalPoint.x, y: focalPoint.y }, - bounds, - }; - - this.container.ws.send(JSON.stringify(focalData)); - this.container.displayMessage( - `Focal point sent with position (${focalData.position.x}, ${focalData.position.y}) and bounds (${bounds.x1}, ${bounds.y1}, ${bounds.x2}, ${bounds.y2})`, - 'success' - ); - // Emit custom event - this.container.dispatchEvent(new CustomEvent('focal-point-sent', { detail: focalData })); - } - - cleanup() { - if (this.focalButton) { - this.focalButton.removeEventListener('click', this.setFocalPointBound); - } - } -} - -/** - * Main WebSocket container class. - */ -class SpwashiWebSocketContainer extends HTMLElement { - constructor() { - super(); - this.currentMode = 'boof'; - this.modeHandlers = { - boof: BoofModeHandler, - boon: BoonModeHandler, - focal: FocalModeHandler, - // Additional modes can be added here - }; - this.setupWebSocketContainer(); - } - - /** - * Initializes the WebSocket container and its event listeners. - */ - setupWebSocketContainer() { - this.shadow = this.attachShadow({ mode: 'open' }); - this.render(); // Render the initial UI - - const room = document.getElementById('title-md5').innerText - console.log('ancient knowledge being used ... title-md5') - this.ws = new WebSocket(`ws://${window.location.host}/ws/${room}`); - this.setupWebSocket(); - } - - /** - * Renders the UI based on the current mode. - */ - render() { - const template = document.createElement('template'); - template.innerHTML = this.getTemplateHtml(); - // Clear the shadow DOM and append the new content - this.shadow.innerHTML = ''; - this.shadow.appendChild(template.content.cloneNode(true)); - - // Always get reference to message container - this.messageContainer = this.shadow.querySelector('.response-wrapper'); - - // Get references to mode selection elements - this.modeSelector = this.shadow.querySelector('.mode-selector select'); - this.modeSelector.value = this.currentMode; - - // Cleanup previous mode handler - if (this.currentModeHandler) { - this.currentModeHandler.cleanup(); - } - - // Initialize the current mode handler - const ModeHandlerClass = this.modeHandlers[this.currentMode]; - if (ModeHandlerClass) { - this.currentModeHandler = new ModeHandlerClass(this); - this.currentModeHandler.setupEventListeners(); - } else { - this.currentModeHandler = null; - } - - // Setup event listeners - this.setupEventListeners(); - } - - /** - * Returns the HTML template for the component. - */ - getTemplateHtml() { - const modes = ['passive', 'boof', 'boon', 'focal']; - const optionsHtml = modes - .map( - (mode) => - `` - ) - .join(''); - - let modeSpecificHtml = ''; - if (this.modeHandlers[this.currentMode]) { - modeSpecificHtml = this.modeHandlers[this.currentMode].prototype.render(); - } - - return ` - -
- -
-
- ${modeSpecificHtml} - `; - } - - /** - * Sets up WebSocket event listeners for message handling and error reporting. - */ - setupWebSocket() { - this.ws.onmessage = (event) => this.handleWebSocketMessage(event); - this.ws.onopen = () => { - this.displayMessage('WebSocket connection opened', 'success'); - this.dispatchEvent(new Event('ws-open')); - }; - this.ws.onclose = () => { - this.displayMessage('WebSocket connection closed', 'warning'); - this.dispatchEvent(new Event('ws-close')); - }; - this.ws.onerror = () => { - this.displayMessage('WebSocket error', 'error'); - this.dispatchEvent(new Event('ws-error')); - }; - } - - /** - * Sets up event listeners for user input interactions. - */ - setupEventListeners() { - // Remove any existing event listeners to prevent duplicates - if (this.modeSelector) { - this.modeSelector.removeEventListener('change', this.handleModeChangeBound); - } - - // Mode selector event listener - this.handleModeChangeBound = this.handleModeChange.bind(this); - this.modeSelector.addEventListener('change', this.handleModeChangeBound); - } - - /** - * Handles the mode change event. - */ - handleModeChange() { - this.currentMode = this.modeSelector.value; - this.render(); // Re-render the UI based on the new mode - // Emit custom event - this.dispatchEvent(new CustomEvent('mode-changed', { detail: { mode: this.currentMode } })); - } - - /** - * Handles incoming WebSocket messages, delegating to the current mode handler. - * - * @param {MessageEvent} event - The WebSocket message event. - */ - handleWebSocketMessage(event) { - try { - const data = JSON.parse(event.data); - - if (this.currentModeHandler && typeof this.currentModeHandler.handleWebSocketMessage === 'function') { - this.currentModeHandler.handleWebSocketMessage(data); - } else { - // Default handling for messages not processed by mode handlers - if (data.image_id) { - this.displayMessage(`Received image with ID: ${data.image_id}`); - } else { - this.displayMessage(`Received message: ${event.data}`, 'info'); - } - } - } catch (error) { - console.error('Error processing WebSocket message:', error); - this.displayMessage('Failed to process WebSocket data.', 'error'); - } - } - - /** - * Processes a Boon object received from the server. - * - * @param {Object} boon - The Boon object to process. - */ - processBoon(boon) { - // Process the Boon object, e.g., add it to the NODE_MANAGER - const node = { - ...boon, - fx: boon.x, - fy: boon.y, - id: `${boon.id}`, // Ensure id is a string - callbacks: { - click: (e, d) => { - console.debug('Node clicked:', d); - }, - }, - }; - NODE_MANAGER.initNodes([node], true); - window.spwashi.reinit(); - - // Display the Boon in the message container - const boonElement = document.createElement('pre'); - boonElement.textContent = `Boon: ${boon.name}, Position: (${boon.x}, ${boon.y}, ${boon.z}), Description: ${boon.boonhonk.description}, Level: ${boon.boonhonk.level}, Active: ${boon.boonhonk.is_active}`; - this.messageContainer.appendChild(boonElement); - this.messageContainer.scrollTop = this.messageContainer.scrollHeight; - - // Emit custom event - this.dispatchEvent(new CustomEvent('boon-processed', { detail: boon })); - } - - /** - * Displays a Boof message in the message container. - * - * @param {Object} boof - The Boof message object. - */ - displayBoof(boof) { - const boofElement = document.createElement('div'); - boofElement.textContent = `Boof: ${boof.content}`; - boofElement.classList.add('boof-message'); - this.messageContainer.appendChild(boofElement); - this.messageContainer.scrollTop = this.messageContainer.scrollHeight; - } - - /** - * Displays a message in the message container. - * - * @param {string} message - The message to display. - * @param {string} [type] - Optional message type for styling (e.g., 'info', 'success', 'warning', 'error'). - */ - displayMessage(message, type = 'info') { - const messageElement = document.createElement('div'); - messageElement.textContent = message; - messageElement.classList.add('message', type); - this.messageContainer.appendChild(messageElement); - // Scroll to the bottom to show the latest message - this.messageContainer.scrollTop = this.messageContainer.scrollHeight; - } -} - -/** - * Initializes and defines the custom WebSocket container element. - */ -export function initWebSocketContainer() { - customElements.define('spwashi-websocket-container', SpwashiWebSocketContainer); -} diff --git a/src/stylesheets/interaction/websocket-container.scss b/src/stylesheets/interaction/websocket-container.scss deleted file mode 100644 index 335c38b..0000000 --- a/src/stylesheets/interaction/websocket-container.scss +++ /dev/null @@ -1,12 +0,0 @@ -#main-websocket-container { - position: fixed; - bottom: 0; - right: 0; - background: #222; - color: var(--accent-color-main); - padding: 1rem; - margin: 1rem; - line-height: 1; - font-size: 2rem; - outline: thin solid var(--accent-color-main); -} \ No newline at end of file diff --git a/src/stylesheets/vendor/ace-edit.scss b/src/stylesheets/vendor/ace-edit.scss deleted file mode 100644 index e69de29..0000000