added board control
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M 12 3 C 18 8 21 14 19 20 C 17 22 7 22 5 20 C 3 14 6 8 12 3 Z" stroke="#a8c7fa" stroke-width="1.5" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="14" r="3" stroke="#a8c7fa" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@@ -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`. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
svg_lines = [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 175">',
|
||||
' <!-- Background -->',
|
||||
' <rect width="300" height="175" fill="#fdf6e3"/>',
|
||||
' <!-- Decoration on left -->',
|
||||
' <circle cx="75" cy="87.5" r="60" fill="none" stroke="#eee8d5" stroke-width="4" stroke-dasharray="10,5"/>',
|
||||
' <circle cx="75" cy="87.5" r="40" fill="none" stroke="#eee8d5" stroke-width="2" />',
|
||||
' <text x="75" y="87.5" fill="#93a1a1" font-family="sans-serif" font-size="12" text-anchor="middle" font-weight="bold">MECHANISM</text>',
|
||||
' <text x="75" y="102.5" fill="#93a1a1" font-family="sans-serif" font-size="10" text-anchor="middle">CLEARANCE</text>',
|
||||
]
|
||||
|
||||
# X from 150 to 300
|
||||
for row in range(9):
|
||||
y_base = 25 + row * 16
|
||||
svg_lines.append(f' <line x1="155" y1="{y_base}" x2="295" y2="{y_base}" stroke="#e0e0e0" stroke-width="1"/>')
|
||||
svg_lines.append(f' <line x1="155" y1="{y_base-5}" x2="295" y2="{y_base-5}" stroke="#e0e0e0" stroke-width="0.5" stroke-dasharray="2,2"/>')
|
||||
svg_lines.append(f' <line x1="155" y1="{y_base-10}" x2="295" y2="{y_base-10}" stroke="#e0e0e0" stroke-width="1"/>')
|
||||
|
||||
spots = []
|
||||
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
for i, letter in enumerate(alphabet):
|
||||
col = i % 3
|
||||
row = i // 3
|
||||
|
||||
svg_x = 175 + col * 50
|
||||
svg_y = 25 + row * 16
|
||||
|
||||
phys_x = svg_x - 150
|
||||
phys_y = 145 - svg_y
|
||||
|
||||
spots.append({
|
||||
"id": letter,
|
||||
"label": f"{letter.upper()}{letter.lower()}",
|
||||
"x": phys_x,
|
||||
"y": phys_y
|
||||
})
|
||||
|
||||
text_str = f' <text x="{svg_x}" y="{svg_y}" fill="#268bd2" font-family="cursive, \'Comic Sans MS\', sans-serif" font-size="14" font-style="italic" text-anchor="middle">{letter.upper()}{letter.lower()}</text>'
|
||||
svg_lines.append(text_str)
|
||||
|
||||
svg_lines.append('</svg>')
|
||||
|
||||
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)
|
||||
+15
-32
@@ -8,6 +8,8 @@
|
||||
<meta name="description"
|
||||
content="WijiBoard SCARA arm control dashboard. Web Bluetooth interface for the ESP32-C3 stepper controller." />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
|
||||
<!-- Shared styles -->
|
||||
<link rel="stylesheet" href="web/styles/base.css" />
|
||||
<link rel="stylesheet" href="web/styles/components.css" />
|
||||
@@ -26,22 +28,19 @@
|
||||
|
||||
<a class="sidebar-nav-item active" id="nav-home" href="#home" data-route="home" role="button"
|
||||
aria-label="Go to Home">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-home"></use></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
<a class="sidebar-nav-item" id="nav-board-control" href="#board-control" data-route="board-control" role="button"
|
||||
aria-label="Go to Board Control">
|
||||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-board"></use></svg>
|
||||
Board Control
|
||||
</a>
|
||||
|
||||
<a class="sidebar-nav-item" id="nav-stepper-test" href="#stepper-test" data-route="stepper-test" role="button"
|
||||
aria-label="Go to Stepper Test">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" />
|
||||
</svg>
|
||||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-stepper"></use></svg>
|
||||
Stepper Test
|
||||
</a>
|
||||
|
||||
@@ -51,18 +50,7 @@
|
||||
<span class="sidebar-section-label">Coming Soon</span>
|
||||
|
||||
<div class="sidebar-nav-item" style="opacity:0.35;cursor:default" aria-disabled="true">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
Board Control
|
||||
</div>
|
||||
|
||||
<div class="sidebar-nav-item" style="opacity:0.35;cursor:default" aria-disabled="true">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z" />
|
||||
</svg>
|
||||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-sequence"></use></svg>
|
||||
Sequence Editor
|
||||
</div>
|
||||
</nav>
|
||||
@@ -83,10 +71,7 @@
|
||||
<!-- Brand -->
|
||||
<div class="topbar-brand">
|
||||
<div class="brand-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2L3 7v10l9 5 9-5V7L12 2Z" stroke="#448aff" stroke-width="1.5" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="12" r="2.5" fill="#448aff" opacity="0.7" />
|
||||
</svg>
|
||||
<svg width="16" height="16"><use href="web/assets/icons.svg#icon-brand"></use></svg>
|
||||
</div>
|
||||
WijiBoard
|
||||
</div>
|
||||
@@ -101,11 +86,7 @@
|
||||
|
||||
<!-- Connect / Disconnect button -->
|
||||
<button class="btn btn-sm btn-primary" id="btn-ble-connect" aria-label="Connect to WijiBoard via Bluetooth">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.5 6.5l11 11M17.5 6.5l-5 5 5 5" />
|
||||
<path d="M6 17.5l5-5-5-5" />
|
||||
</svg>
|
||||
<svg width="13" height="13"><use href="web/assets/icons.svg#icon-ble"></use></svg>
|
||||
<span class="btn-text">Connect</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 175" width="100%" height="100%">
|
||||
<!-- Wooden board background -->
|
||||
<rect width="300" height="175" fill="#d2b48c" rx="10" />
|
||||
|
||||
<!-- Outer border -->
|
||||
<rect x="5" y="5" width="290" height="165" fill="none" stroke="#5c4033" stroke-width="2" rx="5" />
|
||||
|
||||
<!-- Title -->
|
||||
<text x="150" y="25" font-family="serif" font-size="14" text-anchor="middle" fill="#5c4033" font-weight="bold">W I J I B O A R D</text>
|
||||
|
||||
<!-- Sun and Moon motifs -->
|
||||
<circle cx="40" cy="25" r="10" fill="none" stroke="#5c4033" stroke-width="1.5" />
|
||||
<path d="M 260 15 A 10 10 0 0 0 260 35 A 8 8 0 0 1 260 15" fill="#5c4033" />
|
||||
|
||||
<!-- YES / NO -->
|
||||
<text x="80" y="55" font-family="serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#5c4033">YES</text>
|
||||
<text x="220" y="55" font-family="serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#5c4033">NO</text>
|
||||
|
||||
<!-- Decorative arc -->
|
||||
<path d="M 50 85 Q 150 65 250 85" fill="none" stroke="#5c4033" stroke-width="0.5" stroke-dasharray="2,2" />
|
||||
|
||||
<!-- Letters A-K -->
|
||||
<text x="50" y="85" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">A</text>
|
||||
<text x="70" y="81" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">B</text>
|
||||
<text x="90" y="78" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">C</text>
|
||||
<text x="110" y="76" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">D</text>
|
||||
<text x="130" y="75" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">E</text>
|
||||
<text x="150" y="75" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">F</text>
|
||||
<text x="170" y="75" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">G</text>
|
||||
<text x="190" y="76" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">H</text>
|
||||
<text x="210" y="78" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">I</text>
|
||||
<text x="230" y="81" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">J</text>
|
||||
<text x="250" y="85" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">K</text>
|
||||
|
||||
<!-- L-V row below -->
|
||||
<text x="60" y="105" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">L</text>
|
||||
<text x="80" y="102" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">M</text>
|
||||
<text x="100" y="100" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">N</text>
|
||||
<text x="120" y="99" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">O</text>
|
||||
<text x="140" y="98" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">P</text>
|
||||
<text x="160" y="98" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">Q</text>
|
||||
<text x="180" y="99" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">R</text>
|
||||
<text x="200" y="100" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">S</text>
|
||||
<text x="220" y="102" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">T</text>
|
||||
<text x="240" y="105" font-family="serif" font-size="10" text-anchor="middle" fill="#5c4033">U</text>
|
||||
|
||||
<!-- GOODBYE -->
|
||||
<text x="150" y="160" font-family="serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#5c4033">GOODBYE</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -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`).
|
||||
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 175">
|
||||
<!-- Background -->
|
||||
<rect width="120" height="175" fill="#fdf6e3"/>
|
||||
<rect x="5" y="5" width="110" height="165" fill="none" stroke="#eee8d5" stroke-width="2" rx="4"/>
|
||||
|
||||
<g stroke="#e0e0e0">
|
||||
<!-- Row 0: 25 -->
|
||||
<line x1="10" y1="25" x2="110" y2="25" stroke-width="1"/>
|
||||
<line x1="10" y1="20" x2="110" y2="20" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="15" x2="110" y2="15" stroke-width="1"/>
|
||||
<!-- Row 1: 41 -->
|
||||
<line x1="10" y1="41" x2="110" y2="41" stroke-width="1"/>
|
||||
<line x1="10" y1="36" x2="110" y2="36" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="31" x2="110" y2="31" stroke-width="1"/>
|
||||
<!-- Row 2: 57 -->
|
||||
<line x1="10" y1="57" x2="110" y2="57" stroke-width="1"/>
|
||||
<line x1="10" y1="52" x2="110" y2="52" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="47" x2="110" y2="47" stroke-width="1"/>
|
||||
<!-- Row 3: 73 -->
|
||||
<line x1="10" y1="73" x2="110" y2="73" stroke-width="1"/>
|
||||
<line x1="10" y1="68" x2="110" y2="68" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="63" x2="110" y2="63" stroke-width="1"/>
|
||||
<!-- Row 4: 89 -->
|
||||
<line x1="10" y1="89" x2="110" y2="89" stroke-width="1"/>
|
||||
<line x1="10" y1="84" x2="110" y2="84" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="79" x2="110" y2="79" stroke-width="1"/>
|
||||
<!-- Row 5: 105 -->
|
||||
<line x1="10" y1="105" x2="110" y2="105" stroke-width="1"/>
|
||||
<line x1="10" y1="100" x2="110" y2="100" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="95" x2="110" y2="95" stroke-width="1"/>
|
||||
<!-- Row 6: 121 -->
|
||||
<line x1="10" y1="121" x2="110" y2="121" stroke-width="1"/>
|
||||
<line x1="10" y1="116" x2="110" y2="116" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="111" x2="110" y2="111" stroke-width="1"/>
|
||||
<!-- Row 7: 137 -->
|
||||
<line x1="10" y1="137" x2="110" y2="137" stroke-width="1"/>
|
||||
<line x1="10" y1="132" x2="110" y2="132" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="127" x2="110" y2="127" stroke-width="1"/>
|
||||
<!-- Row 8: 153 -->
|
||||
<line x1="10" y1="153" x2="110" y2="153" stroke-width="1"/>
|
||||
<line x1="10" y1="148" x2="110" y2="148" stroke-width="0.5" stroke-dasharray="2,2"/>
|
||||
<line x1="10" y1="143" x2="110" y2="143" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<g fill="#268bd2" font-family="cursive, 'Comic Sans MS', sans-serif" font-size="14" font-style="italic" text-anchor="middle">
|
||||
<text x="25" y="25">Aa</text><text x="60" y="25">Bb</text><text x="95" y="25">Cc</text>
|
||||
<text x="25" y="41">Dd</text><text x="60" y="41">Ee</text><text x="95" y="41">Ff</text>
|
||||
<text x="25" y="57">Gg</text><text x="60" y="57">Hh</text><text x="95" y="57">Ii</text>
|
||||
<text x="25" y="73">Jj</text><text x="60" y="73">Kk</text><text x="95" y="73">Ll</text>
|
||||
<text x="25" y="89">Mm</text><text x="60" y="89">Nn</text><text x="95" y="89">Oo</text>
|
||||
<text x="25" y="105">Pp</text><text x="60" y="105">Qq</text><text x="95" y="105">Rr</text>
|
||||
<text x="25" y="121">Ss</text><text x="60" y="121">Tt</text><text x="95" y="121">Uu</text>
|
||||
<text x="25" y="137">Vv</text><text x="60" y="137">Ww</text><text x="95" y="137">Xx</text>
|
||||
<text x="25" y="153">Yy</text><text x="60" y="153">Zz</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="icon-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="icon-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="icon-stepper" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="icon-sequence" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="icon-brand" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M 12 3 C 18 8 21 14 19 20 C 17 22 7 22 5 20 C 3 14 6 8 12 3 Z" stroke="#a8c7fa" stroke-width="1.5" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="14" r="3" stroke="#a8c7fa" stroke-width="1.5" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="icon-ble" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.5 6.5l11 11M17.5 6.5l-5 5 5 5" />
|
||||
<path d="M6 17.5l5-5-5-5" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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 `
|
||||
<style>
|
||||
.board-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 280px;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
}
|
||||
@media (max-width: 900px) { .board-grid { grid-template-columns: 1fr; } }
|
||||
.spot-marker:hover {
|
||||
background: rgba(0, 230, 118, 0.8) !important;
|
||||
transform: translate(-50%, -50%) scale(1.3) !important;
|
||||
}
|
||||
.homing-banner {
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.homing-banner.homed {
|
||||
background: rgba(0, 230, 118, 0.1);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(0, 230, 118, 0.3);
|
||||
}
|
||||
.homing-banner.not-homed {
|
||||
background: rgba(255, 82, 82, 0.1);
|
||||
color: var(--accent-red, #ff5252);
|
||||
border: 1px solid rgba(255, 82, 82, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="section-page fade-in" style="display:flex; flex-direction:column; height: 100%;">
|
||||
<div class="section-header">
|
||||
<h2>Board Control</h2>
|
||||
<p>Click on the map or select from the list to move the pointer.</p>
|
||||
</div>
|
||||
|
||||
<div class="board-grid">
|
||||
<!-- ── Map Area ─────────────────────────────────────── -->
|
||||
<div class="card" style="display:flex; flex-direction:column; padding: 12px; position:relative;">
|
||||
<div class="card-header"><span class="card-title">Interactive Map</span></div>
|
||||
|
||||
<div style="flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden;">
|
||||
<div id="map-container" style="position: relative; width: 100%; max-width: 800px; aspect-ratio: 300/175; background: #111; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5);">
|
||||
<img id="bg-image" src="web/assets/backgrounds/${currentBg}/bg.svg" style="width: 100%; height: 100%; object-fit: cover; position: absolute; top:0; left:0; border-radius: 8px;" />
|
||||
<div id="markers-layer" style="position: absolute; top:0; left:0; width:100%; height:100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Control Panel ──────────────────────────────── -->
|
||||
<div style="display:flex; flex-direction:column; gap: 12px;">
|
||||
|
||||
<!-- Homing Card -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span class="card-title">System Status</span>
|
||||
<div style="display:flex; gap: 12px;">
|
||||
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">X (mm)</div><div id="pos-x" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
|
||||
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">Y (mm)</div><div id="pos-y" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="homing-banner" class="homing-banner ${isHomed ? 'homed' : 'not-homed'}">
|
||||
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
|
||||
</div>
|
||||
<button id="btn-force-home" class="btn btn-danger btn-full" style="padding: 10px; font-weight:bold;">
|
||||
${isHomed ? 'FORCE HOME ALL' : 'HOME ALL MOTORS'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Controls Card -->
|
||||
<div class="card" style="flex:1; display:flex; flex-direction:column;">
|
||||
<div class="card-header"><span class="card-title">Navigation</span></div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 12px;">
|
||||
<label class="form-label">Background Map</label>
|
||||
<select id="bg-select" class="form-input" style="width:100%;">
|
||||
<option value="default">Default Spirit Board (Landscape)</option>
|
||||
<option value="portrait-right">Right Side Test (Portrait)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 12px;">
|
||||
<label class="form-label">Movement Mode</label>
|
||||
<select id="mode-select" class="form-input" style="width:100%;">
|
||||
<option value="direct">Direct (Normal)</option>
|
||||
<option value="erratic">Erratic (Coming Soon)</option>
|
||||
<option value="random">Random (Coming Soon)</option>
|
||||
<option value="snaky">Snaky (Coming Soon)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="flex:1; display:flex; flex-direction:column;">
|
||||
<label class="form-label">Available Spots</label>
|
||||
<select id="spot-list" size="10" class="form-input" style="flex:1; width: 100%; margin-bottom: 12px; font-family: monospace; font-size: 1rem; padding: 8px;">
|
||||
<!-- Populated dynamically -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="btn-go" class="btn btn-success btn-full" style="padding: 12px; font-weight:bold; font-size: 1.1rem;" disabled>
|
||||
GO TO SELECTION
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updatePositionReadout() {
|
||||
const t1 = IK.stepsToRad(steps1);
|
||||
const t2 = IK.stepsToRad(steps2);
|
||||
const { endX, endY } = IK.forward(t1, t2);
|
||||
const px = document.getElementById('pos-x');
|
||||
const py = document.getElementById('pos-y');
|
||||
if (px) px.textContent = isFinite(endX) ? endX.toFixed(1) : '---';
|
||||
if (py) py.textContent = isFinite(endY) ? endY.toFixed(1) : '---';
|
||||
}
|
||||
|
||||
function updateHomingUI() {
|
||||
const banner = document.getElementById('homing-banner');
|
||||
const btn = document.getElementById('btn-force-home');
|
||||
if (!banner || !btn) return;
|
||||
|
||||
if (isHomed) {
|
||||
banner.className = 'homing-banner homed';
|
||||
banner.textContent = '✓ HOMED';
|
||||
btn.textContent = 'FORCE HOME ALL';
|
||||
} else {
|
||||
banner.className = 'homing-banner not-homed';
|
||||
banner.textContent = '⚠ NOT HOMED! Movement Locked.';
|
||||
btn.textContent = 'HOME ALL MOTORS';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSpots() {
|
||||
try {
|
||||
const res = await fetch(`web/assets/backgrounds/${currentBg}/spots.json?v=${Date.now()}`);
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
spots = data;
|
||||
} else {
|
||||
spots = data.spots || [];
|
||||
if (data.bounds) bounds = data.bounds;
|
||||
}
|
||||
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
|
||||
renderSpots();
|
||||
} catch(e) {
|
||||
UI.log('Failed to load spots.json', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderSpots() {
|
||||
const listEl = document.getElementById('spot-list');
|
||||
const markersEl = document.getElementById('markers-layer');
|
||||
const mapContainer = document.getElementById('map-container');
|
||||
if (!listEl || !markersEl || !mapContainer) return;
|
||||
|
||||
const w = bounds.xMax - bounds.xMin;
|
||||
const h = bounds.yMax - bounds.yMin;
|
||||
mapContainer.style.aspectRatio = `${w}/${h}`;
|
||||
|
||||
listEl.innerHTML = '';
|
||||
markersEl.innerHTML = '';
|
||||
|
||||
spots.forEach(spot => {
|
||||
// List item
|
||||
const opt = document.createElement('option');
|
||||
opt.value = spot.id;
|
||||
opt.textContent = `${spot.label.padEnd(10, ' ')} [${spot.x}, ${spot.y}]`;
|
||||
listEl.appendChild(opt);
|
||||
|
||||
// Map marker
|
||||
const marker = document.createElement('div');
|
||||
const xPct = ((spot.x - bounds.xMin) / w) * 100;
|
||||
const yPct = ((bounds.yMax - spot.y) / h) * 100;
|
||||
|
||||
marker.className = 'spot-marker';
|
||||
marker.style = `position: absolute; left: ${xPct}%; top: ${yPct}%; transform: translate(-50%, -50%); width: 18px; height: 18px; border-radius: 50%; background: rgba(0, 230, 118, 0.4); border: 2px solid var(--accent-green); cursor: pointer; transition: all 0.2s;`;
|
||||
marker.title = spot.label;
|
||||
|
||||
marker.addEventListener('click', () => {
|
||||
listEl.value = spot.id;
|
||||
document.getElementById('btn-go').disabled = false;
|
||||
executeMove(spot);
|
||||
});
|
||||
|
||||
markersEl.appendChild(marker);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeMove(spot) {
|
||||
if (!isHomed) {
|
||||
UI.log('Cannot move: System is not homed! Please click HOME ALL.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = document.getElementById('mode-select').value;
|
||||
if (mode !== 'direct') {
|
||||
UI.log(`Mode '${mode}' not fully implemented yet! Using direct move.`, 'warn');
|
||||
}
|
||||
|
||||
// Calculate IK
|
||||
const wsCheck = IK.checkWorkspace(spot.x, spot.y);
|
||||
if (!wsCheck.ok) {
|
||||
UI.log(`⛔ ${wsCheck.reason}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = IK.solve(spot.x, spot.y);
|
||||
if (!res.reachable) {
|
||||
UI.log(`Target ${spot.label} is unreachable geometrically.`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (IK.armsCrossed(res.theta1, res.theta2)) {
|
||||
UI.log(`Target ${spot.label} rejected: elbows crossed!`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newSteps1 = IK.radToSteps(res.theta1);
|
||||
const newSteps2 = IK.radToSteps(res.theta2);
|
||||
const delta1 = newSteps1 - steps1;
|
||||
const delta2 = newSteps2 - steps2;
|
||||
|
||||
steps1 = newSteps1;
|
||||
steps2 = newSteps2;
|
||||
saveSteps();
|
||||
updatePositionReadout();
|
||||
|
||||
const cmd1 = `S1${delta1 >= 0 ? '+' : ''}${delta1}`;
|
||||
const cmd2 = `S2${delta2 >= 0 ? '+' : ''}${delta2}`;
|
||||
|
||||
UI.log(`Moving to ${spot.label} (${spot.x}, ${spot.y})`, 'success');
|
||||
|
||||
if (BLE.isConnected()) {
|
||||
try {
|
||||
await BLE.write(cmd1);
|
||||
await BLE.write(cmd2);
|
||||
} catch (e) {
|
||||
UI.log(`BLE error: ${e.message}`, 'error');
|
||||
}
|
||||
} else {
|
||||
UI.log(`[sim] ${cmd1} ${cmd2}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mount(container) {
|
||||
syncStateFromStorage();
|
||||
container.innerHTML = buildHTML();
|
||||
|
||||
updateHomingUI();
|
||||
loadSpots();
|
||||
|
||||
// ── Background Selection ──────────────────────────────────
|
||||
const bgSelect = document.getElementById('bg-select');
|
||||
if (bgSelect) {
|
||||
bgSelect.value = currentBg;
|
||||
bgSelect.addEventListener('change', () => {
|
||||
currentBg = bgSelect.value;
|
||||
localStorage.setItem('wiji_bg', currentBg);
|
||||
loadSpots();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Homing Logic ──────────────────────────────────────────
|
||||
document.getElementById('btn-force-home').addEventListener('click', async () => {
|
||||
isHomed = true;
|
||||
localStorage.setItem('wiji_homed', 'true');
|
||||
updateHomingUI();
|
||||
|
||||
steps1 = IK.ARM.HOME_STEPS.m1;
|
||||
steps2 = IK.ARM.HOME_STEPS.m2;
|
||||
saveSteps();
|
||||
updatePositionReadout();
|
||||
|
||||
UI.log('Homing all motors...', 'info');
|
||||
if (BLE.isConnected()) {
|
||||
try {
|
||||
await BLE.write('HOMEALL');
|
||||
} catch (e) {
|
||||
UI.log(e.message, 'error');
|
||||
}
|
||||
} else {
|
||||
UI.log('[sim] HOMEALL', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
// ── List Selection ────────────────────────────────────────
|
||||
const listEl = document.getElementById('spot-list');
|
||||
const goBtn = document.getElementById('btn-go');
|
||||
|
||||
listEl.addEventListener('change', () => {
|
||||
goBtn.disabled = !listEl.value;
|
||||
});
|
||||
|
||||
goBtn.addEventListener('click', () => {
|
||||
const selectedId = listEl.value;
|
||||
const spot = spots.find(s => s.id === selectedId);
|
||||
if (spot) {
|
||||
executeMove(spot);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Sync Steps from BLE ───────────────────────────────────
|
||||
const onBLEStatus = (e) => {
|
||||
const msg = e.detail;
|
||||
if (msg.startsWith('P:')) {
|
||||
const [s1, s2] = msg.slice(2).split(',').map(Number);
|
||||
steps1 = s1; steps2 = s2;
|
||||
saveSteps();
|
||||
updatePositionReadout();
|
||||
}
|
||||
};
|
||||
document.addEventListener('ble:status', onBLEStatus);
|
||||
|
||||
// Sync position on mount if connected
|
||||
if (BLE.isConnected()) {
|
||||
BLE.write('POS').catch(() => {});
|
||||
} else {
|
||||
updatePositionReadout();
|
||||
}
|
||||
|
||||
eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus));
|
||||
},
|
||||
|
||||
unmount() {
|
||||
eventCleanup.forEach(fn => fn());
|
||||
eventCleanup = [];
|
||||
}
|
||||
};
|
||||
+12
-10
@@ -57,6 +57,18 @@ const HomeSection = {
|
||||
<span class="card-title">Sections</span>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<button class="sidebar-nav-item" data-route="board-control"
|
||||
onclick="document.getElementById('nav-board-control').click()"
|
||||
style="padding:14px 16px;background:var(--surface-2);text-align:left;border:none;cursor:pointer;width:100%;">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.875rem;color:var(--text-primary)">Board Control</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:2px">Interactive map navigation</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="sidebar-nav-item" data-route="stepper-test"
|
||||
onclick="document.getElementById('nav-stepper-test').click()"
|
||||
style="padding:14px 16px;background:var(--surface-2)">
|
||||
@@ -68,16 +80,6 @@ const HomeSection = {
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:2px">Manually jog each motor, visualize arm position</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="sidebar-nav-item" style="padding:14px 16px;background:var(--surface-2);opacity:0.45;cursor:default">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.875rem;color:var(--text-primary)">Board Control</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:2px">Coming soon</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,10 +45,23 @@ const PX_L2 = IK.ARM.l2 * SV.SCALE;
|
||||
const PX_D2 = IK.ARM.d2 * SV.SCALE;
|
||||
|
||||
// ── Section state ─────────────────────────────────────────────────
|
||||
let steps1 = IK.ARM.HOME_STEPS.m1;
|
||||
let steps2 = IK.ARM.HOME_STEPS.m2;
|
||||
let steps1 = 0;
|
||||
let steps2 = 0;
|
||||
let eventCleanup = [];
|
||||
|
||||
function syncStateFromStorage() {
|
||||
const 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 (localStorage.getItem('wiji_homed') === 'true') {
|
||||
localStorage.setItem('wiji_steps1', steps1);
|
||||
localStorage.setItem('wiji_steps2', steps2);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SVG element refs ──────────────────────────────────────────────
|
||||
let svgEl;
|
||||
// Real arm elements
|
||||
@@ -135,6 +148,7 @@ function updateReadouts(t1, t2, ex, ey) {
|
||||
if (elTheta2) elTheta2.textContent = IK.radToDeg(t2).toFixed(1) + '°';
|
||||
if (elSteps1) elSteps1.textContent = steps1;
|
||||
if (elSteps2) elSteps2.textContent = steps2;
|
||||
saveSteps();
|
||||
}
|
||||
|
||||
// ── Ghost arm show/hide ───────────────────────────────────────────
|
||||
@@ -218,6 +232,7 @@ async function zeroMotor(motor) {
|
||||
if (motor === 'ALL') {
|
||||
steps1 = IK.ARM.HOME_STEPS.m1;
|
||||
steps2 = IK.ARM.HOME_STEPS.m2;
|
||||
localStorage.setItem('wiji_homed', 'true');
|
||||
} else if (motor === 1) {
|
||||
steps1 = IK.ARM.HOME_STEPS.m1;
|
||||
} else {
|
||||
@@ -616,6 +631,7 @@ function buildHTML() {
|
||||
|
||||
const StepperTestSection = {
|
||||
mount(container) {
|
||||
syncStateFromStorage();
|
||||
container.innerHTML = buildHTML();
|
||||
|
||||
// ── Cache refs ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user