feat: device flip

This commit is contained in:
2026-03-31 19:40:01 +02:00
parent 04540a55ef
commit 07491b9741
2 changed files with 128 additions and 78 deletions

View File

@@ -14,7 +14,6 @@
CompiledLayout, CompiledLayout,
CompiledLayoutKey, CompiledLayoutKey,
} from "$lib/assets/layouts/layout.d.ts"; } from "$lib/assets/layouts/layout.d.ts";
import { setting } from "$lib/setting.js";
const { scale, margin, strokeWidth, fontSize, iconFontSize } = const { scale, margin, strokeWidth, fontSize, iconFontSize } =
getContext<VisualLayoutConfig>("visual-layout-config"); getContext<VisualLayoutConfig>("visual-layout-config");
@@ -120,9 +119,13 @@
} }
function edit(index: number) { function edit(index: number) {
const keyInfo = layoutInfo.keys[index]; const keyInfo =
layoutInfo.keys.find(({ id }) => id === index) ??
layoutInfo.fixedKeys.find(({ id }) => id === index);
if (!keyInfo) return; if (!keyInfo) return;
const clickedGroup = groupParent.children.item(index) as SVGGElement; const clickedGroup = groupParent.querySelector(
`g[data-id="${index}"]`,
) as SVGGElement;
const nextAction = const nextAction =
get(layout)[get(activeProfile)]![get(activeLayer)]?.[keyInfo.id]; get(layout)[get(activeProfile)]![get(activeLayer)]?.[keyInfo.id];
const currentAction = const currentAction =
@@ -190,7 +193,8 @@
} }
let focusKey: CompiledLayoutKey; let focusKey: CompiledLayoutKey;
let groupParent: SVGElement; let groupParent: SVGGElement;
let rotationTarget: SVGCircleElement;
let rotationSetting = $derived( let rotationSetting = $derived(
$deviceMeta?.settings $deviceMeta?.settings
.find((it) => it.name === "misc") .find((it) => it.name === "misc")
@@ -201,26 +205,43 @@
? ($settings[$activeProfile]?.[rotationSetting.id]?.value ?? 90) ? ($settings[$activeProfile]?.[rotationSetting.id]?.value ?? 90)
: 90, : 90,
); );
let flippedSetting = $derived(
$deviceMeta?.settings
.find((it) => it.name === "misc")
?.items.find((it) => it.name === "device orientation"),
);
let flipped = $derived(
flippedSetting
? $settings[$activeProfile]?.[flippedSetting.id]?.value !== 0
: false,
);
let draggingRotation = $state(90); let draggingRotation = $state(90);
let isDragging = $state(false); let isDragging = $state(false);
let rotation = $derived(isDragging ? draggingRotation : settingRotation); let rotation = $derived(isDragging ? draggingRotation : settingRotation);
let dragOffset = 0; let dragOffset = 0;
function calcDragOffset(event: MouseEvent) { function calcDragOffset(event: MouseEvent) {
const offset = groupParent.getBoundingClientRect(); const offset = rotationTarget.getBoundingClientRect();
const centerX = const cx = offset.x + offset.width / 2;
offset.x + const cy = offset.y + offset.height / 2;
(layoutInfo.rotationAnchor?.[0] ?? 0) * const a = Math.atan2(event.x - cx, event.y - cy) * (180 / Math.PI) + 90;
scale * return flipped ? (a + 180) % 360 : a;
(offset.width / (layoutInfo.size[0] * scale)); }
const centerY =
offset.y + function toggleFlip() {
(layoutInfo.rotationAnchor?.[1] ?? 0) * changes.update((changes) => {
scale * changes.push([
(offset.height / (layoutInfo.size[1] * scale)); {
return ( type: ChangeType.Setting,
Math.atan2(event.x - centerX, event.y - centerY) * (180 / Math.PI) + 90 id: flippedSetting!.id,
); setting: flipped ? 0 : 1,
profile: get(activeProfile),
},
]);
return changes;
});
} }
function dragRotation(event: MouseEvent) { function dragRotation(event: MouseEvent) {
@@ -261,73 +282,96 @@
<svg <svg
class="print" class="print"
viewBox="0 0 {layoutInfo.size[0] * scale} {layoutInfo.size[1] * scale}" viewBox="0 {flipped ? margin * -3 : 0} {layoutInfo.size[0] *
bind:this={groupParent} scale} {layoutInfo.size[1] * scale}"
transition:fly={{ y: 48, easing: expoOut }} transition:fly={{ y: 48, easing: expoOut }}
onmousemove={dragRotation} onmousemove={dragRotation}
onmouseup={dragDisable} onmouseup={dragDisable}
> >
<g <g
bind:this={groupParent}
transform="rotate({flipped ? 180 : 0})"
transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) * transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
scale} {(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}" scale} {(layoutInfo.size[1] * scale) / 2}"
transform="rotate({-(rotation - 90)})"
class="group"
> >
{#each layoutInfo.keys as key, i} <g
<KeyboardKey transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
{i} scale} {(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}"
{key} transform="rotate({-(rotation - 90)})"
onfocusin={() => (focusKey = key)} class="group"
onclick={() => edit(i)} >
onkeypress={({ key }) => { {#each layoutInfo.keys as key}
if (key === "Enter") { <KeyboardKey
edit(i); {key}
} {flipped}
}} onfocusin={() => (focusKey = key)}
/> onclick={() => edit(key.id)}
{/each} onkeypress={(event) => {
{#if rotationSetting} if (event.key === "Enter") {
<rect edit(key.id);
role="button" }
tabindex="-1" }}
onmousedown={dragEnable} />
class="handle" {/each}
x={(layoutInfo.size[0] * scale) / 2 - (0.5 * scale) / 2} {#if rotationSetting}
y={layoutInfo.size[1] * scale + margin - 0.05 * scale} <rect
width={0.5 * scale} role="button"
height={0.05 * scale} tabindex="-1"
ry={0.025 * scale} onmousedown={dragEnable}
fill="currentColor" ondblclick={toggleFlip}
stroke="currentColor" class="handle"
stroke-width={strokeWidth} x={(layoutInfo.size[0] * scale) / 2 - (0.5 * scale) / 2}
/> y={layoutInfo.size[1] * scale + margin - 0.05 * scale}
{#if isDragging} width={0.5 * scale}
<text height={0.05 * scale}
transition:fly={{ y: 2, easing: expoOut }} ry={0.025 * scale}
class="handle-label"
text-anchor="middle"
font-size={fontSize}
fill="currentColor" fill="currentColor"
x={(layoutInfo.size[0] * scale) / 2} stroke="currentColor"
y={layoutInfo.size[1] * scale + margin * 3}>{rotation - 90}°</text stroke-width={strokeWidth}
> />
{#if isDragging}
{@const x = (layoutInfo.size[0] * scale) / 2}
{@const y = layoutInfo.size[1] * scale + margin * (flipped ? 2 : 3)}
<text
transition:fly={{ y: 2, easing: expoOut }}
transform={flipped ? "rotate(180)" : undefined}
transform-origin="{x} {y}"
class="handle-label"
text-anchor="middle"
font-size={fontSize}
fill="currentColor"
{x}
{y}>{rotation - 90}°</text
>
{/if}
{/if} {/if}
{/if} </g>
</g> <circle
bind:this={rotationTarget}
{#each layoutInfo.fixedKeys as key, i} cx={(layoutInfo.rotationAnchor?.[0] ?? 0) * scale}
<KeyboardKey cy={(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}
{i} r="0"
{key}
onfocusin={() => (focusKey = key)}
onclick={() => edit(i)}
onkeypress={({ key }) => {
if (key === "Enter") {
edit(i);
}
}}
/> />
{/each}
<g
transform="rotate({flipped ? 180 : 0})"
transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
scale} {(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}"
>
{#each layoutInfo.fixedKeys as key}
<KeyboardKey
{key}
onfocusin={() => (focusKey = key)}
onclick={() => edit(key.id)}
onkeypress={(event) => {
if (event.key === "Enter") {
edit(key.id);
}
}}
/>
{/each}
</g>
</g>
</svg> </svg>
<style lang="scss"> <style lang="scss">

View File

@@ -17,14 +17,14 @@
const highlight = getContext<Writable<Set<number>> | undefined>("highlight"); const highlight = getContext<Writable<Set<number>> | undefined>("highlight");
let { let {
i,
key, key,
flipped = false,
onclick, onclick,
onkeypress, onkeypress,
onfocusin, onfocusin,
}: { }: {
i: number;
key: CompiledLayoutKey; key: CompiledLayoutKey;
flipped?: boolean;
onclick: MouseEventHandler<SVGGElement>; onclick: MouseEventHandler<SVGGElement>;
onkeypress: KeyboardEventHandler<SVGGElement>; onkeypress: KeyboardEventHandler<SVGGElement>;
onfocusin: FocusEventHandler<SVGGElement>; onfocusin: FocusEventHandler<SVGGElement>;
@@ -44,7 +44,10 @@
{onkeypress} {onkeypress}
{onfocusin} {onfocusin}
role="button" role="button"
tabindex={i + 1} tabindex={key.id + 1}
transform={flipped ? "rotate(180)" : undefined}
transform-origin="center"
data-id={key.id}
> >
{#if key.shape === "square"} {#if key.shape === "square"}
<rect <rect
@@ -122,6 +125,9 @@
path, path,
g { g {
transform-box: fill-box; transform-box: fill-box;
}
path {
transform-origin: top left; transform-origin: top left;
} }