Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ source and release history remains available from Git commits and version tags.

## Next

- Closed empty-canvas press as `clear`, and Escape as one layer at a time:
cancel an open gesture, then clear selection. `pointercancel` still
cancels only the gesture.
- Closed plane hit selection: pressing an already selected object keeps
the set so a drag moves every selected object. Canvas Hands apply
`planeHitAffordance`.
- Attached typeahead to Order Hands and marquee, pan, snap, nudge, and
escape to Canvas Hands. Those catalog rows leave TBD.
- Opened Affordance as the product-facing keyboard and mouse contract, with
usage and API for select, fold, drag, and undo/redo. Live widget screens
stay as proofs, not the catalog entrance.
Expand Down
53 changes: 27 additions & 26 deletions docs/public/affordance-cancel.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
# Escape

TBD.

Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape 키는 메뉴·대화·
드래그를 닫고, `pointercancel`은 포인터 제스처를 중단합니다.
Escape는 안쪽 손부터 한 겹씩 닫습니다. 끌어 옮기는 중이면 그 손을 버리고,
열린 손이 없으면 고른 것을 지웁니다. `pointercancel`은 포인터 제스처만
중단하고 선택은 건드리지 않습니다.

```ts
import {
applyAffordance,
escapeAffordance,
} from "@interactive-os/json-document-affordance";
import { applyAffordance, escapeAffordance } from "@interactive-os/json-document-affordance";

function onKeyDown(event: KeyboardEvent) {
applyAffordance(escapeAffordance(event), {
hand: (hand) => {
if (hand.type !== "cancel") return;
setMarquee(null);
setDrag(null);
},
});
}

function onPointerCancel(event: PointerEvent) {
applyAffordance(escapeAffordance(event), {
hand: (hand) => {
if (hand.type === "cancel") setDrag(null);
applyAffordance(
escapeAffordance({
key: event.key,
grabbing: drag != null || marquee != null,
selected: editor.selectedIds.length > 0,
}),
{
hand: (hand) => {
if (hand.type === "cancel") {
setDrag(null);
setMarquee(null);
return;
}
if (hand.type === "clear") {
editor.dispatch({ type: "selection.set", objectIds: [], mode: "replace" });
}
},
},
});
);
}
```

호스트는 무엇이 열려 있는지를 가집니다. 버리는 손은 보통 호스트 화면
상태입니다. 문서 값은 바꾸지 않습니다.
상태입니다. 선택은 `clear`로 json-document에 갑니다.

닫는 손:
- Escape: 메뉴, 대화, 드래그, Rename 중단
- pointercancel / lostpointercapture: 포인터 제스처 중단
- 제스처가 열려 있으면 `cancel` (드래그, 마키, 팬, 타입어헤드 버퍼, 메뉴, Rename)
- 제스처가 없고 고른 것이 있으면 `clear`
- `pointercancel` / `lostpointercapture`: 항상 `cancel`

근거: [APG Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), [Pointer Events](https://www.w3.org/TR/pointerevents/)
근거: [APG Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), [Pointer Events](https://www.w3.org/TR/pointerevents/), Finder/Figma/tldraw의 Escape 한 겹 닫기
22 changes: 7 additions & 15 deletions docs/public/affordance-marquee.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Marquee

TBD.

Marquee는 빈 평면에서 사각형을 끌어 여러 대상을 집는 손입니다. 고른
대상을 옮기는 [Drag](affordance-drag.md)와 다릅니다. `crosshair` / `cell`
커서가 이 손을 가리킬 수 있습니다.
Expand All @@ -11,12 +9,11 @@ import {
applyAffordance,
commitAffordance,
marqueeAffordance,
pointerSelect,
} from "@interactive-os/json-document-affordance";

function onPointerMove(event: PointerEvent) {
const point = { x: event.offsetX, y: event.offsetY };
applyAffordance(marqueeAffordance(origin, point), {
applyAffordance(marqueeAffordance(origin, point, event), {
cursor: (cursor) => {
event.currentTarget.style.cursor = cursor;
},
Expand All @@ -27,20 +24,15 @@ function onPointerMove(event: PointerEvent) {
}

function onPointerUp(event: PointerEvent) {
const committed = commitAffordance(marqueeAffordance(origin, { x: event.offsetX, y: event.offsetY }));
const committed = commitAffordance(marqueeAffordance(origin, { x: event.offsetX, y: event.offsetY }, event));
if (!committed) return;
applyAffordance(committed, {
commit: (hand) => {
if (hand.type !== "select" || !hand.rect) return;
applyAffordance(pointerSelect(event), {
hand: (selectHand) => {
if (selectHand.type !== "select") return;
editor.dispatch({
type: "selection.set",
objectIds: hostHits(hand.rect),
mode: selectHand.operation,
});
},
editor.dispatch({
type: "selection.set",
objectIds: hostHits(hand.rect),
mode: hand.operation === "extend" ? "add" : hand.operation,
});
},
});
Expand All @@ -49,7 +41,7 @@ function onPointerUp(event: PointerEvent) {

호스트는 히트 테스트와 기하를 계산합니다. 어떤 키가 범위에 들어오는지는
호스트가 보고, 손이 replace인지 extend인지는 Affordance가 닫고, 선택은
json-document로 갑니다.
json-document로 갑니다. 이동이 없는 빈 곳 누르기는 `clear`입니다.

닫는 손:
- 빈 곳에서 pointerdown → move → up
Expand Down
23 changes: 12 additions & 11 deletions docs/public/affordance-nudge.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
# Nudge

TBD.

Nudge는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위,
Shift+화살표는 큰 단위입니다. 항목 이웃으로 초점을 옮기는
[Select](affordance-select.md)와 다릅니다.

```ts
import { nudgeAffordance } from "@interactive-os/json-document-affordance";
import { applyAffordance, nudgeAffordance } from "@interactive-os/json-document-affordance";

function onKeyDown(event: KeyboardEvent) {
const hand = nudgeAffordance(event);
if (!hand) return;
editor.dispatch({
type: "object.translate",
objectIds: editor.selectedObjects.map((object) => object.id),
dx: hand.dx * unit,
dy: hand.dy * unit,
applyAffordance(nudgeAffordance(event), {
hand: (hand) => {
if (hand.type !== "nudge") return;
editor.dispatch({
type: "object.translate",
objectIds: editor.selectedObjects.map((object) => object.id),
dx: hand.dx,
dy: hand.dy,
});
},
});
}
```

호스트는 단위(px, 칸, 그리드)를 곱합니다. 이동은 json-document로 갑니다.
손은 1과 10을 닫습니다. 이동은 json-document로 갑니다.

닫는 손:
- Arrow: 1 단위
Expand Down
2 changes: 0 additions & 2 deletions docs/public/affordance-pan.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Pan

TBD.

Pan은 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `grab` /
`grabbing` / `all-scroll` 커서가 이 손을 가리킵니다.

Expand Down
12 changes: 12 additions & 0 deletions docs/public/affordance-select.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Select는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, S
import {
applyAffordance,
pointerSelect,
planeHitAffordance,
} from "@interactive-os/json-document-affordance";

function onPointerDown(event: PointerEvent, itemId: string) {
Expand All @@ -19,9 +20,20 @@ function onPointerDown(event: PointerEvent, itemId: string) {
},
});
}

function onPlanePointerDown(event: PointerEvent, hitId: string, selectedIds: ReadonlyArray<string>) {
applyAffordance(planeHitAffordance({ hitId, selectedIds, ...event }), {
hand: (hand) => {
if (hand.type !== "select" || !hand.objectIds) return;
editor.dispatch({ type: "selection.set", objectIds: hand.objectIds, mode: "replace" });
},
});
}
```

호스트는 보이는 키와 장르 Intent만 넘깁니다. keymap을 덮어쓰지 않습니다.
이미 고른 상자를 수정 키 없이 누르면 집합을 유지합니다. 안 고른 상자는
그 상자만으로 바꿉니다.

## TBD

Expand Down
2 changes: 0 additions & 2 deletions docs/public/affordance-snap.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Snap

TBD.

Snap은 [Drag](affordance-drag.md)·[Resize](affordance-resize.md)·
[Nudge](affordance-nudge.md) 중에 그리드나 가이드에 붙는 손입니다.
수정 키를 누르면 붙지 않습니다.
Expand Down
2 changes: 0 additions & 2 deletions docs/public/affordance-typeahead.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Typeahead

TBD.

Typeahead는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손입니다. 한 글자는
그 글자로 시작하는 다음 항목, 빠르게 이어 치면 그 문자열로 시작합니다.

Expand Down
16 changes: 8 additions & 8 deletions docs/public/affordance.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,26 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command

| Affordance | API | Hand |
| --- | --- | --- |
| [Select](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 |
| [Select](affordance-select.md) | `pointerSelect`, `planeHitAffordance`, `resolveAffordanceKey` | 클릭, 이미 고른 집합 유지, Shift 범위, Mod 토글, 화살표 |
| [Typeahead](affordance-typeahead.md) | `typeaheadAffordance` | 인쇄 글쇠 prefix 점프 |
| [Escape](affordance-cancel.md) | `escapeAffordance` | 제스처 `cancel`, 그다음 선택 `clear` |
| [Expand/Collapse](affordance-fold.md) | `treeAffordance` | 나무 왼쪽 접힘, 오른쪽 펼침 |
| [Drag](affordance-drag.md) | `dragAffordance`, `commitAffordance` | 고른 대상을 포인터로 옮김 |
| [Undo](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z |
| [Nudge](affordance-nudge.md) | `nudgeAffordance` | 화살표 한 단위, Shift 큰 단위 |
| [Drag](affordance-drag.md) | `dragAffordance`, `commitAffordance` | 고른 대상을 포인터로 옮김 |
| [Marquee](affordance-marquee.md) | `marqueeAffordance`, `commitAffordance` | 빈 곳에서 사각형으로 여러 대상 |
| [Pan](affordance-pan.md) | `panAffordance` | Space+드래그, grab |
| [Snap](affordance-snap.md) | `snapAffordance` | 그리드·가이드, 수정 키로 해제 |

## 키보드 TBD

| Affordance | API | Hand |
| --- | --- | --- |
| [Focus](affordance-focus.md) | `focusAffordance` | Tab 사이, 화살표 안, 초점 ≠ 선택 |
| [Caret](affordance-caret.md) | `caretAffordance`, `caretCursor` | I-beam 삽입점, 글 범위 |
| [Typeahead](affordance-typeahead.md) | `typeaheadAffordance` | 인쇄 글쇠 prefix 점프 |
| [Activate](affordance-activate.md) | `activateAffordance` | Enter, Space, 기본 클릭 |
| [Escape](affordance-cancel.md) | `escapeAffordance` | Escape, pointercancel |
| [Delete](affordance-delete.md) | `deleteAffordance` | Delete, Backspace. Delete chord는 이미 닫힘 |
| [Rename](affordance-rename.md) | `renameAffordance` | F2, 느린 두 번 누르기 |
| [Nudge](affordance-nudge.md) | `nudgeAffordance` | 화살표 한 단위, Shift 큰 단위 |

## 마우스 TBD

Expand All @@ -87,14 +90,11 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command
| [Double-click](affordance-double-click.md) | `clickCountAffordance` | `detail` 2 |
| [Triple-click](affordance-triple-click.md) | `clickCountAffordance` | `detail` 3 |
| [Context menu](affordance-context-menu.md) | `contextMenuAffordance` | 오른쪽 클릭, Shift+F10, Menu |
| [Marquee](affordance-marquee.md) | `marqueeAffordance`, `commitAffordance` | 빈 곳에서 사각형으로 여러 대상 |
| [Drop](affordance-drop.md) | `dropAffordance` | drop 대상, no-drop |
| [Duplicate](affordance-copy-drag.md) | `dragOperation` | Alt/Option 드래그 복제 |
| [Resize](affordance-resize.md) | `resizeCursor`, `resizeOffset` | 모서리, 칸, 분할선 |
| [Pan](affordance-pan.md) | `panAffordance` | Space+드래그, grab |
| [Scroll](affordance-scroll.md) | `wheelAffordance`, `autoscrollAffordance` | wheel, autoscroll |
| [Zoom](affordance-zoom.md) | `zoomAffordance`, `zoomCursor` | Mod+휠, +/− |
| [Snap](affordance-snap.md) | `snapAffordance` | 그리드·가이드, 수정 키로 해제 |
| [Not-allowed](affordance-forbid.md) | `forbiddenCursor` | not-allowed, no-drop |

## 커서가 닫는 손
Expand Down
25 changes: 22 additions & 3 deletions packages/json-document-affordance/src/drag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { WebModifierState } from "@interactive-os/json-document-web";
import {
selectionOperationFromModifiers,
type WebModifierState,
} from "@interactive-os/json-document-web";
import type {
AffordancePreview,
AffordanceRect,
Expand Down Expand Up @@ -45,10 +48,26 @@ function marqueeRect(origin: Point, point: Point): Rect {
};
}

export function marqueeAffordance(origin: Point, point: Point): AffordancePreview {
export function marqueeAffordance(
origin: Point,
point: Point,
modifiers?: {
readonly shiftKey?: boolean;
readonly metaKey?: boolean;
readonly ctrlKey?: boolean;
},
): AffordancePreview {
const rect = marqueeRect(origin, point);
if (rect.width === 0 && rect.height === 0) {
return { hand: { type: "clear" } };
}
const operation = selectionOperationFromModifiers({
shiftKey: modifiers?.shiftKey ?? false,
metaKey: modifiers?.metaKey ?? false,
ctrlKey: modifiers?.ctrlKey ?? false,
});
return {
hand: { type: "select", operation: "replace", rect },
hand: { type: "select", operation, rect },
cursor: "crosshair",
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/json-document-affordance/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export type {
AffordanceResult,
SelectOperation,
} from "./result.js";
export { activateAffordance, clickCountAffordance, escapeAffordance, focusAffordance, pointerSelect, resolveAffordanceKey, selectAllAffordance, typeaheadAffordance } from "./select.js";
export { activateAffordance, clickCountAffordance, escapeAffordance, focusAffordance, planeHitAffordance, pointerSelect, resolveAffordanceKey, selectAllAffordance, typeaheadAffordance } from "./select.js";
10 changes: 6 additions & 4 deletions packages/json-document-affordance/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export type AffordanceRect = {
};

export type AffordanceHand =
| { readonly type: "select"; readonly operation: SelectOperation; readonly rect?: AffordanceRect }
| {
readonly type: "select";
readonly operation: SelectOperation;
readonly rect?: AffordanceRect;
readonly objectIds?: ReadonlyArray<string>;
}
| { readonly type: "move"; readonly direction: AffordanceMoveDirection; readonly operation: "replace" | "extend" }
| { readonly type: "boundary"; readonly edge: "start" | "end"; readonly operation: "replace" | "extend" }
| { readonly type: "toggle" }
Expand Down Expand Up @@ -86,9 +91,6 @@ export function commitAffordance<H extends AffordanceHand>(
const hand = result.hand;
if (hand == null) return null;
if (hand.type === "translate" && hand.dx === 0 && hand.dy === 0) return null;
if (hand.type === "select" && hand.rect && hand.rect.width === 0 && hand.rect.height === 0) {
return null;
}
return result.cursor === undefined
? { hand, commit: true }
: { hand, cursor: result.cursor, commit: true };
Expand Down
Loading