diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..7e0f31f --- /dev/null +++ b/favicon.svg @@ -0,0 +1,4 @@ + diff --git a/gemini.md b/gemini.md index 5ae05dc..251afce 100644 --- a/gemini.md +++ b/gemini.md @@ -375,10 +375,11 @@ The `?v=Date.now()` on imports handles this automatically on page load. The router currently only has two routes. These sections need to be created: -### `board-control` section (not started) -- Full keyboard layout overlaid on a top-view board image -- Click a letter/number → IK move to `LOOKUP_TABLE[char]` -- Perhaps a "type a word" sequential move feature +### `board-control` section (implemented) +- Uses predefined background maps (SVG/PNG) with corresponding JSON config files specifying clickable coordinates in physical mm. +- Click a spot on the map or select from a compact list → IK move to coordinates. +- Supports movement modes (e.g., Direct, Erratic, Random, Snaky). +- Requires arm to be homed; homing state is persisted across sessions via `localStorage`. ### `sequence-editor` section (not started) - Record and replay a sequence of moves @@ -407,6 +408,7 @@ To add a new section: | **No dynamic BLE imports**| `index.html` must import `ble.js` via static `import`. Dynamic cache-busters create multiple instances of the BLE singleton, isolating UI event listeners! | | **`fivebarIKGame.js` is reference only** | The original site's game uses a different angle convention (absolute degrees, different home, with `ikElbowSigns` tracking). We use the C++ IK formula instead because it directly produces step deltas from home. `fivebarIKGame.js` is kept in the repo for reference and understanding, not imported. | | **Hash-based routing** | Keeps the SPA working from `file://` and simple static servers without needing a history API setup | +| **Persistent Homing State** | The UI uses `localStorage.getItem('wiji_homed')` to track if the arm has been homed during the user's ongoing interaction. This avoids forcing the user to re-home every time they switch tabs or pages. Sending `HOMEALL` sets this to `true`. | --- diff --git a/generate_bg.py b/generate_bg.py new file mode 100644 index 0000000..bb0c9e4 --- /dev/null +++ b/generate_bg.py @@ -0,0 +1,54 @@ +import json +import os + +svg_lines = [ + '') + +with open('web/assets/backgrounds/portrait-right/bg.svg', 'w') as f: + f.write('\n'.join(svg_lines)) + +with open('web/assets/backgrounds/portrait-right/spots.json', 'w') as f: + json.dump({ + "bounds": { "xMin": -150, "xMax": 150, "yMin": -30, "yMax": 145 }, + "spots": spots + }, f, indent=2) diff --git a/index.html b/index.html index 427bda4..110a0ea 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,8 @@ + + @@ -26,22 +28,19 @@ - + Home + + + Board Control + + - + Stepper Test @@ -51,18 +50,7 @@
- - @@ -83,10 +71,7 @@ @@ -101,11 +86,7 @@ @@ -126,10 +107,12 @@ import UI from './web/js/ui.js'; import HomeSection from './web/js/sections/home.js'; import StepperSection from './web/js/sections/stepper-test.js'; + import BoardControlSection from './web/js/sections/board-control.js'; // ── Register routes ──────────────────────────────────────────── Router.register('home', HomeSection); Router.register('stepper-test', StepperSection); + Router.register('board-control', BoardControlSection); // ── Sidebar toggle logic ─────────────────────────────────────── const sidebar = document.getElementById('sidebar'); diff --git a/web/assets/backgrounds/default/bg.svg b/web/assets/backgrounds/default/bg.svg new file mode 100644 index 0000000..3ad0b24 --- /dev/null +++ b/web/assets/backgrounds/default/bg.svg @@ -0,0 +1,49 @@ + diff --git a/web/assets/backgrounds/default/spots.json b/web/assets/backgrounds/default/spots.json new file mode 100644 index 0000000..24f270c --- /dev/null +++ b/web/assets/backgrounds/default/spots.json @@ -0,0 +1,29 @@ +{ + "bounds": { "xMin": -150, "xMax": 150, "yMin": -30, "yMax": 145 }, + "spots": [ + { "id": "YES", "label": "YES", "x": -70, "y": 90 }, + { "id": "NO", "label": "NO", "x": 70, "y": 90 }, + { "id": "A", "label": "A", "x": -100, "y": 60 }, + { "id": "B", "label": "B", "x": -80, "y": 64 }, + { "id": "C", "label": "C", "x": -60, "y": 67 }, + { "id": "D", "label": "D", "x": -40, "y": 69 }, + { "id": "E", "label": "E", "x": -20, "y": 70 }, + { "id": "F", "label": "F", "x": 0, "y": 70 }, + { "id": "G", "label": "G", "x": 20, "y": 70 }, + { "id": "H", "label": "H", "x": 40, "y": 69 }, + { "id": "I", "label": "I", "x": 60, "y": 67 }, + { "id": "J", "label": "J", "x": 80, "y": 64 }, + { "id": "K", "label": "K", "x": 100, "y": 60 }, + { "id": "L", "label": "L", "x": -90, "y": 40 }, + { "id": "M", "label": "M", "x": -70, "y": 43 }, + { "id": "N", "label": "N", "x": -50, "y": 45 }, + { "id": "O", "label": "O", "x": -30, "y": 46 }, + { "id": "P", "label": "P", "x": -10, "y": 47 }, + { "id": "Q", "label": "Q", "x": 10, "y": 47 }, + { "id": "R", "label": "R", "x": 30, "y": 46 }, + { "id": "S", "label": "S", "x": 50, "y": 45 }, + { "id": "T", "label": "T", "x": 70, "y": 43 }, + { "id": "U", "label": "U", "x": 90, "y": 40 }, + { "id": "GOODBYE", "label": "GOODBYE", "x": 0, "y": -15 } + ] +} diff --git a/web/assets/backgrounds/image_spec.md b/web/assets/backgrounds/image_spec.md new file mode 100644 index 0000000..4d3e74e --- /dev/null +++ b/web/assets/backgrounds/image_spec.md @@ -0,0 +1,66 @@ +# WijiBoard Background Image Specifications + +If you are developing custom backgrounds for the Board Control section, follow these technical specifications to ensure your design perfectly aligns with the SCARA mechanism's physical limits and the web UI's coordinate system. + +## 1. Physical Dimensions & Proportions + +The underlying coordinate system of the WijiBoard maps directly to the physical workspace of the SCARA arm in millimeters (mm). + +- **Total Physical Width (X-axis):** 300 mm +- **Total Physical Height (Y-axis):** 175 mm +- **Aspect Ratio:** 300:175 (which simplifies exactly to **12:7**) + +### Physical Boundaries: +- **X Range:** `-150` (Left) to `+150` (Right). Center is `0`. +- **Y Range:** `-30` (Bottom) to `+145` (Top). *Note: The arm bases (motors) are located near Y=0, so the usable board area extends upwards.* + +## 2. Raster Images (PNG, WebP, JPG) + +If you are designing your background in Photoshop, GIMP, or another raster editor: +- **Resolution:** Your image **must** have a 12:7 aspect ratio. Recommended resolutions are: + - 1200 × 700 px + - 2400 × 1400 px (Recommended for high-DPI/Retina screens) + - 3000 × 1750 px +- **Color Depth:** Standard 24-bit RGB or 32-bit RGBA (if transparency is needed). +- **Format:** Optimized PNG or WebP is recommended to prevent compression artifacts around text/letters. + +## 3. Vector Graphics (SVG) - *Highly Recommended* + +If you are using Illustrator, Inkscape, or writing SVG by hand, vector graphics are preferred because they scale infinitely without losing quality. + +- **ViewBox:** Set your SVG `viewBox` to exactly match the physical proportions, for example: `viewBox="0 0 300 175"`. +- **Coordinate Mapping:** If your viewBox is `0 0 300 175`: + - `SVG X = Physical X + 150` + - `SVG Y = 145 - Physical Y` +- Keep paths clean and compress the SVG if it contains highly complex paths. + +## 4. Setting Up Your New Background + +1. Create a new folder inside `web/assets/backgrounds/` (e.g., `web/assets/backgrounds/my-custom-board/`). +2. Place your image in this folder and name it `bg.svg` or `bg.png` (Update `board-control.js` if you change the file extension to PNG). +3. Create a `spots.json` file in the exact same folder to define your clickable targets. + +### `spots.json` Format +This file tells the web UI where your letters/targets are located in **physical millimeters**, NOT image pixels. + +```json +{ + "bounds": { + "xMin": -150, + "xMax": 150, + "yMin": -30, + "yMax": 145 + }, + "spots": [ + { "id": "yes", "label": "YES", "x": -60.5, "y": 125.0 }, + { "id": "no", "label": "NO", "x": 60.5, "y": 125.0 }, + { "id": "A", "label": "A", "x": -118.0, "y": 95.0 } + ] +} +``` +- **bounds**: Defines the physical bounding box (in mm) that your background image represents. This automatically adjusts the UI's aspect ratio and coordinate mapping! +- **spots**: Array of target objects. +- **id**: Unique identifier for the dropdown list. +- **label**: Human-readable text shown on the UI. +- **x**: The X coordinate in physical mm (`-150` to `+150`). +- **y**: The Y coordinate in physical mm (`-30` to `+145`). diff --git a/web/assets/backgrounds/portrait-right/bg.svg b/web/assets/backgrounds/portrait-right/bg.svg new file mode 100644 index 0000000..5df5e99 --- /dev/null +++ b/web/assets/backgrounds/portrait-right/bg.svg @@ -0,0 +1,56 @@ + diff --git a/web/assets/backgrounds/portrait-right/spots.json b/web/assets/backgrounds/portrait-right/spots.json new file mode 100644 index 0000000..3736a5f --- /dev/null +++ b/web/assets/backgrounds/portrait-right/spots.json @@ -0,0 +1,31 @@ +{ + "bounds": { "xMin": 30, "xMax": 150, "yMin": -30, "yMax": 145 }, + "spots": [ + { "id": "A", "label": "Aa", "x": 55, "y": 120 }, + { "id": "B", "label": "Bb", "x": 90, "y": 120 }, + { "id": "C", "label": "Cc", "x": 125, "y": 120 }, + { "id": "D", "label": "Dd", "x": 55, "y": 104 }, + { "id": "E", "label": "Ee", "x": 90, "y": 104 }, + { "id": "F", "label": "Ff", "x": 125, "y": 104 }, + { "id": "G", "label": "Gg", "x": 55, "y": 88 }, + { "id": "H", "label": "Hh", "x": 90, "y": 88 }, + { "id": "I", "label": "Ii", "x": 125, "y": 88 }, + { "id": "J", "label": "Jj", "x": 55, "y": 72 }, + { "id": "K", "label": "Kk", "x": 90, "y": 72 }, + { "id": "L", "label": "Ll", "x": 125, "y": 72 }, + { "id": "M", "label": "Mm", "x": 55, "y": 56 }, + { "id": "N", "label": "Nn", "x": 90, "y": 56 }, + { "id": "O", "label": "Oo", "x": 125, "y": 56 }, + { "id": "P", "label": "Pp", "x": 55, "y": 40 }, + { "id": "Q", "label": "Qq", "x": 90, "y": 40 }, + { "id": "R", "label": "Rr", "x": 125, "y": 40 }, + { "id": "S", "label": "Ss", "x": 55, "y": 24 }, + { "id": "T", "label": "Tt", "x": 90, "y": 24 }, + { "id": "U", "label": "Uu", "x": 125, "y": 24 }, + { "id": "V", "label": "Vv", "x": 55, "y": 8 }, + { "id": "W", "label": "Ww", "x": 90, "y": 8 }, + { "id": "X", "label": "Xx", "x": 125, "y": 8 }, + { "id": "Y", "label": "Yy", "x": 55, "y": -8 }, + { "id": "Z", "label": "Zz", "x": 90, "y": -8 } + ] +} diff --git a/web/assets/icons.svg b/web/assets/icons.svg new file mode 100644 index 0000000..860cfa5 --- /dev/null +++ b/web/assets/icons.svg @@ -0,0 +1,29 @@ + diff --git a/web/js/sections/board-control.js b/web/js/sections/board-control.js new file mode 100644 index 0000000..7c213b4 --- /dev/null +++ b/web/js/sections/board-control.js @@ -0,0 +1,362 @@ +import BLE from '../ble.js'; +import UI from '../ui.js'; +import IK from '../kinematics.js'; + +let eventCleanup = []; +let isHomed = false; +let spots = []; +let bounds = { xMin: -150, xMax: 150, yMin: -30, yMax: 145 }; +let currentBg = localStorage.getItem('wiji_bg') || 'default'; +let steps1 = 0; +let steps2 = 0; + +function syncStateFromStorage() { + isHomed = localStorage.getItem('wiji_homed') === 'true'; + steps1 = isHomed ? parseInt(localStorage.getItem('wiji_steps1') || IK.ARM.HOME_STEPS.m1) : IK.ARM.HOME_STEPS.m1; + steps2 = isHomed ? parseInt(localStorage.getItem('wiji_steps2') || IK.ARM.HOME_STEPS.m2) : IK.ARM.HOME_STEPS.m2; +} + +function saveSteps() { + if (isHomed) { + localStorage.setItem('wiji_steps1', steps1); + localStorage.setItem('wiji_steps2', steps2); + } +} + +function buildHTML() { + return ` + + +Click on the map or select from the list to move the pointer.
+