diff --git a/ADAPTER_PARITY_PLAN.md b/ADAPTER_PARITY_PLAN.md index 2fed735..7ec01a8 100644 --- a/ADAPTER_PARITY_PLAN.md +++ b/ADAPTER_PARITY_PLAN.md @@ -629,6 +629,19 @@ The live playtest, eight-slot catalog, controller-native labels, automatic active-profile synchronization, and Sets A/B are implemented. Set C remains candidate work; the native Switch-family output plan below is separate. +Studio now uses nine supplied SVG assets with model-specific, viewBox-relative +hotspots and sideways Joy-Con source mappings. Live playtest schema 4 adds a +read-only topology byte; the host retains schema 2/3 compatibility. Auto views +follow actual matching-owner pair/solo metadata; explicit previews do not +change firmware topology or highlight another controller as detected hardware. +Rear/rail controls remain off-art and unavailable stored mappings are retained. +Chromium checks covered all assets, ten layout/orientation views, mobile +panning and non-overlapping hotspots, remapping, and simulated live topology +transitions without losing focus or draft data. The connected real Joy-Con pair +reported its paired layout on the flashed firmware. All 293 tests and all five +firmware builds passed. Pair profiles still share the left bank at this +checkpoint; independent pair banks are separate work. + | Priority | Candidate | Intended scope | Dependency or principal risk | |---|---|---|---| | A1 — Complete | Named and copyable profiles | Profile names and controller aliases use catalog metadata rather than input-profile fields. Copy/export/import preserve the current draft and per-section resets leave other settings unchanged. | Atomic metadata records; no report-path cost. | diff --git a/README.md b/README.md index 076a3f0..f56856c 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,11 @@ Development USB identities are `CAFE:4010` (XInput), `CAFE:4020` (DInput), and ` Switch 2's **C, GL, GR, Left SL/SR and Right SL/SR** are additional source-only controls. Map each to a normal button or trigger, assign a button-only alternate Shift mapping, or use it in action/macro chords, cancellation and modifiers. Extra mappings default to disabled. The live playtest shows raw extras separately; they are not fictitious output channels on the emulated Switch Pro/XInput controller. -The editor selects Switch Pro, DualSense, or Xbox artwork from the connected controller's USB VID/PID and places each remappable control directly over the matching physical button. Controller artwork is from [AL2009man/Gamepad-Asset-Pack](https://github.com/AL2009man/Gamepad-Asset-Pack) under its MIT license; the bundled license and source revision are recorded beside the assets. +Controller Studio uses the supplied lightweight SVGs for Switch 2 Pro, Joy-Con 2 left/right solo and paired layouts, original Switch Pro, DualSense, Xbox, and Wii Remote/Nunchuk views. Hotspots follow the artwork's actual coordinates; solo Joy-Con views rotate with their firmware input mappings. Rear buttons and rails are labeled below the front view rather than drawn in fictitious positions. On narrow screens, pan the diagram or use the **Source control** menu. + +**Auto** uses matching-owner live metadata to distinguish a Joy-Con pair from a solo half. **Preview** changes only the editor's diagram and source labels; it does not pair controllers or change saved mappings, and physical highlighting is disabled. Source choices reflect the layout while stored unavailable mappings are retained. Wii orientation is not reported, so horizontal/vertical views require an explicit preview. The current pair still uses the left controller's profile bank; select that owner to edit paired input. + +The read-only playtest endpoint (`0x39`) uses schema 4, 56 bytes: byte 55 identifies unspecified (0), Joy-Con 2 left solo (1), right solo (2), pair (3), Wii Remote (4), or Wii Remote + Nunchuk (5). Host tools still read schema 2/54-byte and schema 3/55-byte payloads; older firmware cannot confirm Joy-Con topology. This metadata does not change profile records, input mapping, or rumble. Profile names and controller aliases are stored as independently checksummed catalog metadata. Runtime profiles use schema 7 and unchanged 384-byte records; schemas 1–6 migrate with extra inputs unmapped and existing settings preserved. Names remain separate. The diff --git a/firmware/switch-pico-adapter-feasibility.elf b/firmware/switch-pico-adapter-feasibility.elf index efd2e3a..f436fee 100755 Binary files a/firmware/switch-pico-adapter-feasibility.elf and b/firmware/switch-pico-adapter-feasibility.elf differ diff --git a/firmware/switch-pico-adapter-feasibility.uf2 b/firmware/switch-pico-adapter-feasibility.uf2 index d023c05..8617236 100644 Binary files a/firmware/switch-pico-adapter-feasibility.uf2 and b/firmware/switch-pico-adapter-feasibility.uf2 differ diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index 105953f..bd239e3 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index c62875f..351794a 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/src/firmware/input/bluepad32_input_backend.cpp b/src/firmware/input/bluepad32_input_backend.cpp index 70d2e67..20c09cb 100644 --- a/src/firmware/input/bluepad32_input_backend.cpp +++ b/src/firmware/input/bluepad32_input_backend.cpp @@ -2829,6 +2829,32 @@ void bluepad32_input_backend_playtest_snapshot( (slot.device->report_parser.set_lightbar_color != nullptr ? 2u : 0u) | (slot.device->report_parser.set_player_leds != nullptr ? 4u : 0u) | (slot.state.motion_sample_count != 0 ? 8u : 0u); + if (slot.active) { + const int side = joycon_side(slot.device); + if (side != 0) { + out->controller_layout = slot.companion != nullptr + ? Bluepad32ControllerLayout::kJoyCon2MergedPair + : side < 0 + ? Bluepad32ControllerLayout::kJoyCon2LeftSolo + : Bluepad32ControllerLayout::kJoyCon2RightSolo; + } else { + switch (slot.device->controller_subtype) { + case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL: + case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL: + case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL: + out->controller_layout = + Bluepad32ControllerLayout::kWiiRemote; + break; + case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK: + case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL: + out->controller_layout = + Bluepad32ControllerLayout::kWiiNunchuk; + break; + default: + break; + } + } + } } critical_section_exit(&g_state_lock); } diff --git a/src/firmware/input/bluepad32_input_backend.h b/src/firmware/input/bluepad32_input_backend.h index 02a85eb..b56cbd8 100644 --- a/src/firmware/input/bluepad32_input_backend.h +++ b/src/firmware/input/bluepad32_input_backend.h @@ -64,6 +64,15 @@ struct Bluepad32SlotSnapshot { ControllerState state; }; +enum class Bluepad32ControllerLayout : uint8_t { + kUnspecified = 0, + kJoyCon2LeftSolo = 1, + kJoyCon2RightSolo = 2, + kJoyCon2MergedPair = 3, + kWiiRemote = 4, + kWiiNunchuk = 5, +}; + // Side-effect-free raw input snapshot for management telemetry. Unlike the // report-path snapshot, reading this does not consume motion samples. struct Bluepad32PlaytestSnapshot { @@ -74,6 +83,8 @@ struct Bluepad32PlaytestSnapshot { uint16_t physical_button_mask = 0; uint8_t battery = 0; uint8_t capabilities = 0; + Bluepad32ControllerLayout controller_layout = + Bluepad32ControllerLayout::kUnspecified; ControllerState state{}; }; diff --git a/src/firmware/usb/usb_configuration_management.cpp b/src/firmware/usb/usb_configuration_management.cpp index 823013b..0c4f163 100644 --- a/src/firmware/usb/usb_configuration_management.cpp +++ b/src/firmware/usb/usb_configuration_management.cpp @@ -516,7 +516,9 @@ size_t encode_profile_playtest( CONTROLLER_IDENTITY_ENCODED_SIZE) || snapshot.state.motion_sample_count > CONTROLLER_MOTION_SAMPLE_CAPACITY || - (snapshot.state.extra_buttons & 0x80u) != 0) { + (snapshot.state.extra_buttons & 0x80u) != 0 || + static_cast(snapshot.controller_layout) > + static_cast(Bluepad32ControllerLayout::kWiiNunchuk)) { return 0; } payload[0] = 1; @@ -538,6 +540,7 @@ size_t encode_profile_playtest( payload[39] = snapshot.battery; payload[40] = snapshot.capabilities; payload[54] = snapshot.state.extra_buttons; + payload[55] = static_cast(snapshot.controller_layout); if (snapshot.state.motion_sample_count != 0) { payload[0] |= 2; const ControllerMotionSample& motion = diff --git a/src/firmware/usb/usb_configuration_management.h b/src/firmware/usb/usb_configuration_management.h index 2e63396..c277696 100644 --- a/src/firmware/usb/usb_configuration_management.h +++ b/src/firmware/usb/usb_configuration_management.h @@ -23,8 +23,8 @@ constexpr size_t kProfileListRowSize = 16 + PROFILE_SERVICE_METADATA_MAX_BYTES + 1; constexpr size_t kProfileListPayloadSize = 1 + PROFILE_SERVICE_LIST_CAPACITY * kProfileListRowSize; -constexpr uint16_t kProfilePlaytestSchemaVersion = 3; -constexpr size_t kProfilePlaytestPayloadSize = 55; +constexpr uint16_t kProfilePlaytestSchemaVersion = 4; +constexpr size_t kProfilePlaytestPayloadSize = 56; constexpr size_t kProfileMetadataPayloadSize = (CONTROLLER_PROFILE_COUNT + 1) * (PROFILE_SERVICE_METADATA_MAX_BYTES + 1); diff --git a/src/switch_pico_bridge/config_manager.py b/src/switch_pico_bridge/config_manager.py index d2afe9a..7bea264 100755 --- a/src/switch_pico_bridge/config_manager.py +++ b/src/switch_pico_bridge/config_manager.py @@ -166,8 +166,18 @@ PROFILE_MACRO_REPEAT_MIN = 1 PROFILE_MACRO_REPEAT_MAX = 255 PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION = 2 PROFILE_PLAYTEST_LEGACY_SIZE = 54 -PROFILE_PLAYTEST_SCHEMA_VERSION = 3 -PROFILE_PLAYTEST_SIZE = 55 +PROFILE_PLAYTEST_EXTRA_BUTTON_SCHEMA_VERSION = 3 +PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE = 55 +PROFILE_PLAYTEST_SCHEMA_VERSION = 4 +PROFILE_PLAYTEST_SIZE = 56 +PROFILE_PLAYTEST_LAYOUTS = ( + None, + "joycon2-left", + "joycon2-right", + "joycon2-pair", + "wii-remote", + "wii-nunchuk", +) PROFILE_PLAYTEST_SLOT_COUNT = 4 PROFILE_METADATA_SCHEMA_VERSION = 1 PROFILE_METADATA_MAX_BYTES = 31 @@ -900,6 +910,7 @@ class ProfilePlaytest: capabilities: int motion: tuple[int, int, int, int, int, int] | None extra_buttons: int = 0 + layout: str | None = None def to_json_object(self) -> dict[str, Any]: return { @@ -917,6 +928,7 @@ class ProfilePlaytest: if self.identity is not None else None ), + "layout": self.layout, "buttons": _button_mask_to_json(self.button_mask), "extra_buttons": [ name for index, name in enumerate(EXTRA_BUTTONS) @@ -3629,12 +3641,16 @@ def parse_profile_playtest(envelope: Envelope) -> ProfilePlaytest: _raise_status(envelope) expected_size = { PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION: PROFILE_PLAYTEST_LEGACY_SIZE, + PROFILE_PLAYTEST_EXTRA_BUTTON_SCHEMA_VERSION: PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE, PROFILE_PLAYTEST_SCHEMA_VERSION: PROFILE_PLAYTEST_SIZE, }.get(envelope.schema_version) if len(envelope.payload) != expected_size: raise ConfigManagerError("invalid profile playtest payload") payload = envelope.payload - extra_buttons = payload[54] if envelope.schema_version == PROFILE_PLAYTEST_SCHEMA_VERSION else 0 + extra_buttons = payload[54] if len(payload) >= PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE else 0 + layout_code = payload[55] if envelope.schema_version == PROFILE_PLAYTEST_SCHEMA_VERSION else 0 + if layout_code >= len(PROFILE_PLAYTEST_LAYOUTS): + raise ConfigManagerError("invalid playtest controller layout") if extra_buttons & ~0x7F: raise ConfigManagerError("invalid playtest extra buttons") flags = payload[0] @@ -3687,6 +3703,7 @@ def parse_profile_playtest(envelope: Envelope) -> ProfilePlaytest: capabilities=payload[40], motion=motion_values if has_motion else None, extra_buttons=extra_buttons, + layout=PROFILE_PLAYTEST_LAYOUTS[layout_code], ) diff --git a/src/switch_pico_bridge/profile_web.py b/src/switch_pico_bridge/profile_web.py index 7aeadd1..9aca4c7 100644 --- a/src/switch_pico_bridge/profile_web.py +++ b/src/switch_pico_bridge/profile_web.py @@ -24,17 +24,35 @@ _ASSET_TYPES = { "text/javascript; charset=utf-8", ), "/style.css": ("profile_editor.css", "text/css; charset=utf-8"), - "/assets/controller-switch-pro.svg": ( - "assets/controller-switch-pro.svg", - "image/svg+xml", + "/assets/controller_layouts.js": ( + "controller_layouts.js", "text/javascript; charset=utf-8", ), - "/assets/controller-dualsense.svg": ( - "assets/controller-dualsense.svg", - "image/svg+xml", + "/assets/switch-pro-controller-simple.svg": ( + "assets/switch-pro-controller-simple.svg", "image/svg+xml", ), - "/assets/controller-xbox.svg": ( - "assets/controller-xbox.svg", - "image/svg+xml", + "/assets/switch-2-pro-controller-simple.svg": ( + "assets/switch-2-pro-controller-simple.svg", "image/svg+xml", + ), + "/assets/switch-2-joycon-left.svg": ( + "assets/switch-2-joycon-left.svg", "image/svg+xml", + ), + "/assets/switch-2-joycon-single.svg": ( + "assets/switch-2-joycon-single.svg", "image/svg+xml", + ), + "/assets/switch-2-joycons-connected.svg": ( + "assets/switch-2-joycons-connected.svg", "image/svg+xml", + ), + "/assets/ps5-dualsense-simple.svg": ( + "assets/ps5-dualsense-simple.svg", "image/svg+xml", + ), + "/assets/xbox-controller-simple.svg": ( + "assets/xbox-controller-simple.svg", "image/svg+xml", + ), + "/assets/wii-remote-simple.svg": ( + "assets/wii-remote-simple.svg", "image/svg+xml", + ), + "/assets/wii-remote-nunchuk-simple.svg": ( + "assets/wii-remote-nunchuk-simple.svg", "image/svg+xml", ), } @@ -110,34 +128,92 @@ _CONTROL_LABELS = { "left_stick": "L3", "right_stick": "R3", }, + "wii": { + "north": "B", + "east": "2", + "south": "1", + "west": "A", + "select": "Minus", + "start": "Plus", + "system": "Home", + }, } def _controller_presentation( identity: config_manager.ControllerIdentity, + layout: str | None = None, ) -> dict[str, str]: + live_layouts = { + "joycon2-left": ("Nintendo Joy-Con 2 (L)", "switch", {0x2067}), + "joycon2-right": ("Nintendo Joy-Con 2 (R)", "switch", {0x2066}), + "joycon2-pair": ("Nintendo Joy-Con 2 pair", "switch", {0x2067, 0x2066}), + "wii-remote": ("Nintendo Wii Remote", "wii", {0x0306, 0x0330}), + "wii-nunchuk": ("Nintendo Wii Remote + Nunchuk", "wii", {0x0306, 0x0330}), + } + live = live_layouts.get(layout) + if live is not None and ( + identity.is_global_fallback + or identity.vendor_id == 0x057E and identity.product_id in live[2] + ): + return {"model": live[0], "style": live[1], "layout": cast(str, layout)} if identity.is_global_fallback: - return {"model": "Generic controller", "style": "generic"} + return {"model": "Generic controller", "style": "generic", "layout": "generic"} known = { - (0x057E, 0x2009): ("Nintendo Switch Pro Controller", "switch"), - (0x057E, 0x2069): ("Nintendo Switch 2 Pro Controller", "switch"), - (0x057E, 0x2067): ("Nintendo Joy-Con 2 (L)", "switch"), - (0x057E, 0x2066): ("Nintendo Joy-Con 2 (R)", "switch"), - (0x054C, 0x0CE6): ("Sony DualSense", "playstation"), - (0x054C, 0x09CC): ("Sony DualShock 4", "playstation"), + (0x057E, 0x2009): ("Nintendo Switch Pro Controller", "switch", "switch-pro"), + (0x057E, 0x2069): ("Nintendo Switch 2 Pro Controller", "switch", "switch2-pro"), + (0x057E, 0x2067): ("Nintendo Joy-Con 2 (L)", "switch", "joycon2-left"), + (0x057E, 0x2066): ("Nintendo Joy-Con 2 (R)", "switch", "joycon2-right"), + (0x054C, 0x0CE6): ("Sony DualSense", "playstation", "dualsense"), + (0x054C, 0x0DF2): ("Sony DualSense Edge", "playstation", "dualsense"), + (0x054C, 0x09CC): ("Sony DualShock 4", "playstation", "generic"), + # VID/PID cannot distinguish Wii extensions or Wii U Pro from Remote Plus. + (0x057E, 0x0306): ("Nintendo Wii controller", "generic", "generic"), + (0x057E, 0x0330): ("Nintendo Wii / Wii U controller", "generic", "generic"), } exact = known.get((identity.vendor_id, identity.product_id)) if exact is not None: - model, style = exact - return {"model": model, "style": style} + model, style, model_layout = exact + return {"model": model, "style": style, "layout": model_layout} if identity.vendor_id == 0x054C: - return {"model": "Sony controller", "style": "playstation"} + return {"model": "Sony controller", "style": "playstation", "layout": "generic"} if identity.vendor_id == 0x045E: - return {"model": "Xbox controller", "style": "xbox"} + return {"model": "Xbox controller", "style": "xbox", "layout": "xbox"} if identity.vendor_id == 0x2DC8: - return {"model": "8BitDo controller", "style": "switch"} - return {"model": "Connected controller", "style": "generic"} + return {"model": "8BitDo controller", "style": "switch", "layout": "generic"} + return {"model": "Connected controller", "style": "generic", "layout": "generic"} + + +def _source_controls(controller: dict[str, str]) -> list[str]: + layout = controller["layout"] + if layout == "switch2-pro": + controls = config_manager.OUTPUT_CONTROLS + ("c", "gl", "gr") + elif layout == "joycon2-pair": + controls = config_manager.OUTPUT_CONTROLS + ( + "c", "left_sl", "left_sr", "right_sl", "right_sr", + ) + elif layout in {"joycon2-left", "joycon2-right"}: + controls = ( + "south", "east", "west", "north", "left_shoulder", + "right_shoulder", "left_stick", + ) + ( + ("select", "capture", "left_trigger", "left_sl", "left_sr") + if layout == "joycon2-left" + else ("start", "system", "right_trigger", "c", "right_sl", "right_sr") + ) + elif layout in {"wii-remote", "wii-nunchuk"}: + controls = ( + "south", "east", "west", "north", "select", "start", "system", + "dpad_up", "dpad_down", "dpad_left", "dpad_right", + ) + ( + ("left_shoulder", "right_shoulder") if layout == "wii-nunchuk" else () + ) + elif layout in {"switch-pro", "dualsense", "xbox"} or controller["style"] == "playstation": + controls = config_manager.OUTPUT_CONTROLS + else: + controls = config_manager.LOGICAL_CONTROLS + return list(controls) def _controller_label( @@ -446,28 +522,21 @@ class ProfileEditorHandler(BaseHTTPRequestHandler): def _list_profiles(self) -> dict[str, Any]: device = self.profile_server.find_device() entries = config_manager.list_profiles(device) - return { - "identities": [ - { - "index": index, - "label": _controller_label(entry.identity, entry.alias), - "key": entry.identity.to_bytes().hex(), - "active_profile": entry.active_profile_index + 1, - "controller": _controller_presentation(entry.identity), - "alias": entry.alias, - "modifier_controls": list( - config_manager.LOGICAL_CONTROLS - if entry.identity.vendor_id == 0x057E - and entry.identity.product_id in {0x2069, 0x2067, 0x2066} - else config_manager.OUTPUT_CONTROLS - if _controller_presentation(entry.identity)["style"] - in {"xbox", "playstation"} - else config_manager.LOGICAL_BUTTONS + config_manager.EXTRA_BUTTONS - ), - } - for index, entry in enumerate(entries) - ] - } + identities = [] + for index, entry in enumerate(entries): + controller = _controller_presentation(entry.identity) + source_controls = _source_controls(controller) + identities.append({ + "index": index, + "label": _controller_label(entry.identity, entry.alias), + "key": entry.identity.to_bytes().hex(), + "active_profile": entry.active_profile_index + 1, + "controller": controller, + "alias": entry.alias, + "source_controls": source_controls, + "modifier_controls": source_controls, + }) + return {"identities": identities} def _read_profile(self, identity_index: int, profile_index: int) -> dict[str, Any]: device = self.profile_server.find_device() @@ -495,10 +564,11 @@ class ProfileEditorHandler(BaseHTTPRequestHandler): else None ) result["controller"] = ( - _controller_presentation(playtest.identity) - if playtest.identity is not None - else None + _controller_presentation(playtest.identity, playtest.layout) + if playtest.connected and playtest.identity is not None + else _controller_presentation(identity) ) + result["source_controls"] = _source_controls(result["controller"]) result["label"] = ( _controller_label(playtest.identity) if playtest.identity is not None diff --git a/src/switch_pico_bridge/web/assets/GAMEPAD_ASSET_LICENSE.txt b/src/switch_pico_bridge/web/assets/GAMEPAD_ASSET_LICENSE.txt deleted file mode 100644 index 64f58ef..0000000 --- a/src/switch_pico_bridge/web/assets/GAMEPAD_ASSET_LICENSE.txt +++ /dev/null @@ -1,35 +0,0 @@ -Controller artwork source -========================= - -The following files are redistributed from AL2009man/Gamepad-Asset-Pack -revision a6d1113b0b3b2438bbbcb08a33f9a25eb2391d7c: - -- controller-dualsense.svg -- controller-switch-pro.svg -- controller-xbox.svg - -Source: https://github.com/AL2009man/Gamepad-Asset-Pack -Copyright (c) 2024 Al. Lopez -License: MIT - -MIT License - -Copyright (c) 2024 Al. Lopez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/switch_pico_bridge/web/assets/controller-dualsense.svg b/src/switch_pico_bridge/web/assets/controller-dualsense.svg deleted file mode 100644 index 92a893e..0000000 --- a/src/switch_pico_bridge/web/assets/controller-dualsense.svg +++ /dev/null @@ -1,2013 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/switch_pico_bridge/web/assets/controller-switch-pro.svg b/src/switch_pico_bridge/web/assets/controller-switch-pro.svg deleted file mode 100644 index 5b68d3e..0000000 --- a/src/switch_pico_bridge/web/assets/controller-switch-pro.svg +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/switch_pico_bridge/web/assets/controller-xbox.svg b/src/switch_pico_bridge/web/assets/controller-xbox.svg deleted file mode 100644 index a8c9e97..0000000 --- a/src/switch_pico_bridge/web/assets/controller-xbox.svg +++ /dev/null @@ -1,1383 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/switch_pico_bridge/web/assets/ps5-dualsense-simple.svg b/src/switch_pico_bridge/web/assets/ps5-dualsense-simple.svg new file mode 100644 index 0000000..893ebe0 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/ps5-dualsense-simple.svg @@ -0,0 +1,117 @@ + + PS5 DualSense wireless controller — white + Front-view white-and-black PlayStation 5 DualSense controller, with symmetrical analog sticks, a touchpad with blue edge lights, directional buttons, triangle circle cross square controls, speaker, PlayStation and microphone buttons. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images. + Visual reference: https://www.playstation.com/en-us/accessories/dualsense-wireless-controller/ . Independently drawn vector artwork. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/switch-2-joycon-left.svg b/src/switch_pico_bridge/web/assets/switch-2-joycon-left.svg new file mode 100644 index 0000000..2acac85 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/switch-2-joycon-left.svg @@ -0,0 +1,69 @@ + + Nintendo Switch 2 — left Joy-Con 2 + Front-view left Joy-Con 2 with a graphite shell, light-blue accent ring and attachment edge, analog stick, four directional buttons, minus and capture buttons. Transparent background; four gradients, no filters, textures or external assets. + Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/switch-2-joycon-single.svg b/src/switch_pico_bridge/web/assets/switch-2-joycon-single.svg new file mode 100644 index 0000000..b3cb7e1 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/switch-2-joycon-single.svg @@ -0,0 +1,76 @@ + + Nintendo Switch 2 — right Joy-Con 2 + Front-view right Joy-Con 2 with a graphite shell, coral accent ring, ABXY buttons, analog stick, plus, home and C buttons. Transparent background; four gradients, no filters, textures or external assets. + Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/switch-2-joycons-connected.svg b/src/switch_pico_bridge/web/assets/switch-2-joycons-connected.svg new file mode 100644 index 0000000..5270231 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/switch-2-joycons-connected.svg @@ -0,0 +1,143 @@ + + Nintendo Switch 2 — connected Joy-Con 2 pair + Front-view left and right Joy-Con 2 attached to a central controller grip. Graphite shells, light-blue and coral stick rings, directional and ABXY buttons, capture, home and C buttons. Transparent background; four gradients, no filters, textures or external assets. + Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/switch-2-pro-controller-simple.svg b/src/switch_pico_bridge/web/assets/switch-2-pro-controller-simple.svg new file mode 100644 index 0000000..018ce86 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/switch-2-pro-controller-simple.svg @@ -0,0 +1,132 @@ + + Nintendo Switch 2 Pro Controller — lightweight transparent illustration + A simplified graphite and ivory Nintendo Switch 2 Pro Controller on a transparent background. Five simple gradients and solid vector shapes; no filters, textures, masks, shadows, fonts, or external assets. + Reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-pro-controller-123674/ . Original vector illustration; no embedded photographs or external assets. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/switch-pro-controller-simple.svg b/src/switch_pico_bridge/web/assets/switch-pro-controller-simple.svg new file mode 100644 index 0000000..9388351 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/switch-pro-controller-simple.svg @@ -0,0 +1,113 @@ + + Nintendo Switch Pro Controller — original model + Front-view original Nintendo Switch Pro Controller, with a smoky charcoal face, separate dark grip panels, black shoulder buttons, offset analog sticks, cross directional pad, Nintendo ABXY layout, capture and home buttons, and lower player indicators. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images. + Visual reference: https://www.nintendo.com/us/store/products/pro-controller/ . Independently drawn vector artwork. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/wii-remote-nunchuk-simple.svg b/src/switch_pico_bridge/web/assets/wii-remote-nunchuk-simple.svg new file mode 100644 index 0000000..c1203b4 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/wii-remote-nunchuk-simple.svg @@ -0,0 +1,149 @@ + + Wii Remote and Nunchuk — connected white controllers + Original white Nintendo Wii Remote beside a white Nunchuk, joined by a gently looping cable at the remote expansion connector. The Nunchuk shows its raised analog stick and rear C and Z buttons in a three-quarter view. Transparent background. Simple vector gradients, with no filters, textures, fonts or external assets. + Visual references: https://commons.wikimedia.org/wiki/File:Nintendo-Wii-Remote-wNunchuk.jpg and https://en.wikipedia.org/wiki/Wii_Remote . Independently drawn artwork; no embedded photographs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/wii-remote-simple.svg b/src/switch_pico_bridge/web/assets/wii-remote-simple.svg new file mode 100644 index 0000000..8d8cc27 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/wii-remote-simple.svg @@ -0,0 +1,95 @@ + + Wii Remote — original white controller + Front-view original white Nintendo Wii Remote, with power, cross directional pad, A, minus, home, plus, speaker, 1 and 2 buttons and blue player indicator. Transparent background. Simple vector gradients, with no filters, textures, fonts or external assets. + Visual references: https://commons.wikimedia.org/wiki/File:Nintendo-Wii-Remote-wNunchuk.jpg and https://en.wikipedia.org/wiki/Wii_Remote . Independently drawn artwork; no embedded photographs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/assets/xbox-controller-simple.svg b/src/switch_pico_bridge/web/assets/xbox-controller-simple.svg new file mode 100644 index 0000000..386fef3 --- /dev/null +++ b/src/switch_pico_bridge/web/assets/xbox-controller-simple.svg @@ -0,0 +1,97 @@ + + Xbox Wireless Controller — Carbon Black + Front-view Xbox Series X and Series S wireless controller, with offset analog sticks, a faceted hybrid directional pad, colored ABXY letters, Xbox, view, menu and share buttons. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images. + Visual reference: https://www.xbox.com/en-US/accessories/controllers/xbox-wireless-controller . Independently drawn vector artwork. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/switch_pico_bridge/web/controller_layouts.js b/src/switch_pico_bridge/web/controller_layouts.js new file mode 100644 index 0000000..2a325d2 --- /dev/null +++ b/src/switch_pico_bridge/web/controller_layouts.js @@ -0,0 +1,192 @@ +"use strict"; + +// Coordinates are in the supplied SVGs' original viewBoxes, including their +// offsets and nested group translations. A single stage rotates art + targets. +const ControllerLayouts = (() => { + const point = (x, y, label, glyph) => ({ x, y, label, glyph }); + const back = (label, glyph) => ({ label, glyph, offArt: true }); + const dpad = (x, y, distance) => ({ + dpad_up: point(x, y - distance, "D-pad Up", "▲"), + dpad_right: point(x + distance, y, "D-pad Right", "▶"), + dpad_down: point(x, y + distance, "D-pad Down", "▼"), + dpad_left: point(x - distance, y, "D-pad Left", "◀"), + }); + const triggers = { + left_trigger: back("Left trigger · rear", "LT"), + right_trigger: back("Right trigger · rear", "RT"), + }; + const xbox = { + name: "Xbox Wireless Controller", style: "xbox", asset: "xbox-controller-simple.svg", + viewBox: [0, 0, 800, 552], minWidth: 640, + controls: { + ...triggers, + left_shoulder: point(204, 39, "LB", "LB"), right_shoulder: point(596, 39, "RB", "RB"), + left_stick: point(204, 177, "Left stick click", "L3"), right_stick: point(501, 298, "Right stick click", "R3"), + ...dpad(295, 306, 37), + north: point(600, 131, "Y", "Y"), east: point(652, 182, "B", "B"), + south: point(600, 233, "A", "A"), west: point(548, 182, "X", "X"), + select: point(340, 180, "View", "View"), start: point(460, 180, "Menu", "Menu"), + capture: point(400, 225, "Share", "Share"), system: point(400, 93, "Xbox", "Xbox"), + }, + }; + const leftFace = { + left_stick: point(84, 139, "Left stick click", "L3"), + select: point(133, 62.5, "Minus", "−"), capture: point(119, 374, "Capture", "Cap"), + ...dpad(84, 278, 39), + }; + // The SVG's left/right directional centers are 40 units from its center. + leftFace.dpad_left.x = 44; + leftFace.dpad_right.x = 124; + const rightFace = { + north: point(80, 100, "X", "X"), west: point(40, 139, "Y", "Y"), + east: point(120, 139, "A", "A"), south: point(80, 178, "B", "B"), + right_stick: point(80, 276, "Right stick click", "R3"), + start: point(31, 63, "Plus", "+"), system: point(45, 374, "Home", "Home"), + c: point(45, 430, "C", "C"), + }; + const translate = (controls, x, y = 0) => Object.fromEntries( + Object.entries(controls).map(([id, value]) => [id, { ...value, x: value.x + x, y: value.y + y }]) + ); + const railNote = "Rail SL/SR are source-only extras and also emit the left/right shoulder source in solo mode. The physical L or R button shares that shoulder source. Each source ID is editable once; changing an extra does not disable its shoulder alias."; + const wiiCommon = { + select: point(60, 294, "Minus", "−"), system: point(100, 294, "Home", "Home"), + start: point(140, 294, "Plus", "+"), + }; + const wiiVertical = { + ...wiiCommon, ...dpad(100, 128, 25), + south: back("B · underside", "B"), east: point(100, 216, "A", "A"), + west: point(100, 445, "1", "1"), north: point(100, 495, "2", "2"), + }; + const layouts = { + generic: { ...xbox, name: "Generic controller reference", style: "generic", generic: true }, + xbox, + dualsense: { + name: "Sony DualSense", style: "playstation", asset: "ps5-dualsense-simple.svg", + viewBox: [0, 0, 800, 548], minWidth: 640, + controls: { + left_trigger: back("L2 · rear", "L2"), right_trigger: back("R2 · rear", "R2"), + left_shoulder: point(165, 32, "L1", "L1"), right_shoulder: point(635, 32, "R1", "R1"), + left_stick: point(282, 280.3, "Left stick click", "L3"), right_stick: point(518, 280.3, "Right stick click", "R3"), + ...dpad(166, 174, 39), + north: point(634, 119, "Triangle", "△"), east: point(688, 173, "Circle", "○"), + south: point(634, 227, "Cross", "×"), west: point(580, 173, "Square", "□"), + select: point(231, 92, "Create", "Create"), start: point(569, 92, "Options", "Opt"), + capture: point(400, 120, "Touchpad click", "TP"), system: point(400, 277, "PS", "PS"), + }, + note: "The microphone mute button is not exposed as a remappable source. Rear triggers are listed below the front view.", + }, + "switch-pro": { + name: "Nintendo Switch Pro Controller", style: "switch", asset: "switch-pro-controller-simple.svg", + viewBox: [0, 0, 800, 570], minWidth: 640, + controls: { + left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"), + left_shoulder: point(205, 32, "L", "L"), right_shoulder: point(595, 32, "R", "R"), + left_stick: point(190, 179, "Left stick click", "L3"), right_stick: point(500, 304, "Right stick click", "R3"), + ...dpad(294, 306, 40), + north: point(610, 137, "X", "X"), east: point(662, 189, "A", "A"), + south: point(610, 241, "B", "B"), west: point(558, 189, "Y", "Y"), + select: point(301, 118, "Minus", "−"), start: point(499, 118, "Plus", "+"), + capture: point(349, 180, "Capture", "Cap"), system: point(451, 180, "Home", "Home"), + }, + }, + "switch2-pro": { + name: "Nintendo Switch 2 Pro Controller", style: "switch", asset: "switch-2-pro-controller-simple.svg", + viewBox: [-16, 160, 1232, 884], minWidth: 720, + controls: { + left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"), + gl: back("GL · back button", "GL"), gr: back("GR · back button", "GR"), + left_shoulder: point(290, 193, "L", "L"), right_shoulder: point(910, 193, "R", "R"), + left_stick: point(272, 434, "Left stick click", "L3"), right_stick: point(746, 605, "Right stick click", "R3"), + ...dpad(424, 602, 60), + north: point(909, 350, "X", "X"), east: point(1005, 433, "A", "A"), + south: point(909, 517, "B", "B"), west: point(813, 433, "Y", "Y"), + select: point(454, 341, "Minus", "−"), start: point(743, 341, "Plus", "+"), + capture: point(526, 434, "Capture", "Cap"), system: point(672, 435, "Home", "Home"), + c: point(599, 693, "C", "C"), + }, + note: "C is on the front. GL and GR are rear controls, not front-face buttons. C, GL and GR are source-only inputs.", + }, + "joycon2-pair": { + name: "Joy-Con 2 pair", style: "switch", asset: "switch-2-joycons-connected.svg", + viewBox: [-8, -10, 688, 560], minWidth: 620, + controls: { + ...translate(leftFace, 64), ...translate(rightFace, 444), + left_shoulder: back("L · top edge", "L"), right_shoulder: back("R · top edge", "R"), + left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"), + left_sl: back("Left SL · rail", "L SL"), left_sr: back("Left SR · rail", "L SR"), + right_sl: back("Right SL · rail", "R SL"), right_sr: back("Right SR · rail", "R SR"), + }, + note: "Paired inputs use the left controller's profile. The grip is illustrative, not detected. Rear triggers and rails are not visible in this front view. C and rail inputs are source-only; GL/GR belong to Switch 2 Pro, not Joy-Con 2.", + }, + "joycon2-left": { + name: "Joy-Con 2 left · sideways solo", style: "switch", asset: "switch-2-joycon-left.svg", + viewBox: [-17, -10, 200, 560], rotation: -90, minWidth: 560, + controls: { + left_stick: leftFace.left_stick, select: leftFace.select, capture: leftFace.capture, + south: { ...leftFace.dpad_left, label: "Left arrow (solo south)", glyph: "▼" }, + east: { ...leftFace.dpad_down, label: "Down arrow (solo east)", glyph: "▶" }, + west: { ...leftFace.dpad_up, label: "Up arrow (solo west)", glyph: "◀" }, + north: { ...leftFace.dpad_right, label: "Right arrow (solo north)", glyph: "▲" }, + left_shoulder: back("L / SL · shoulder alias", "L / SL"), right_shoulder: back("SR · shoulder alias", "SR"), + left_trigger: back("ZL · rear", "ZL"), + left_sl: back("SL · rail extra", "SL extra"), left_sr: back("SR · rail extra", "SR extra"), + }, + note: "Solo firmware rotates the directional buttons into face-button sources and keeps the stick on LEFT axes. Arrow names refer to the original upright art. " + railNote, + }, + "joycon2-right": { + name: "Joy-Con 2 right · sideways solo", style: "switch", asset: "switch-2-joycon-single.svg", + viewBox: [-19, -10, 200, 560], rotation: 90, minWidth: 560, + controls: { + south: { ...rightFace.east, label: "A (solo south)" }, east: { ...rightFace.north, label: "X (solo east)" }, + west: { ...rightFace.south, label: "B (solo west)" }, north: { ...rightFace.west, label: "Y (solo north)" }, + left_stick: { ...rightFace.right_stick, label: "Right stick · solo LEFT axes/click", glyph: "L3" }, + start: rightFace.start, system: rightFace.system, c: rightFace.c, + left_shoulder: back("SL · shoulder alias", "SL"), right_shoulder: back("R / SR · shoulder alias", "R / SR"), + right_trigger: back("ZR · rear", "ZR"), + right_sl: back("SL · rail extra", "SL extra"), right_sr: back("SR · rail extra", "SR extra"), + }, + note: "Solo firmware rotates ABXY sources; the right stick becomes LEFT axes and left-stick click. Output labels do not rotate. When paired, select the left controller's profile to edit the pair. " + railNote, + }, + "wii-remote": { + name: "Wii Remote · orientation unknown", style: "switch", asset: "wii-remote-simple.svg", + viewBox: [0, 0, 200, 632], minWidth: 240, maxWidth: 260, + controls: { + ...wiiCommon, + ...Object.fromEntries(["south", "east", "west", "north", "dpad_up", "dpad_right", "dpad_down", "dpad_left"].map(id => [id, back("Logical " + id.replaceAll("_", " "), id.startsWith("dpad") ? id.slice(5) : id)])), + }, + note: "Firmware reports a Wii Remote, not its orientation. Only Minus, Plus and Home can be placed unambiguously. Choose an explicit horizontal/accelerometer or vertical preview to place the other sources. Power is not remappable.", + }, + "wii-horizontal": { + name: "Wii Remote · horizontal / accelerometer", style: "switch", asset: "wii-remote-simple.svg", + viewBox: [0, 0, 200, 632], rotation: -90, minWidth: 760, + controls: { + ...wiiCommon, + south: point(100, 445, "1", "1"), east: point(100, 495, "2", "2"), + west: point(100, 216, "A", "A"), north: back("B · underside", "B"), + dpad_left: point(100, 103, "D-pad physical Up → Left", "◀"), + dpad_up: point(125, 128, "D-pad physical Right → Up", "▲"), + dpad_right: point(100, 153, "D-pad physical Down → Right", "▶"), + dpad_down: point(75, 128, "D-pad physical Left → Down", "▼"), + }, + note: "Explicit orientation preview, not detected: 1→south, 2→east, A→west, B→north. D-pad sources rotate counterclockwise. B is underneath; Power is not remappable.", + }, + "wii-vertical": { + name: "Wii Remote · vertical", style: "switch", asset: "wii-remote-simple.svg", + viewBox: [0, 0, 200, 632], minWidth: 240, maxWidth: 260, controls: wiiVertical, + note: "Explicit orientation preview, not detected: B→south, A→east, 1→west, 2→north. D-pad directions stay upright. B is underneath; Power is not remappable.", + }, + "wii-nunchuk": { + name: "Wii Remote + Nunchuk", style: "switch", asset: "wii-remote-nunchuk-simple.svg", + viewBox: [0, 0, 540, 764], minWidth: 600, + controls: { + ...translate({ ...wiiCommon, ...dpad(100, 128, 25), east: wiiVertical.east }, 320, 10), + south: back("B · Remote underside", "B"), + left_shoulder: point(420, 455, "1", "1"), right_shoulder: point(420, 505, "2", "2"), + west: back("Nunchuk C · rear", "C"), north: back("Nunchuk Z · rear", "Z"), + }, + annotations: [{ x: 233, y: 220.4, label: "Stick → RIGHT axes (no click)" }], + note: "Nunchuk stick uses RIGHT axes and has no click. B→south, A→east, 1/2→left/right shoulder, Nunchuk C→west and Z→north. Nunchuk C is a normal face source, not the Switch 2 C extra. Power is not remappable.", + }, + }; + return layouts; +})(); diff --git a/src/switch_pico_bridge/web/profile_editor.css b/src/switch_pico_bridge/web/profile_editor.css index 6506256..0111b45 100644 --- a/src/switch_pico_bridge/web/profile_editor.css +++ b/src/switch_pico_bridge/web/profile_editor.css @@ -293,6 +293,7 @@ h4 { font-size: 1rem; } .panel-heading > p { max-width: 440px; color: var(--muted); font-size: 0.78rem; line-height: 1.55; text-align: right; } .controller-mapper { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 18px; align-items: stretch; } .controller-canvas { + min-width: 0; padding: 18px; overflow: hidden; border: 1px solid rgba(89, 199, 255, 0.24); @@ -300,16 +301,20 @@ h4 { font-size: 1rem; } background: #090f1d; box-shadow: inset 0 1px rgba(255, 255, 255, 0.04); } -.controller-caption { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 27px; } +.controller-caption { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; min-height: 27px; } .controller-caption strong { color: #dfe8ff; font-size: 0.8rem; } -.controller-live { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font-size: 0.65rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; } -.controller-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 0 4px rgba(74, 222, 165, 0.09), 0 0 12px var(--mint); } -.controller-photo-wrap { position: relative; width: 100%; margin: 10px auto 4px; aspect-ratio: 1.62; transition: aspect-ratio 180ms ease; } -.controller-canvas[data-style="switch"] .controller-photo-wrap { aspect-ratio: 1.38; } -.controller-canvas[data-style="playstation"] .controller-photo-wrap { aspect-ratio: 1.8; } -.controller-photo { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; user-select: none; filter: drop-shadow(0 22px 28px rgba(0, 0, 0, 0.46)); } +.controller-live { display: inline-flex; align-items: center; gap: 7px; color: var(--amber); font-size: 0.65rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; } +.controller-live[data-state="live"] { color: var(--mint); } +.layout-picker { margin-block: 16px 8px; } +.layout-picker .select { width: 100%; margin-top: 6px; } +.controller-viewport { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 16px 0; scrollbar-color: var(--subtle) var(--surface-2); } +.controller-viewport:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; } +.controller-photo-wrap { --diagram-min: 640px; --diagram-max: 900px; position: relative; width: 100%; min-width: var(--diagram-min); max-width: var(--diagram-max); margin: 0 auto; aspect-ratio: var(--diagram-ratio, 800 / 552); } +.controller-stage { position: absolute; top: 50%; left: 50%; width: var(--stage-width, 100%); height: var(--stage-height, 100%); transform: translate(-50%, -50%) rotate(var(--stage-angle, 0deg)); } +.controller-photo { display: block; width: 100%; height: 100%; pointer-events: none; user-select: none; filter: drop-shadow(0 16px 20px rgba(0, 0, 0, 0.46)); } +.controller-annotation { position: absolute; left: var(--x); top: var(--y); max-width: 170px; padding: 6px; transform: translate(-50%, -50%) rotate(var(--label-angle, 0deg)); border: 1px solid var(--muted); border-radius: 6px; color: var(--ink); background: var(--surface-2); font-size: 0.65rem; text-align: center; pointer-events: none; } .extra-controls { display: flex; flex-wrap: wrap; gap: 9px; margin: 10px 0; } -.extra-controls button { min-width: 48px; padding: 9px 12px; color: var(--ink); border: 1px solid var(--sky); border-radius: 9px; background: var(--surface-2); font-size: 0.72rem; font-weight: 800; cursor: pointer; } +.extra-controls button { min-height: 44px; min-width: 48px; padding: 9px 12px; color: var(--ink); border: 1px solid var(--sky); border-radius: 9px; background: var(--surface-2); font-size: 0.72rem; font-weight: 800; cursor: pointer; } .extra-controls button.disabled-map { color: var(--muted); border-color: var(--control-line); } .extra-controls button.selected, .extra-controls button:focus-visible { outline: 2px solid var(--sky); outline-offset: 3px; } .extra-controls button.pressed { color: #090f1d; background: var(--amber); border-color: var(--amber); } @@ -324,90 +329,28 @@ h4 { font-size: 1rem; } top: var(--y); display: grid; place-items: center; - width: 34px; - height: 34px; + width: 30px; + height: 30px; padding: 0; color: #eff6ff; border: 2px solid var(--sky); border-radius: 50%; background: rgba(7, 12, 25, 0.9); box-shadow: 0 0 0 4px rgba(89, 199, 255, 0.12), 0 5px 16px rgba(0, 0, 0, 0.42); - transform: translate(-50%, -50%); - font-size: 0.7rem; + transform: translate(-50%, -50%) rotate(var(--label-angle, 0deg)); + font-size: 0.59rem; font-weight: 900; line-height: 1; transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, box-shadow 120ms ease; } -.controller-hotspots button:hover, .controller-hotspots button:focus-visible { z-index: 2; border-color: var(--blue); transform: translate(-50%, -50%) scale(1.13); } -.controller-hotspots button.selected { z-index: 3; color: #090a16; border-color: var(--rose); background: var(--rose); box-shadow: 0 0 0 5px rgba(255, 124, 159, 0.18), 0 8px 20px rgba(0, 0, 0, 0.48); transform: translate(-50%, -50%) scale(1.16); } -.controller-hotspots button[data-controller-button$="_trigger"] { width: 60px; height: 30px; border-radius: 11px; } -.controller-hotspots button.disabled-map { color: var(--subtle); border-color: #56617a; background: rgba(8, 11, 20, 0.9); box-shadow: none; } -.controller-hotspots button[data-controller-button$="_shoulder"] { width: 60px; height: 30px; border-radius: 11px; } -.controller-hotspots button[data-controller-button^="dpad_"] { width: 27px; height: 27px; font-size: 0.58rem; } -.controller-hotspots button[data-controller-button="select"], -.controller-hotspots button[data-controller-button="start"], -.controller-hotspots button[data-controller-button="capture"], -.controller-hotspots button[data-controller-button="system"] { width: 29px; height: 29px; font-size: 0.63rem; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_shoulder"] { --x: 22%; --y: 12%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_trigger"] { --x: 22%; --y: 4%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_trigger"] { --x: 78%; --y: 4%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_shoulder"] { --x: 78%; --y: 12%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_stick"] { --x: 23%; --y: 43%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_stick"] { --x: 63%; --y: 68%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_up"] { --x: 36%; --y: 62%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_right"] { --x: 40%; --y: 68%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_down"] { --x: 36%; --y: 74%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_left"] { --x: 32%; --y: 68%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="north"] { --x: 77%; --y: 33%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="east"] { --x: 84%; --y: 43%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="south"] { --x: 77%; --y: 54%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="west"] { --x: 70%; --y: 43%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="select"] { --x: 42%; --y: 43%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="start"] { --x: 57%; --y: 43%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="capture"] { --x: 50%; --y: 52%; } -.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="system"] { --x: 50%; --y: 26%; } - -.controller-canvas[data-style="switch"] [data-controller-button="left_shoulder"] { --x: 27%; --y: 8%; } -.controller-canvas[data-style="switch"] [data-controller-button="right_shoulder"] { --x: 73%; --y: 8%; } -.controller-canvas[data-style="switch"] [data-controller-button="left_trigger"] { --x: 27%; --y: 2%; } -.controller-canvas[data-style="switch"] [data-controller-button="right_trigger"] { --x: 73%; --y: 2%; } -.controller-canvas[data-style="switch"] [data-controller-button="left_stick"] { --x: 24%; --y: 30%; } -.controller-canvas[data-style="switch"] [data-controller-button="right_stick"] { --x: 63%; --y: 51%; } -.controller-canvas[data-style="switch"] [data-controller-button="dpad_up"] { --x: 35%; --y: 46%; } -.controller-canvas[data-style="switch"] [data-controller-button="dpad_right"] { --x: 39%; --y: 51%; } -.controller-canvas[data-style="switch"] [data-controller-button="dpad_down"] { --x: 35%; --y: 56%; } -.controller-canvas[data-style="switch"] [data-controller-button="dpad_left"] { --x: 31%; --y: 51%; } -.controller-canvas[data-style="switch"] [data-controller-button="north"] { --x: 77%; --y: 22%; } -.controller-canvas[data-style="switch"] [data-controller-button="east"] { --x: 85%; --y: 32%; } -.controller-canvas[data-style="switch"] [data-controller-button="south"] { --x: 77%; --y: 43%; } -.controller-canvas[data-style="switch"] [data-controller-button="west"] { --x: 69%; --y: 32%; } -.controller-canvas[data-style="switch"] [data-controller-button="select"] { --x: 39%; --y: 21%; } -.controller-canvas[data-style="switch"] [data-controller-button="start"] { --x: 61%; --y: 21%; } -.controller-canvas[data-style="switch"] [data-controller-button="capture"] { --x: 44%; --y: 31%; } -.controller-canvas[data-style="switch"] [data-controller-button="system"] { --x: 57%; --y: 32%; } - -.controller-canvas[data-style="playstation"] [data-controller-button="left_shoulder"] { --x: 21%; --y: 12%; } -.controller-canvas[data-style="playstation"] [data-controller-button="right_shoulder"] { --x: 79%; --y: 12%; } -.controller-canvas[data-style="playstation"] [data-controller-button="left_stick"] { --x: 34%; --y: 69%; } -.controller-canvas[data-style="playstation"] [data-controller-button="left_trigger"] { --x: 21%; --y: 3%; } -.controller-canvas[data-style="playstation"] [data-controller-button="right_trigger"] { --x: 79%; --y: 3%; } -.controller-canvas[data-style="playstation"] [data-controller-button="right_stick"] { --x: 66%; --y: 69%; } -.controller-canvas[data-style="playstation"] [data-controller-button="dpad_up"] { --x: 18%; --y: 42%; } -.controller-canvas[data-style="playstation"] [data-controller-button="dpad_right"] { --x: 24%; --y: 50%; } -.controller-canvas[data-style="playstation"] [data-controller-button="dpad_down"] { --x: 18%; --y: 58%; } -.controller-canvas[data-style="playstation"] [data-controller-button="dpad_left"] { --x: 12%; --y: 50%; } -.controller-canvas[data-style="playstation"] [data-controller-button="north"] { --x: 83%; --y: 39%; } -.controller-canvas[data-style="playstation"] [data-controller-button="east"] { --x: 91%; --y: 50%; } -.controller-canvas[data-style="playstation"] [data-controller-button="south"] { --x: 83%; --y: 61%; } -.controller-canvas[data-style="playstation"] [data-controller-button="west"] { --x: 75%; --y: 50%; } -.controller-canvas[data-style="playstation"] [data-controller-button="select"] { --x: 25%; --y: 30%; } -.controller-canvas[data-style="playstation"] [data-controller-button="start"] { --x: 75%; --y: 30%; } -.controller-canvas[data-style="playstation"] [data-controller-button="capture"] { --x: 50%; --y: 36%; } -.controller-canvas[data-style="playstation"] [data-controller-button="system"] { --x: 50%; --y: 72%; } +.controller-hotspots button:hover, .controller-hotspots button:focus-visible { z-index: 2; outline: 2px solid var(--blue); outline-offset: 2px; } +.controller-hotspots button.selected { z-index: 3; color: #090a16; border-color: var(--rose); background: var(--rose); box-shadow: 0 0 0 4px rgba(255, 124, 159, 0.18); } +.controller-hotspots button.disabled-map:not(.selected):not(.pressed) { color: var(--subtle); border-color: #56617a; background: rgba(8, 11, 20, 0.9); box-shadow: none; } +.controller-hotspots button[data-controller-button^="dpad_"] { width: 26px; height: 26px; font-size: 0.58rem; } +.source-unavailable { color: var(--amber); } .controller-hint { margin-top: 4px; color: var(--muted); font-size: 0.72rem; text-align: center; } .controller-credit { margin-top: 8px; color: var(--subtle); font-size: 0.64rem; text-align: center; } -.controller-credit a { color: var(--muted); text-decoration-color: rgba(171, 181, 204, 0.45); text-underline-offset: 3px; } .mapping-inspector { display: flex; flex-direction: column; diff --git a/src/switch_pico_bridge/web/profile_editor.html b/src/switch_pico_bridge/web/profile_editor.html index 0334682..696a4b4 100644 --- a/src/switch_pico_bridge/web/profile_editor.html +++ b/src/switch_pico_bridge/web/profile_editor.html @@ -110,49 +110,45 @@
- Connected layout + Reference layout Generic controller
-
- Connected controller -
- - - - - - - - - - - - - - - - - - +
+ + +

Preview changes only this editor's source labels and diagram, never firmware topology or saved mappings.

+
+
+
+
+ Generic controller reference +
+
-

Select a highlighted control on the controller to remap it.

-

Switch 2 extra inputs

+

Select a control on the illustration or below it. On narrow screens, swipe sideways or use the Source control menu.

+

Off-art controls · rear, rails or unlocated sources

-

C, back and rail buttons are source-only controls. Unmapped extra inputs produce no console output.

-

- Image: - Controller artwork by Al. Lopez · MIT -

+

+

Original vector artwork supplied for Switch Pico Controller Studio.