- Node.js 20+
- npm (lockfile-based)
- GitHub Packages auth for
@satvisorcomscope — add to~/.npmrc://npm.pkg.github.com/:_authToken=YOUR_TOKEN @satvisorcom:registry=https://npm.pkg.github.com
npm install
npm run dev # Vite dev server on http://localhost:1420
npm run build # tsc + vite build → dist/
npm run preview # serve production build locally| Directory | Purpose |
|---|---|
src/stores/ |
Svelte 5 rune-based singleton stores (*.svelte.ts) |
src/ui/ |
Svelte components — windows, panels, toolbar. shared/ for reusables |
src/scene/ |
Three.js scene objects (earth, orbits, atmosphere, moon, sun, orrery) |
src/astro/ |
Pure math — SGP4 helpers, az/el, eclipse, magnitude, epoch utils |
src/passes/ |
Pass prediction (predictor class + Web Worker) |
src/rotator/ |
Rotator protocol drivers (rotctld, GS-232, EasyComm) |
src/shaders/ |
GLSL vertex/fragment shaders |
src/feedback/ |
Multi-target sensory feedback (audio, haptic, BLE devices) |
src/data/ |
TLE loading and source definitions |
src/interaction/ |
Camera controller and input |
src/styles/ |
Global CSS with all theme variables |
Two ways to test rotator functionality without hardware:
The project includes a WebSocket rotator simulator that speaks rotctld protocol with simulated motor physics (acceleration, deceleration, backlash):
npm run rotator-sim
# or with a custom port:
node scripts/rotator-sim.mjs 4540Connect in the app: Setup tab → Network mode → ws://localhost:4540 (or 4533 for default).
Use Hamlib's rotctld with the dummy backend for a more realistic test (supports az/el limits, error codes):
# Terminal 1: start rotctld with dummy rotator model
rotctld -m 1 -vvvvv -t 1234 -C min_el=5
# Terminal 2: bridge WebSocket to TCP (pick one)
websocat --binary ws-l:127.0.0.1:4540 tcp:127.0.0.1:1234
# or
websockify 4540 localhost:1234Install a bridge if needed:
cargo install websocat # single Rust binary
# or
pip install websockify # PythonConnect in the app: Setup tab → Network mode → ws://localhost:4540.
Verify rotctld is responding:
echo "p" | nc -q1 localhost 1234
# Should return two lines: azimuth and elevationSSH port-forward rotctld from a remote host, then bridge locally:
ssh user@rotator-host -L 4533:localhost:4533 -N &
websocat --binary ws-l:127.0.0.1:4540 tcp:127.0.0.1:4533Requires Rust stable toolchain and system dependencies:
# Ubuntu/Debian
sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# Build
npx tauri build --bundles deb,appimage # Linux
npx tauri build --bundles nsis,msi # WindowsFor development: npm run tauri dev
- Fork the repo and create a branch from
master - Make your changes and verify
npm run buildpasses (TypeScript is the only automated quality gate) - Open a pull request against
master
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.
Text color tiers:
| CSS variable | Palette field | Use for |
|---|---|---|
--text |
palette.text |
Primary text, hover states |
--text-dim |
palette.textDim |
Labels, secondary text |
--text-muted |
palette.textMuted |
Muted info |
--text-faint |
palette.textFaint |
Faint labels, axis text |
--text-ghost |
palette.textGhost |
Placeholders, section headers |
Semantic colors: --danger, --warning, --live for status indicators and alerts.
Satellite colors: 9-entry Pride flag palette in src/constants.ts. Use helpers, never inline rgb():
satColorCss(index)— CSS/canvas fillStylesatColorRgba(index, alpha)— translucent canvassatColorGl(index)— WebGL/Three.js (0–1 floats)
Class-based singletons with $state() fields, exported as export const fooStore = new FooStore().
- Callback hooks (e.g.,
onGraphicsChange) registered byAppat init, not Svelte subscriptions - localStorage persistence:
load()at startup + immediate writes in setters, all keys prefixedsatvisor_ - Immutable updates for collections:
this.x = new Set(...),this.x = { ...this.x, ... } - Store
load()calls go inapp.tsinit
Most user-facing toggles persist to localStorage. If a setting should survive page reload, follow this pattern:
- Add
$statefield to the store - Add to
loadToggles()with asatvisor_*key - Add a case to
setToggle() - Wire in component with
<Checkbox>+onchangecalling the setter
Never toggle state without persisting — direct assignment without a localStorage write is a bug.
| Component | Use for |
|---|---|
Checkbox.svelte |
All toggle checkboxes |
DraggableWindow.svelte |
Floating windows (collision avoidance, edge snapping) |
MobileSheet.svelte |
Mobile bottom sheets |
InfoTip.svelte |
Hover tooltips on labels |
Modal.svelte |
Modal dialogs |
Slider.svelte |
Range inputs with label and value display |
Button.svelte |
All buttons |
icons.ts |
Inline SVG strings via {@html ICON_FOO} |
Every window must support both desktop and mobile:
- Add open state to
uiStore:myWindowOpen = $state(false) - Choose a unique
idstring shared by DraggableWindow and MobileSheet - Use the snippet pattern — extract content into
{#snippet windowContent()}:{#if uiStore.isMobile} <MobileSheet id="my-feature" title="Title" icon={myIcon}> {@render windowContent()} </MobileSheet> {:else} <DraggableWindow id="my-feature" title="Title" icon={myIcon} bind:open={uiStore.myWindowOpen} initialX={10} initialY={200}> {@render windowContent()} </DraggableWindow> {/if} - Register in
MobileNav.svelte(moreItemsarray) - Mount in
Overlay.svelteunconditionally - Add toolbar button in
TlePicker.svelteand optionally a keyboard shortcut ininput-handler.ts - Add command palette action in
CommandPalette.svelte
Satellite icons come from a sprite atlas — a horizontal strip of 256x256 sprites in public/textures/ui/sat_sprites.png.
To add a new sprite:
- Create
public/textures/ui/sprites/NN-name.svg(256x256 viewBox,#e3e3e3fill) - Add slot constant in
src/scene/sprite-config.ts - Update
getSpriteIndex()for matching satellites - Run
./scripts/generate-sprite.sh
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.