|
| 1 | +# Contributing |
| 2 | + |
| 3 | +## Prerequisites |
| 4 | + |
| 5 | +- **Node.js** 20+ |
| 6 | +- **npm** (lockfile-based) |
| 7 | +- GitHub Packages auth for `@satvisorcom` scope — add to `~/.npmrc`: |
| 8 | + ``` |
| 9 | + //npm.pkg.github.com/:_authToken=YOUR_TOKEN |
| 10 | + @satvisorcom:registry=https://npm.pkg.github.com |
| 11 | + ``` |
| 12 | + |
| 13 | +## Getting Started |
| 14 | + |
| 15 | +```bash |
| 16 | +npm install |
| 17 | +npm run dev # Vite dev server on http://localhost:1420 |
| 18 | +npm run build # tsc + vite build → dist/ |
| 19 | +npm run preview # serve production build locally |
| 20 | +``` |
| 21 | + |
| 22 | +## Project Structure |
| 23 | + |
| 24 | +| Directory | Purpose | |
| 25 | +|-----------|---------| |
| 26 | +| `src/stores/` | Svelte 5 rune-based singleton stores (`*.svelte.ts`) | |
| 27 | +| `src/ui/` | Svelte components — windows, panels, toolbar. `shared/` for reusables | |
| 28 | +| `src/scene/` | Three.js scene objects (earth, orbits, atmosphere, moon, sun, orrery) | |
| 29 | +| `src/astro/` | Pure math — SGP4 helpers, az/el, eclipse, magnitude, epoch utils | |
| 30 | +| `src/passes/` | Pass prediction (predictor class + Web Worker) | |
| 31 | +| `src/rotator/` | Rotator protocol drivers (rotctld, GS-232, EasyComm) | |
| 32 | +| `src/shaders/` | GLSL vertex/fragment shaders | |
| 33 | +| `src/feedback/` | Multi-target sensory feedback (audio, haptic, BLE devices) | |
| 34 | +| `src/data/` | TLE loading and source definitions | |
| 35 | +| `src/interaction/` | Camera controller and input | |
| 36 | +| `src/styles/` | Global CSS with all theme variables | |
| 37 | + |
| 38 | +## Testing the Rotator |
| 39 | + |
| 40 | +Two ways to test rotator functionality without hardware: |
| 41 | + |
| 42 | +### Built-in simulator |
| 43 | + |
| 44 | +The project includes a WebSocket rotator simulator that speaks rotctld protocol with simulated motor physics (acceleration, deceleration, backlash): |
| 45 | + |
| 46 | +```bash |
| 47 | +npm run rotator-sim |
| 48 | +# or with a custom port: |
| 49 | +node scripts/rotator-sim.mjs 4534 |
| 50 | +``` |
| 51 | + |
| 52 | +Connect in the app: Setup tab → Network mode → `ws://localhost:4534` (or `4533` for default). |
| 53 | + |
| 54 | +### rotctld dummy rotator |
| 55 | + |
| 56 | +Use Hamlib's `rotctld` with the dummy backend for a more realistic test (supports az/el limits, error codes): |
| 57 | + |
| 58 | +```bash |
| 59 | +# Terminal 1: start rotctld with dummy rotator model |
| 60 | +rotctld -m 1 -vvvvv -t 1234 -C min_el=5 |
| 61 | + |
| 62 | +# Terminal 2: bridge WebSocket to TCP (pick one) |
| 63 | +websocat --binary ws-l:127.0.0.1:4534 tcp:127.0.0.1:1234 |
| 64 | +# or |
| 65 | +websockify 4534 localhost:1234 |
| 66 | +``` |
| 67 | + |
| 68 | +Install a bridge if needed: |
| 69 | +```bash |
| 70 | +cargo install websocat # single Rust binary |
| 71 | +# or |
| 72 | +pip install websockify # Python |
| 73 | +``` |
| 74 | + |
| 75 | +Connect in the app: Setup tab → Network mode → `ws://localhost:4534`. |
| 76 | + |
| 77 | +Verify rotctld is responding: |
| 78 | +```bash |
| 79 | +echo "p" | nc -q1 localhost 1234 |
| 80 | +# Should return two lines: azimuth and elevation |
| 81 | +``` |
| 82 | + |
| 83 | +### Testing with a remote rotator |
| 84 | + |
| 85 | +SSH port-forward rotctld from a remote host, then bridge locally: |
| 86 | + |
| 87 | +```bash |
| 88 | +ssh user@rotator-host -L 4533:localhost:4533 -N & |
| 89 | +websocat --binary ws-l:127.0.0.1:4534 tcp:127.0.0.1:4533 |
| 90 | +``` |
| 91 | + |
| 92 | +## Desktop Builds (Tauri) |
| 93 | + |
| 94 | +Requires Rust stable toolchain and system dependencies: |
| 95 | + |
| 96 | +```bash |
| 97 | +# Ubuntu/Debian |
| 98 | +sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf |
| 99 | + |
| 100 | +# Build |
| 101 | +npx tauri build --bundles deb,appimage # Linux |
| 102 | +npx tauri build --bundles nsis,msi # Windows |
| 103 | +``` |
| 104 | + |
| 105 | +For development: `npm run tauri dev` |
| 106 | + |
| 107 | +## Submitting Changes |
| 108 | + |
| 109 | +1. Fork the repo and create a branch from `master` |
| 110 | +2. Make your changes and verify `npm run build` passes (TypeScript is the only automated quality gate) |
| 111 | +3. Open a pull request against `master` |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Conventions & Patterns |
| 116 | + |
| 117 | +### Theming |
| 118 | + |
| 119 | +All colors go through CSS custom properties in `src/styles/global.css` `:root`. Never hardcode hex/rgb/rgba values in `<style>` blocks or inline styles. For canvas rendering, use the `palette` object from `src/ui/shared/theme.ts`. |
| 120 | + |
| 121 | +**Text color tiers:** |
| 122 | + |
| 123 | +| CSS variable | Palette field | Use for | |
| 124 | +|---|---|---| |
| 125 | +| `--text` | `palette.text` | Primary text, hover states | |
| 126 | +| `--text-dim` | `palette.textDim` | Labels, secondary text | |
| 127 | +| `--text-muted` | `palette.textMuted` | Muted info | |
| 128 | +| `--text-faint` | `palette.textFaint` | Faint labels, axis text | |
| 129 | +| `--text-ghost` | `palette.textGhost` | Placeholders, section headers | |
| 130 | + |
| 131 | +**Semantic colors:** `--danger`, `--warning`, `--live` for status indicators and alerts. |
| 132 | + |
| 133 | +**Satellite colors:** 9-entry Pride flag palette in `src/constants.ts`. Use helpers, never inline `rgb()`: |
| 134 | +- `satColorCss(index)` — CSS/canvas fillStyle |
| 135 | +- `satColorRgba(index, alpha)` — translucent canvas |
| 136 | +- `satColorGl(index)` — WebGL/Three.js (0–1 floats) |
| 137 | + |
| 138 | +### Stores |
| 139 | + |
| 140 | +Class-based singletons with `$state()` fields, exported as `export const fooStore = new FooStore()`. |
| 141 | + |
| 142 | +- Callback hooks (e.g., `onGraphicsChange`) registered by `App` at init, not Svelte subscriptions |
| 143 | +- localStorage persistence: `load()` at startup + immediate writes in setters, all keys prefixed `satvisor_` |
| 144 | +- Immutable updates for collections: `this.x = new Set(...)`, `this.x = { ...this.x, ... }` |
| 145 | +- Store `load()` calls go in `app.ts` init |
| 146 | + |
| 147 | +### Persisted Toggles |
| 148 | + |
| 149 | +Most user-facing toggles persist to localStorage. If a setting should survive page reload, follow this pattern: |
| 150 | + |
| 151 | +1. Add `$state` field to the store |
| 152 | +2. Add to `loadToggles()` with a `satvisor_*` key |
| 153 | +3. Add a case to `setToggle()` |
| 154 | +4. Wire in component with `<Checkbox>` + `onchange` calling the setter |
| 155 | + |
| 156 | +Never toggle state without persisting — direct assignment without a localStorage write is a bug. |
| 157 | + |
| 158 | +### Shared Components |
| 159 | + |
| 160 | +| Component | Use for | |
| 161 | +|-----------|---------| |
| 162 | +| `Checkbox.svelte` | All toggle checkboxes | |
| 163 | +| `DraggableWindow.svelte` | Floating windows (collision avoidance, edge snapping) | |
| 164 | +| `MobileSheet.svelte` | Mobile bottom sheets | |
| 165 | +| `InfoTip.svelte` | Hover tooltips on labels | |
| 166 | +| `Modal.svelte` | Modal dialogs | |
| 167 | +| `Slider.svelte` | Range inputs with label and value display | |
| 168 | +| `Button.svelte` | All buttons | |
| 169 | +| `icons.ts` | Inline SVG strings via `{@html ICON_FOO}` | |
| 170 | + |
| 171 | +### Adding a New Window |
| 172 | + |
| 173 | +Every window must support both desktop and mobile: |
| 174 | + |
| 175 | +1. Add open state to `uiStore`: `myWindowOpen = $state(false)` |
| 176 | +2. Choose a unique `id` string shared by DraggableWindow and MobileSheet |
| 177 | +3. Use the snippet pattern — extract content into `{#snippet windowContent()}`: |
| 178 | + ```svelte |
| 179 | + {#if uiStore.isMobile} |
| 180 | + <MobileSheet id="my-feature" title="Title" icon={myIcon}> |
| 181 | + {@render windowContent()} |
| 182 | + </MobileSheet> |
| 183 | + {:else} |
| 184 | + <DraggableWindow id="my-feature" title="Title" icon={myIcon} |
| 185 | + bind:open={uiStore.myWindowOpen} initialX={10} initialY={200}> |
| 186 | + {@render windowContent()} |
| 187 | + </DraggableWindow> |
| 188 | + {/if} |
| 189 | + ``` |
| 190 | +4. Register in `MobileNav.svelte` (`moreItems` array) |
| 191 | +5. Mount in `Overlay.svelte` unconditionally |
| 192 | +6. Add toolbar button in `TlePicker.svelte` and optionally a keyboard shortcut in `input-handler.ts` |
| 193 | +7. Add command palette action in `CommandPalette.svelte` |
| 194 | + |
| 195 | +### Sprite Atlas |
| 196 | + |
| 197 | +Satellite icons come from a sprite atlas — a horizontal strip of 256x256 sprites in `public/textures/ui/sat_sprites.png`. |
| 198 | + |
| 199 | +To add a new sprite: |
| 200 | +1. Create `public/textures/ui/sprites/NN-name.svg` (256x256 viewBox, `#e3e3e3` fill) |
| 201 | +2. Add slot constant in `src/scene/sprite-config.ts` |
| 202 | +3. Update `getSpriteIndex()` for matching satellites |
| 203 | +4. Run `./scripts/generate-sprite.sh` |
| 204 | + |
| 205 | +### Feedback System |
| 206 | + |
| 207 | +The app routes events to audio, haptic, and BLE outputs via `feedbackStore`. Shared components (`Button`, `Checkbox`, `Slider`) already fire feedback through global DOM listeners — no per-component wiring needed. For new discrete interactions, add to `FeedbackEvent` enum and `FEEDBACK_MAP` in `src/feedback/types.ts`. For continuous interactions (drag, scrub), use `feedbackStore.fireDynamic(intensity)` with 0–1 range. |
0 commit comments