chore(architecture): enforce layer boundaries
This commit is contained in:
9
input/AGENTS.md
Normal file
9
input/AGENTS.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Input Rules
|
||||
|
||||
- `input/` resolves keyboard, pointer, mouse, touch, pen, and wheel input only.
|
||||
- Resolution order is strict: global consumer first, command fallback second.
|
||||
- If the global consumer handles input, stop and do not dispatch a command.
|
||||
- Input handlers must never mutate state directly.
|
||||
- Input may dispatch commands by id with payloads.
|
||||
- Keep this layer independent from React, renderer, editor store internals, and persisted core domain models.
|
||||
- DOM/browser events should be normalized into small input event types before command mapping.
|
||||
3
input/index.ts
Normal file
3
input/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
|
||||
export { handleKeybind, keybindFromEvent } from "./keyboard";
|
||||
export type { GlobalPointerConsumer, GlobalWheelConsumer, PointerInputEvent, WheelInputEvent } from "./pointer";
|
||||
48
input/keyboard.ts
Normal file
48
input/keyboard.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Dispatch } from "@commands/dispatcher";
|
||||
|
||||
export type Keybind = string;
|
||||
|
||||
export type KeybindEvent = {
|
||||
key: string;
|
||||
code: string;
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
shiftKey: boolean;
|
||||
};
|
||||
|
||||
export type GlobalKeybindConsumer = (event: KeybindEvent) => boolean;
|
||||
|
||||
export type CommandKeybind = {
|
||||
commandId: string;
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
export type KeybindMap = ReadonlyMap<Keybind, CommandKeybind>;
|
||||
|
||||
export function keybindFromEvent(event: KeybindEvent): Keybind {
|
||||
const parts = [
|
||||
event.metaKey ? "Meta" : undefined,
|
||||
event.ctrlKey ? "Ctrl" : undefined,
|
||||
event.altKey ? "Alt" : undefined,
|
||||
event.shiftKey ? "Shift" : undefined,
|
||||
event.key,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.join("+");
|
||||
}
|
||||
|
||||
export function handleKeybind(options: {
|
||||
event: KeybindEvent;
|
||||
globalConsumer: GlobalKeybindConsumer;
|
||||
keybindMap: KeybindMap;
|
||||
dispatch: Dispatch;
|
||||
}): boolean {
|
||||
if (options.globalConsumer(options.event)) return true;
|
||||
|
||||
const command = options.keybindMap.get(keybindFromEvent(options.event));
|
||||
if (!command) return false;
|
||||
|
||||
options.dispatch(command.commandId, command.payload);
|
||||
return true;
|
||||
}
|
||||
24
input/pointer.ts
Normal file
24
input/pointer.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Vec2D } from "@core/geometry";
|
||||
|
||||
export type PointerInputEvent = {
|
||||
pointerId: number;
|
||||
pointerType: "mouse" | "pen" | "touch";
|
||||
position: Vec2D;
|
||||
buttons: number;
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
shiftKey: boolean;
|
||||
};
|
||||
|
||||
export type WheelInputEvent = {
|
||||
position: Vec2D;
|
||||
delta: Vec2D;
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
shiftKey: boolean;
|
||||
};
|
||||
|
||||
export type GlobalPointerConsumer = (event: PointerInputEvent) => boolean;
|
||||
export type GlobalWheelConsumer = (event: WheelInputEvent) => boolean;
|
||||
Reference in New Issue
Block a user