@neta-art/cohub 12.2.0 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -3
- package/dist/board/editor/context.d.ts +2 -0
- package/dist/board/editor/document.d.ts +3 -1
- package/dist/board/editor/document.js +8 -6
- package/dist/board/editor/editor.d.ts +7 -3
- package/dist/board/editor/editor.js +8 -1
- package/dist/board/editor/index.d.ts +3 -3
- package/dist/board/editor/index.js +2 -2
- package/dist/board/editor/interaction.d.ts +23 -2
- package/dist/board/editor/interaction.js +99 -5
- package/dist/board/editor/query.d.ts +1 -1
- package/dist/board/editor/query.js +3 -7
- package/dist/board/editor/selection.js +13 -1
- package/dist/board/editor/tool.d.ts +4 -1
- package/dist/board/editor/tool.js +6 -1
- package/dist/board/model/asset-key.d.ts +9 -1
- package/dist/board/model/asset-key.js +27 -7
- package/dist/board/model/index.d.ts +5 -5
- package/dist/board/model/index.js +4 -4
- package/dist/board/model/items/create.js +1 -1
- package/dist/board/model/items/definition.d.ts +9 -1
- package/dist/board/model/items/index.d.ts +2 -2
- package/dist/board/model/items/registry.d.ts +2 -1
- package/dist/board/model/items/registry.js +1 -0
- package/dist/board/model/text-metrics.d.ts +21 -1
- package/dist/board/model/text-metrics.js +24 -1
- package/dist/board/model/themes.d.ts +49 -42
- package/dist/board/model/themes.js +49 -44
- package/dist/board/render/index.d.ts +8 -6
- package/dist/board/render/index.js +7 -5
- package/dist/board/render/labels.d.ts +15 -0
- package/dist/board/render/labels.js +14 -0
- package/dist/board/render/lod.d.ts +9 -1
- package/dist/board/render/lod.js +7 -1
- package/dist/board/render/media-interaction.d.ts +5 -2
- package/dist/board/render/media-interaction.js +14 -2
- package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
- package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
- package/dist/board/render/renderers/board-renderer-registry.js +9 -4
- package/dist/board/render/renderers/far-plate.d.ts +4 -1
- package/dist/board/render/renderers/far-plate.js +5 -1
- package/dist/board/render/renderers/file-card-renderer.js +44 -24
- package/dist/board/render/renderers/frame-card-renderer.js +9 -3
- package/dist/board/render/renderers/image-card-renderer.js +2 -2
- package/dist/board/render/renderers/model-card-renderer.js +2 -2
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +2 -2
- package/dist/board/render/style.d.ts +1 -0
- package/dist/board/render/style.js +3 -1
- package/dist/board/render/text-edit.d.ts +32 -0
- package/dist/board/render/text-edit.js +134 -0
- package/dist/board/render/text-fit.d.ts +1 -3
- package/dist/board/render/text-fit.js +4 -43
- package/dist/board/render/text-measurement.d.ts +12 -2
- package/dist/board/render/text-measurement.js +55 -10
- package/dist/board/replica/index.d.ts +4 -2
- package/dist/board/replica/index.js +34 -21
- package/dist/board/replica/indexeddb.d.ts +16 -0
- package/dist/board/replica/indexeddb.js +155 -0
- package/dist/board/stage/assets.d.ts +16 -4
- package/dist/board/stage/assets.js +155 -30
- package/dist/board/stage/bench.d.ts +35 -0
- package/dist/board/stage/bench.js +153 -0
- package/dist/board/stage/device.d.ts +41 -0
- package/dist/board/stage/device.js +62 -0
- package/dist/board/stage/frame-budget.d.ts +19 -0
- package/dist/board/stage/frame-budget.js +69 -0
- package/dist/board/stage/idle.d.ts +4 -0
- package/dist/board/stage/idle.js +26 -0
- package/dist/board/stage/index.d.ts +9 -5
- package/dist/board/stage/index.js +5 -2
- package/dist/board/stage/presses.d.ts +21 -0
- package/dist/board/stage/presses.js +66 -0
- package/dist/board/stage/scene.d.ts +4 -0
- package/dist/board/stage/scene.js +45 -13
- package/dist/board/stage/stage.d.ts +21 -2
- package/dist/board/stage/stage.js +309 -89
- package/dist/board/stage/texture-pool.d.ts +6 -4
- package/dist/board/stage/texture-pool.js +42 -8
- package/dist/board/stage/views.d.ts +1 -1
- package/dist/board/stage/views.js +2 -2
- package/dist/chunks/environment.d.ts +6 -0
- package/dist/chunks/http.d.ts +9 -1
- package/dist/chunks/http.js +791 -4
- package/dist/index.d.ts +52 -1
- package/dist/index.js +65 -733
- package/dist/protocol/dist/board-model.d.ts +15 -1
- package/dist/protocol/dist/board-model.js +12 -2
- package/dist/protocol/dist/board-schema.d.ts +2 -1
- package/dist/protocol/dist/board-schema.js +4 -1
- package/dist/protocol/dist/core/media.js +4 -0
- package/dist/protocol/dist/index.d.ts +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ const created = await space.boards.create({
|
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
const board = space.board(created.id);
|
|
115
|
+
const board = space.board(created.id); // or await space.boards.open("boards/plan.board")
|
|
116
116
|
const { items, version } = await board.get({ only: ["items"] });
|
|
117
117
|
await board.apply({ items: { goal: { props: { text: "Shipped" } } } }, { baseVersion: version });
|
|
118
118
|
|
|
@@ -159,7 +159,7 @@ change, so they work directly with `useSyncExternalStore`:
|
|
|
159
159
|
```ts
|
|
160
160
|
import { createBoardRegistry, defineBoardItem, worldPoint } from "@neta-art/cohub/board";
|
|
161
161
|
import { createBoardEditor } from "@neta-art/cohub/board/editor";
|
|
162
|
-
import { createBoardReplica } from "@neta-art/cohub/board/replica";
|
|
162
|
+
import { createBoardReplica, createIndexedDbBoardReplicaStorage } from "@neta-art/cohub/board/replica";
|
|
163
163
|
import { createBoardAssetManager, mountBoardStage } from "@neta-art/cohub/board/stage";
|
|
164
164
|
import { z } from "zod";
|
|
165
165
|
|
|
@@ -172,7 +172,13 @@ const order = defineBoardItem({
|
|
|
172
172
|
capabilities: { canRotate: false },
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
// Cached documents and unsent writes survive a reload; clear them on sign-out
|
|
176
|
+
// with `clearBoardReplicaStorage({ userKey })`. The replica reads the Board again
|
|
177
|
+
// after a realtime reconnect and resends pending writes when the network returns.
|
|
178
|
+
const replica = createBoardReplica({
|
|
179
|
+
remote: space.board(boardId),
|
|
180
|
+
storage: createIndexedDbBoardReplicaStorage({ userKey, spaceId: space.id, boardId }),
|
|
181
|
+
});
|
|
176
182
|
await replica.start();
|
|
177
183
|
|
|
178
184
|
const editor = createBoardEditor({
|
|
@@ -230,6 +236,84 @@ rewritten by older ones. For a canvas look instead of DOM, pass renderers to
|
|
|
230
236
|
`createBoardCardRendererResolver` from `board/render` and hand the result to
|
|
231
237
|
the stage or to `renderBoardExport` as `renderers`.
|
|
232
238
|
|
|
239
|
+
### Hosting a Board
|
|
240
|
+
|
|
241
|
+
Compose the SDK instead of wrapping it, and each release's loading, level of
|
|
242
|
+
detail and text work reaches your Board without code changes.
|
|
243
|
+
|
|
244
|
+
Pictures your own items show go through `itemAsset`. They share the built-in
|
|
245
|
+
loader: CDN variants sized to the screen, loads ordered by where the view is
|
|
246
|
+
heading, prefetch, compact copies when panned away, device budgets, one cache
|
|
247
|
+
and exports. `src` is a Space path, resolved like an image item's, or a URL;
|
|
248
|
+
`fit: "cover"` sizes pictures cropped to fill their box by the short edge. Call
|
|
249
|
+
`invalidateItemAssets()` when the data it reads changes, and `invalidatePath`
|
|
250
|
+
when a file changes:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const assets = createBoardAssetManager({
|
|
254
|
+
resolveFileUrl: source.resolveFileUrl,
|
|
255
|
+
// undefined keeps the built-in rule; null shows no picture.
|
|
256
|
+
itemAsset: (item) => {
|
|
257
|
+
if (item.type !== "acme.order") return undefined;
|
|
258
|
+
const cover = covers.get(item.props.orderId as string); // "orders/o-1.webp"
|
|
259
|
+
return cover ? { src: cover, fit: "cover" } : null;
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- **Budgets.** Leave `lod`, `prefetch`, `concurrency` and `lruBudget` unset; they
|
|
265
|
+
follow `defaultBoardDeviceBudget()`. Force a tier with
|
|
266
|
+
`boardDeviceBudget(readBoardDeviceSignals(), "low")`.
|
|
267
|
+
- **Canvas cards.** Declare `lod` (`{ kind: "media" }` for pictures) and
|
|
268
|
+
`renderFar`, drawn with `drawFarPlate(graphics, item.frame, { fill:
|
|
269
|
+
farPictureTone(item, context) ?? paper })`. Wrap card text with
|
|
270
|
+
`layoutTextLines(text, font, width, { whiteSpace: "normal", maxLines: 2 })`
|
|
271
|
+
and apply `context.textResolution` in the `resolution` hook.
|
|
272
|
+
- **Opening cards.** Give a type `activation: "tap"` and handle `onActivate`: a
|
|
273
|
+
quick press opens the card without selecting it, a drag moves it (or pans with
|
|
274
|
+
the hand tool or read-only), and hovering shows a pointer instead of ports. A
|
|
275
|
+
press held in place past `BOARD_TAP_MAX_MS` does nothing: it was a pick-up. A
|
|
276
|
+
press on a selected card reports `selected: true`. `onLongPress` fires for a still
|
|
277
|
+
touch or pen; call `editor.pickUp()` there to pick the card up and drag it. The
|
|
278
|
+
stage drops the click a touch screen sends after a tap that opened something.
|
|
279
|
+
- **Card keys.** A renderer's `hitAt(item, point, context)` returns `{ action, cursor? }`
|
|
280
|
+
for keys it draws; the stage shows the cursor, passes the hovered key to renderers as
|
|
281
|
+
`context.hoveredHit` and sends presses to `onHit` (return `false` to let the editor
|
|
282
|
+
have them). Media cards report `BOARD_PLAY_MEDIA` this way. `handlesHit(item, action)`
|
|
283
|
+
names the keys you act on; the rest show no cursor and go to the editor.
|
|
284
|
+
- **Touch.** A touch on the selected item drags it, never a port. Pass
|
|
285
|
+
`touchRotation: false` to `createBoardEditor` to move and resize by touch and pen
|
|
286
|
+
without a rotation handle.
|
|
287
|
+
- **DOM views.** `viewLayer: "below"` puts them under the canvas, so selection and
|
|
288
|
+
strokes draw over them; views there get no input. Use a theme with `imageBackground`
|
|
289
|
+
and paint the Board background behind the host, or the canvas covers them.
|
|
290
|
+
- **Previews.** Write `preview: { title, excerpt, cover, src }` on your items so
|
|
291
|
+
clients without your renderer, exports and agents show them as cards. Keep it out
|
|
292
|
+
of undo with `editor.commitPatch(patch, { record: false })`.
|
|
293
|
+
- **Text editing.** Place the in-place editor with `boardTextEditLayout`, so its
|
|
294
|
+
caret sits on the canvas text:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const layout = boardTextEditLayout(item, { viewport: editor.camera, scene: editor.scene, style: stage.theme.style, draft });
|
|
298
|
+
// A content-box textarea at layout.left/top, sized layout.width × height, padded by
|
|
299
|
+
// layout.padding, with `rotate(${layout.rotation}deg) scale(${layout.scale})` about its center.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- **Themes.** Start from a built-in theme and override what differs:
|
|
303
|
+
`{ key: "neta-pixel", ...boardRenderTheme("neta-pixel") }`. `style.frameLabelSize`
|
|
304
|
+
sets frame titles; the editor's title hit box and text editor follow it.
|
|
305
|
+
- **Fonts.** Board text measures in `"Geist Variable"`; load
|
|
306
|
+
`@fontsource-variable/geist` so lines match the CLI export.
|
|
307
|
+
- **Agent context.** `onViewStateChange` on the editor reports `visibleRect` and
|
|
308
|
+
`selectedNodes`; send them with a prompt as
|
|
309
|
+
`buildViewportContentBlock([{ kind: "board", path, boardId, visibleRect, selectedNodes }])`
|
|
310
|
+
so the agent knows which Board, where and what the user is looking at.
|
|
311
|
+
- **Measuring.** `runBoardStageBench(stage)` drives the stage with wheel input and
|
|
312
|
+
reports frame and settle times; `stage.stats()` shows live, far and texture counts.
|
|
313
|
+
|
|
314
|
+
Release notes for Board changes end with `Hosts:`, naming what an embedding app
|
|
315
|
+
must change, or `Hosts: no action`.
|
|
316
|
+
|
|
233
317
|
### Images
|
|
234
318
|
|
|
235
319
|
In the browser, `renderBoardExport(renderer, document, options)` from
|
|
@@ -84,6 +84,8 @@ type BoardEditorOptions = {
|
|
|
84
84
|
initialTool?: BoardToolId;
|
|
85
85
|
key?: string;
|
|
86
86
|
readonly?: boolean;
|
|
87
|
+
/** false: touch and pen move and resize the selection but never rotate it. */
|
|
88
|
+
touchRotation?: boolean;
|
|
87
89
|
preferences?: Partial<BoardEditorPreferences>;
|
|
88
90
|
onPreferencesChange?: (preferences: BoardEditorPreferences) => void;
|
|
89
91
|
onCommit: (patch: BoardPatch) => void | Promise<void>;
|
|
@@ -15,7 +15,9 @@ declare function createDocumentModule(ctx: EditorContext): {
|
|
|
15
15
|
readonly hasScriptItems: boolean;
|
|
16
16
|
setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
|
|
17
17
|
animationItems: () => Map<string, BoardItem>;
|
|
18
|
-
commitPatch: (patch: BoardPatch
|
|
18
|
+
commitPatch: (patch: BoardPatch, { record }?: {
|
|
19
|
+
record?: boolean;
|
|
20
|
+
}) => Promise<void> | undefined;
|
|
19
21
|
rescene: (ids?: Iterable<string>) => void;
|
|
20
22
|
reset: (next: BoardDocument) => void;
|
|
21
23
|
setDraftItem: (id: string, item: BoardItem | null) => void;
|
|
@@ -206,7 +206,7 @@ function createDocumentModule(ctx) {
|
|
|
206
206
|
for (const [id, item] of changes) setDraftItem(id, item);
|
|
207
207
|
commit({}, record);
|
|
208
208
|
}
|
|
209
|
-
function commitPatch(patch) {
|
|
209
|
+
function commitPatch(patch, { record = true } = {}) {
|
|
210
210
|
if (options.readonly) throw new Error("Board is read-only.");
|
|
211
211
|
const before = state.base;
|
|
212
212
|
const result = applyBoardPatchToDocument(before, patch);
|
|
@@ -214,11 +214,13 @@ function createDocumentModule(ctx) {
|
|
|
214
214
|
const ids = Object.keys(patch.items ?? {});
|
|
215
215
|
const redo = diffBoardEdits(before, result.document, ids);
|
|
216
216
|
if (isEmptyBoardPatch(redo)) return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
if (record) {
|
|
218
|
+
state.undoStack = [...state.undoStack.slice(-199), {
|
|
219
|
+
undo: diffBoardEdits(result.document, before, ids),
|
|
220
|
+
redo
|
|
221
|
+
}];
|
|
222
|
+
state.redoStack = [];
|
|
223
|
+
}
|
|
222
224
|
adopt(result.document, ids);
|
|
223
225
|
return send(redo).then((error) => {
|
|
224
226
|
if (error) throw error;
|
|
@@ -11,7 +11,7 @@ import "../model/index.js";
|
|
|
11
11
|
import { AlignMode, DistributeAxis } from "./align.js";
|
|
12
12
|
import { BoardFocusOptions } from "./camera.js";
|
|
13
13
|
import { BoardSelectionTransform, BoardTransformControl } from "./selection-transform.js";
|
|
14
|
-
import { BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
|
|
14
|
+
import { BoardActivation, BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
|
|
15
15
|
import { BoardAppMetadata, BoardTaskSource } from "./creation.js";
|
|
16
16
|
import { BoardClipboardPayload } from "./clipboard.js";
|
|
17
17
|
import { BoardNaturalSize } from "./snapshots.js";
|
|
@@ -136,11 +136,13 @@ declare function createBoardEditor(options: BoardEditorOptions): {
|
|
|
136
136
|
setPlayedItems: (items: ReadonlyMap<string, BoardItem>) => void;
|
|
137
137
|
setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
|
|
138
138
|
animationItems: () => Map<string, BoardItem>;
|
|
139
|
-
commitPatch: (patch: BoardPatch
|
|
139
|
+
commitPatch: (patch: BoardPatch, { record }?: {
|
|
140
|
+
record?: boolean;
|
|
141
|
+
}) => Promise<void> | undefined;
|
|
140
142
|
consumeRecentlyAdded: () => string[];
|
|
141
143
|
itemAt: (point: WorldPoint, exclude?: ReadonlySet<string> | undefined, includeTemporary?: boolean | undefined) => BoardSceneItem | null;
|
|
142
144
|
itemById: (id: string) => BoardSceneItem | null;
|
|
143
|
-
labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
|
|
145
|
+
labelItemAt: (point: WorldPoint, frameLabelSize?: number | undefined) => BoardSceneItem | null;
|
|
144
146
|
idsInRect: (rect: Rect, includeTemporary?: boolean | undefined) => string[];
|
|
145
147
|
setCamera: (viewport: BoardViewport) => void;
|
|
146
148
|
setCameraPolicy: (policy: BoardCameraPolicy) => void;
|
|
@@ -219,6 +221,8 @@ declare function createBoardEditor(options: BoardEditorOptions): {
|
|
|
219
221
|
pointerUp: (event: BoardPointerEvent) => void;
|
|
220
222
|
pointerLeave: () => void;
|
|
221
223
|
subscribeTransforms: (listener: (event: BoardTransformEnd) => void) => () => void;
|
|
224
|
+
subscribeActivations: (listener: (event: BoardActivation) => void) => () => void;
|
|
225
|
+
pickUp: () => boolean;
|
|
222
226
|
cancelPointerInteraction: () => void;
|
|
223
227
|
destroy(): void;
|
|
224
228
|
};
|
|
@@ -96,6 +96,11 @@ function createBoardEditor(options) {
|
|
|
96
96
|
ctx.snapshots = createSnapshotsModule(ctx);
|
|
97
97
|
ctx.remote = createRemoteModule(ctx);
|
|
98
98
|
const { document, query, camera, selection, creation, deletion, copy, interaction, animation, snapshots, remote } = ctx;
|
|
99
|
+
const transformChannels = options.touchRotation === false ? [
|
|
100
|
+
"scene",
|
|
101
|
+
"selection",
|
|
102
|
+
"hover"
|
|
103
|
+
] : ["scene", "selection"];
|
|
99
104
|
let viewStateQueued = false;
|
|
100
105
|
function emitViewState() {
|
|
101
106
|
viewStateQueued = false;
|
|
@@ -193,7 +198,7 @@ function createBoardEditor(options) {
|
|
|
193
198
|
return read(["scene", "selection"], selection.bounds);
|
|
194
199
|
},
|
|
195
200
|
get selectionTransform() {
|
|
196
|
-
return read(
|
|
201
|
+
return read(transformChannels, selection.transform);
|
|
197
202
|
},
|
|
198
203
|
get camera() {
|
|
199
204
|
return read("camera", () => state.camera);
|
|
@@ -438,6 +443,8 @@ function createBoardEditor(options) {
|
|
|
438
443
|
pointerUp: interaction.pointerUp,
|
|
439
444
|
pointerLeave: interaction.pointerLeave,
|
|
440
445
|
subscribeTransforms: interaction.subscribeTransforms,
|
|
446
|
+
subscribeActivations: interaction.subscribeActivations,
|
|
447
|
+
pickUp: interaction.pickUp,
|
|
441
448
|
cancelPointerInteraction: interaction.cancel,
|
|
442
449
|
destroy() {
|
|
443
450
|
stopViewState();
|
|
@@ -2,13 +2,13 @@ import { BOARD_CLIPBOARD_MIME } from "../../protocol/dist/board.js";
|
|
|
2
2
|
import { AlignMode, DistributeAxis, alignFrames, distributeFrames, framesBounds } from "./align.js";
|
|
3
3
|
import { BoardFocusOptions } from "./camera.js";
|
|
4
4
|
import { BoardSelectionTransform, BoardTransformControl, resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt } from "./selection-transform.js";
|
|
5
|
-
import { BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
|
|
5
|
+
import { BoardActivation, BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
|
|
6
6
|
import { BoardAppMetadata, BoardTaskSource } from "./creation.js";
|
|
7
7
|
import { BOARD_CLIPBOARD_VERSION, BoardClipboardPayload, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, defaultPasteOffset, encodeClipboard, materializeClipboard, parseClipboard, remapItems } from "./clipboard.js";
|
|
8
8
|
import { BoardNaturalSize } from "./snapshots.js";
|
|
9
9
|
import { SnapAxis, SnapGuide, SnapOptions, SnapResult, computeSnap } from "./snapping.js";
|
|
10
10
|
import { BoardEditorChannel, BoardEditorListener } from "./state.js";
|
|
11
|
-
import { BOARD_HAND_TAP_SLOP, BoardToolId, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
11
|
+
import { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, BoardToolId, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
12
12
|
import { BoardCameraPolicy, BoardEditorOptions, BoardEditorPreferences, BoardPlayhead, BoardViewState } from "./context.js";
|
|
13
13
|
import { CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, connectionPortAt, connectionPorts, portHitRadius } from "./connection-ports.js";
|
|
14
14
|
import { sameJson } from "../model/json.js";
|
|
@@ -16,4 +16,4 @@ import { BoardEditPlayhead, diffBoardEdits, isEmptyBoardPatch, readPath, routeBo
|
|
|
16
16
|
import { BoardEditor, createBoardEditor } from "./editor.js";
|
|
17
17
|
import { SpatialEntry, SpatialIndex, createSpatialIndex } from "./spatial.js";
|
|
18
18
|
import { normalizeWheelDelta, wheelZoomFactor } from "./wheel.js";
|
|
19
|
-
export { AlignMode, BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, type BoardAppMetadata, type BoardCameraPolicy, BoardClipboardPayload, BoardEditPlayhead, type BoardEditor, type BoardEditorChannel, type BoardEditorListener, type BoardEditorOptions, type BoardEditorPreferences, type BoardFocusOptions, type BoardInteraction, type BoardNaturalSize, type BoardPlayhead, type BoardPointerEvent, BoardSelectionTransform, type BoardTaskSource, BoardToolId, BoardTransformControl, type BoardTransformEnd, type BoardViewState, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, DUPLICATE_OFFSET, DistributeAxis, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, SnapAxis, SnapGuide, SnapOptions, SnapResult, SpatialEntry, SpatialIndex, alignFrames, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
|
|
19
|
+
export { AlignMode, BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, type BoardActivation, type BoardAppMetadata, type BoardCameraPolicy, BoardClipboardPayload, BoardEditPlayhead, type BoardEditor, type BoardEditorChannel, type BoardEditorListener, type BoardEditorOptions, type BoardEditorPreferences, type BoardFocusOptions, type BoardInteraction, type BoardNaturalSize, type BoardPlayhead, type BoardPointerEvent, BoardSelectionTransform, type BoardTaskSource, BoardToolId, BoardTransformControl, type BoardTransformEnd, type BoardViewState, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, DUPLICATE_OFFSET, DistributeAxis, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, SnapAxis, SnapGuide, SnapOptions, SnapResult, SpatialEntry, SpatialIndex, alignFrames, boardTapSlop, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
|
|
@@ -5,9 +5,9 @@ import { diffBoardEdits, isEmptyBoardPatch, readPath, routeBoardEdits, writePath
|
|
|
5
5
|
import { normalizeWheelDelta, wheelZoomFactor } from "./wheel.js";
|
|
6
6
|
import { BOARD_CLIPBOARD_VERSION, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, defaultPasteOffset, encodeClipboard, materializeClipboard, parseClipboard, remapItems } from "./clipboard.js";
|
|
7
7
|
import { CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, connectionPortAt, connectionPorts, portHitRadius } from "./connection-ports.js";
|
|
8
|
-
import { BOARD_HAND_TAP_SLOP, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
8
|
+
import { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
9
9
|
import { resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt } from "./selection-transform.js";
|
|
10
10
|
import { computeSnap } from "./snapping.js";
|
|
11
11
|
import { createSpatialIndex } from "./spatial.js";
|
|
12
12
|
import { createBoardEditor } from "./editor.js";
|
|
13
|
-
export { BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, alignFrames, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
|
|
13
|
+
export { BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, alignFrames, boardTapSlop, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
|
|
@@ -6,9 +6,19 @@ import "../model/index.js";
|
|
|
6
6
|
import { BoardTransformControl } from "./selection-transform.js";
|
|
7
7
|
import { EditorContext } from "./context.js";
|
|
8
8
|
//#region src/board/editor/interaction.d.ts
|
|
9
|
+
type BoardPress = {
|
|
10
|
+
type: "pressing";
|
|
11
|
+
itemId: string;
|
|
12
|
+
pointerId: number;
|
|
13
|
+
pointerType: string;
|
|
14
|
+
start: ScreenPoint;
|
|
15
|
+
world: WorldPoint;
|
|
16
|
+
camera: BoardViewport;
|
|
17
|
+
at: number;
|
|
18
|
+
};
|
|
9
19
|
type BoardInteraction = {
|
|
10
20
|
type: "idle";
|
|
11
|
-
} | {
|
|
21
|
+
} | BoardPress | {
|
|
12
22
|
type: "panning";
|
|
13
23
|
start: ScreenPoint;
|
|
14
24
|
origin: BoardViewport;
|
|
@@ -22,6 +32,10 @@ type BoardInteraction = {
|
|
|
22
32
|
origin: Map<string, BoardFrame>;
|
|
23
33
|
moved: boolean;
|
|
24
34
|
duplicate: boolean;
|
|
35
|
+
tap?: {
|
|
36
|
+
itemId: string;
|
|
37
|
+
at: number;
|
|
38
|
+
};
|
|
25
39
|
} | {
|
|
26
40
|
type: "resizing";
|
|
27
41
|
handle: ResizeHandle;
|
|
@@ -94,6 +108,11 @@ type BoardPointerEvent = {
|
|
|
94
108
|
pressure: number;
|
|
95
109
|
samples?: readonly BoardDrawInputSample[];
|
|
96
110
|
};
|
|
111
|
+
type BoardActivation = {
|
|
112
|
+
itemId: string;
|
|
113
|
+
pointerType: string;
|
|
114
|
+
selected: boolean;
|
|
115
|
+
};
|
|
97
116
|
type BoardTransformEnd = {
|
|
98
117
|
ids: string[];
|
|
99
118
|
operation: "move" | "resize" | "rotate" | "arrow";
|
|
@@ -117,9 +136,11 @@ declare function createInteractionModule(ctx: EditorContext): {
|
|
|
117
136
|
pointerUp: (event: BoardPointerEvent) => void;
|
|
118
137
|
pointerLeave: () => void;
|
|
119
138
|
cancel: () => void;
|
|
139
|
+
pickUp: () => boolean;
|
|
120
140
|
subscribeTransforms(listener: (event: BoardTransformEnd) => void): () => void;
|
|
141
|
+
subscribeActivations(listener: (event: BoardActivation) => void): () => void;
|
|
121
142
|
destroy(): void;
|
|
122
143
|
};
|
|
123
144
|
type InteractionModule = ReturnType<typeof createInteractionModule>;
|
|
124
145
|
//#endregion
|
|
125
|
-
export { BoardInteraction, BoardPointerEvent, BoardTransformEnd, InteractionModule, createInteractionModule };
|
|
146
|
+
export { BoardActivation, BoardInteraction, BoardPointerEvent, BoardTransformEnd, InteractionModule, createInteractionModule };
|
|
@@ -3,7 +3,7 @@ import { resolveSceneArrow } from "../model/arrow-geometry.js";
|
|
|
3
3
|
import { appendBoardDrawSamples } from "../model/draw-input.js";
|
|
4
4
|
import { createBoardItemId } from "../model/items/id.js";
|
|
5
5
|
import { connectionPortAt, connectionPorts } from "./connection-ports.js";
|
|
6
|
-
import { canTapSelectWithHand, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
6
|
+
import { boardTapSlop, canTapSelectWithHand, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
7
7
|
import { selectionTransformControlAt } from "./selection-transform.js";
|
|
8
8
|
import { computeSnap } from "./snapping.js";
|
|
9
9
|
import { memo } from "./state.js";
|
|
@@ -33,6 +33,15 @@ function createInteractionModule(ctx) {
|
|
|
33
33
|
for (const listener of transformListeners) listener(event);
|
|
34
34
|
}
|
|
35
35
|
const { state, options, registry } = ctx;
|
|
36
|
+
const activationListeners = /* @__PURE__ */ new Set();
|
|
37
|
+
function activate(itemId, at, pointerType, selected) {
|
|
38
|
+
if (performance.now() - at > 300 || !state.scene.get(itemId)) return;
|
|
39
|
+
for (const listener of activationListeners) listener({
|
|
40
|
+
itemId,
|
|
41
|
+
pointerType,
|
|
42
|
+
selected
|
|
43
|
+
});
|
|
44
|
+
}
|
|
36
45
|
const activePointers = /* @__PURE__ */ new Map();
|
|
37
46
|
let pinch = null;
|
|
38
47
|
const marquee = memo(() => [state.interaction], (interaction) => {
|
|
@@ -106,6 +115,7 @@ function createInteractionModule(ctx) {
|
|
|
106
115
|
if (!candidateId) return null;
|
|
107
116
|
const item = scene.get(candidateId);
|
|
108
117
|
if (!item || ctx.query.isLocked(item) || item.type === "arrow") return null;
|
|
118
|
+
if (!selection.includes(item.id) && registry.activation(item) === "tap") return null;
|
|
109
119
|
return registry.capabilities(item).canConnect ? item.id : null;
|
|
110
120
|
}
|
|
111
121
|
function visiblePorts() {
|
|
@@ -160,6 +170,71 @@ function createInteractionModule(ctx) {
|
|
|
160
170
|
size: arrow.size
|
|
161
171
|
};
|
|
162
172
|
}
|
|
173
|
+
function onSelectedItem(event) {
|
|
174
|
+
if (!canTapSelectWithHand(event.pointerType)) return false;
|
|
175
|
+
const item = ctx.query.itemAt(event.world);
|
|
176
|
+
return item !== null && state.selection.includes(item.id);
|
|
177
|
+
}
|
|
178
|
+
function plainPrimary(event) {
|
|
179
|
+
return event.button === 0 && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
180
|
+
}
|
|
181
|
+
function press(event) {
|
|
182
|
+
if (!plainPrimary(event)) return false;
|
|
183
|
+
const item = ctx.query.itemAt(event.world);
|
|
184
|
+
if (!item || state.selection.includes(item.id) || registry.activation(item) !== "tap") return false;
|
|
185
|
+
state.interaction = {
|
|
186
|
+
type: "pressing",
|
|
187
|
+
itemId: item.id,
|
|
188
|
+
pointerId: event.pointerId,
|
|
189
|
+
pointerType: event.pointerType,
|
|
190
|
+
start: event.screen,
|
|
191
|
+
world: event.world,
|
|
192
|
+
camera: { ...state.camera },
|
|
193
|
+
at: performance.now()
|
|
194
|
+
};
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
function releasePress(gesture, event) {
|
|
198
|
+
const dx = event.screen.x - gesture.start.x;
|
|
199
|
+
const dy = event.screen.y - gesture.start.y;
|
|
200
|
+
if (Math.hypot(dx, dy) <= boardTapSlop(gesture.pointerType)) return;
|
|
201
|
+
const item = state.scene.get(gesture.itemId);
|
|
202
|
+
if (!(!options.readonly && state.tool === "select" && item && !ctx.query.isLocked(item) && registry.capabilities(item).canMove)) {
|
|
203
|
+
state.interaction = {
|
|
204
|
+
type: "panning",
|
|
205
|
+
start: gesture.start,
|
|
206
|
+
origin: gesture.camera,
|
|
207
|
+
moved: true,
|
|
208
|
+
tapSelection: null
|
|
209
|
+
};
|
|
210
|
+
ctx.camera.setCamera(panBy(gesture.camera, dx, dy));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const translating = {
|
|
214
|
+
type: "translating",
|
|
215
|
+
start: gesture.world,
|
|
216
|
+
origin: ctx.selection.framesFor([gesture.itemId]),
|
|
217
|
+
moved: false,
|
|
218
|
+
duplicate: false
|
|
219
|
+
};
|
|
220
|
+
state.interaction = translating;
|
|
221
|
+
translate(translating, event);
|
|
222
|
+
}
|
|
223
|
+
function pickUp() {
|
|
224
|
+
const gesture = state.interaction;
|
|
225
|
+
if (options.readonly || gesture.type !== "pressing") return false;
|
|
226
|
+
const item = state.scene.get(gesture.itemId);
|
|
227
|
+
if (!item || ctx.query.isLocked(item) || !registry.capabilities(item).canMove) return false;
|
|
228
|
+
state.selection = [item.id];
|
|
229
|
+
state.interaction = {
|
|
230
|
+
type: "translating",
|
|
231
|
+
start: state.hoverPoint ?? gesture.world,
|
|
232
|
+
origin: ctx.selection.framesFor([item.id]),
|
|
233
|
+
moved: false,
|
|
234
|
+
duplicate: false
|
|
235
|
+
};
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
163
238
|
function pointerDown(event) {
|
|
164
239
|
ctx.camera.cancelAnimation();
|
|
165
240
|
state.hoverPoint = event.world;
|
|
@@ -173,6 +248,7 @@ function createInteractionModule(ctx) {
|
|
|
173
248
|
}
|
|
174
249
|
if (activePointers.size > 2) return;
|
|
175
250
|
const additive = event.shiftKey || event.metaKey || event.ctrlKey;
|
|
251
|
+
if (!spaceHeld && (tool === "hand" || options.readonly) && press(event)) return;
|
|
176
252
|
if (tool === "hand" || spaceHeld || event.button === 1) {
|
|
177
253
|
const tapSelection = tool === "hand" && !spaceHeld && event.button !== 1 && canTapSelectWithHand(event.pointerType) ? { targetId: ctx.query.itemAt(event.world)?.id ?? null } : null;
|
|
178
254
|
state.interaction = {
|
|
@@ -225,7 +301,7 @@ function createInteractionModule(ctx) {
|
|
|
225
301
|
ctx.creation.beginTextDraft(event.world);
|
|
226
302
|
return;
|
|
227
303
|
}
|
|
228
|
-
const port = connectionPortAt$1(event.world, event.pointerType);
|
|
304
|
+
const port = onSelectedItem(event) ? null : connectionPortAt$1(event.world, event.pointerType);
|
|
229
305
|
if (port) {
|
|
230
306
|
state.interaction = arrowGesture(port.point, event.world, port.itemId, port.side);
|
|
231
307
|
return;
|
|
@@ -278,17 +354,23 @@ function createInteractionModule(ctx) {
|
|
|
278
354
|
};
|
|
279
355
|
return;
|
|
280
356
|
}
|
|
357
|
+
if (press(event)) return;
|
|
281
358
|
const item = ctx.query.itemAt(event.world);
|
|
282
359
|
if (!item) return brush(event, additive);
|
|
283
360
|
if (additive) ctx.selection.toggle(item.id);
|
|
284
361
|
else if (!state.selection.includes(item.id)) state.selection = [item.id];
|
|
285
362
|
const movable = ctx.selection.movable();
|
|
363
|
+
const tap = plainPrimary(event) && state.selection.length === 1 && registry.activation(item) === "tap" ? {
|
|
364
|
+
itemId: item.id,
|
|
365
|
+
at: performance.now()
|
|
366
|
+
} : void 0;
|
|
286
367
|
state.interaction = movable.length === 0 ? { type: "idle" } : {
|
|
287
368
|
type: "translating",
|
|
288
369
|
start: event.world,
|
|
289
370
|
origin: ctx.selection.framesFor(movable),
|
|
290
371
|
moved: false,
|
|
291
|
-
duplicate: event.altKey
|
|
372
|
+
duplicate: event.altKey,
|
|
373
|
+
...tap ? { tap } : {}
|
|
292
374
|
};
|
|
293
375
|
}
|
|
294
376
|
function pinchMove() {
|
|
@@ -472,6 +554,7 @@ function createInteractionModule(ctx) {
|
|
|
472
554
|
ctx.camera.setCamera(panBy(interaction.origin, dx, dy));
|
|
473
555
|
return;
|
|
474
556
|
}
|
|
557
|
+
case "pressing": return releasePress(interaction, event);
|
|
475
558
|
case "translating": return translate(interaction, event);
|
|
476
559
|
case "resizing": return resize(interaction, event);
|
|
477
560
|
case "rotating": return rotate(interaction, event);
|
|
@@ -532,11 +615,14 @@ function createInteractionModule(ctx) {
|
|
|
532
615
|
state.selection = targetId && state.scene.get(targetId) ? [targetId] : [];
|
|
533
616
|
}
|
|
534
617
|
break;
|
|
618
|
+
case "pressing":
|
|
619
|
+
activate(gesture.itemId, gesture.at, gesture.pointerType, false);
|
|
620
|
+
break;
|
|
535
621
|
case "translating":
|
|
536
622
|
if (gesture.moved) {
|
|
537
|
-
for (const [id, item] of reparentDropped(
|
|
623
|
+
for (const [id, item] of reparentDropped([...gesture.origin.keys()])) ctx.document.setDraftItem(id, item);
|
|
538
624
|
ctx.document.commit();
|
|
539
|
-
}
|
|
625
|
+
} else if (gesture.tap) activate(gesture.tap.itemId, gesture.tap.at, event.pointerType, true);
|
|
540
626
|
break;
|
|
541
627
|
case "resizing":
|
|
542
628
|
case "rotating":
|
|
@@ -610,15 +696,23 @@ function createInteractionModule(ctx) {
|
|
|
610
696
|
pointerUp,
|
|
611
697
|
pointerLeave,
|
|
612
698
|
cancel,
|
|
699
|
+
pickUp,
|
|
613
700
|
subscribeTransforms(listener) {
|
|
614
701
|
transformListeners.add(listener);
|
|
615
702
|
return () => {
|
|
616
703
|
transformListeners.delete(listener);
|
|
617
704
|
};
|
|
618
705
|
},
|
|
706
|
+
subscribeActivations(listener) {
|
|
707
|
+
activationListeners.add(listener);
|
|
708
|
+
return () => {
|
|
709
|
+
activationListeners.delete(listener);
|
|
710
|
+
};
|
|
711
|
+
},
|
|
619
712
|
destroy() {
|
|
620
713
|
activePointers.clear();
|
|
621
714
|
transformListeners.clear();
|
|
715
|
+
activationListeners.clear();
|
|
622
716
|
}
|
|
623
717
|
};
|
|
624
718
|
}
|
|
@@ -18,7 +18,7 @@ declare function createQueryModule(ctx: EditorContext): {
|
|
|
18
18
|
idsInRect: (rect: Rect, includeTemporary?: boolean) => string[];
|
|
19
19
|
frameAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
|
|
20
20
|
itemAt: (point: WorldPoint, exclude?: ReadonlySet<string>, includeTemporary?: boolean) => BoardSceneItem | null;
|
|
21
|
-
labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
|
|
21
|
+
labelItemAt: (point: WorldPoint, frameLabelSize?: number) => BoardSceneItem | null;
|
|
22
22
|
arrowHandleAt: (point: WorldPoint) => "start" | "end" | "mid" | null;
|
|
23
23
|
connectTargetAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => string | null;
|
|
24
24
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
|
|
2
2
|
import { rectContainsPoint, unionRects } from "../model/geometry.js";
|
|
3
3
|
import { arrowHitRadius, arrowPathBounds, distanceToArrow, resolveSceneArrow } from "../model/arrow-geometry.js";
|
|
4
|
+
import { frameLabelHitRect } from "../model/text-metrics.js";
|
|
4
5
|
import { createSpatialIndex } from "./spatial.js";
|
|
5
6
|
//#region src/board/editor/query.ts
|
|
6
7
|
const NO_IDS = /* @__PURE__ */ new Set();
|
|
@@ -139,7 +140,7 @@ function createQueryModule(ctx) {
|
|
|
139
140
|
}
|
|
140
141
|
return null;
|
|
141
142
|
}
|
|
142
|
-
function labelItemAt(point) {
|
|
143
|
+
function labelItemAt(point, frameLabelSize = 12) {
|
|
143
144
|
ensureSpatial();
|
|
144
145
|
const { scene, selection, camera } = state;
|
|
145
146
|
const nearby = spatial.idsAtPoint(point);
|
|
@@ -147,12 +148,7 @@ function createQueryModule(ctx) {
|
|
|
147
148
|
for (const id of new Set(candidates)) {
|
|
148
149
|
const item = scene.get(id);
|
|
149
150
|
if (!item || !state.base.items[id] || isLocked(item)) continue;
|
|
150
|
-
if (item.type === "frame" && rectContainsPoint(
|
|
151
|
-
x: item.frame.x,
|
|
152
|
-
y: item.frame.y - 22,
|
|
153
|
-
width: Math.max(48, item.frame.width * .5),
|
|
154
|
-
height: 24
|
|
155
|
-
}, point)) return item;
|
|
151
|
+
if (item.type === "frame" && rectContainsPoint(frameLabelHitRect(item.frame, frameLabelSize), point)) return item;
|
|
156
152
|
if (item.type === "arrow") {
|
|
157
153
|
const resolved = resolveSceneArrow(item, scene);
|
|
158
154
|
const radius = Math.max(10, (item.props.fontSize ?? 14) * .9) / Math.min(1, camera.zoom);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { selectionBounds } from "../model/geometry.js";
|
|
2
2
|
import { sceneItemToItem } from "../model/scene.js";
|
|
3
3
|
import { alignFrames, distributeFrames } from "./align.js";
|
|
4
|
+
import { canTapSelectWithHand } from "./tool.js";
|
|
4
5
|
import { resolveSelectionTransform } from "./selection-transform.js";
|
|
5
6
|
import { memo } from "./state.js";
|
|
6
7
|
//#region src/board/editor/selection.ts
|
|
@@ -17,7 +18,18 @@ function createSelectionModule(ctx) {
|
|
|
17
18
|
return result;
|
|
18
19
|
});
|
|
19
20
|
const bounds = memo(() => [items()], (selected) => selectionBounds(selected.map((item) => item.frame)));
|
|
20
|
-
const
|
|
21
|
+
const rotatesOnTouch = ctx.options.touchRotation !== false;
|
|
22
|
+
const transform = memo(() => [
|
|
23
|
+
items(),
|
|
24
|
+
bounds(),
|
|
25
|
+
rotatesOnTouch || !canTapSelectWithHand(state.hoverPointerType)
|
|
26
|
+
], (selected, rect, rotates) => {
|
|
27
|
+
const resolved = resolveSelectionTransform(registry, selected, rect);
|
|
28
|
+
return resolved && !rotates ? {
|
|
29
|
+
...resolved,
|
|
30
|
+
canRotate: false
|
|
31
|
+
} : resolved;
|
|
32
|
+
});
|
|
21
33
|
function set(ids) {
|
|
22
34
|
state.selection = ids.filter((id) => Boolean(state.base.items[id]));
|
|
23
35
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
//#region src/board/editor/tool.d.ts
|
|
2
2
|
type BoardToolId = "select" | "hand" | "text" | "shape" | "draw" | "arrow" | "frame";
|
|
3
3
|
declare const BOARD_HAND_TAP_SLOP = 8;
|
|
4
|
+
declare const BOARD_MOUSE_TAP_SLOP = 4;
|
|
5
|
+
declare const BOARD_TAP_MAX_MS = 300;
|
|
6
|
+
declare function boardTapSlop(pointerType: string): number;
|
|
4
7
|
declare function defaultBoardTool(isMobile: boolean): BoardToolId;
|
|
5
8
|
declare function canTapSelectWithHand(pointerType: string): boolean;
|
|
6
9
|
declare function isWithinHandTapSlop(dx: number, dy: number): boolean;
|
|
7
10
|
declare function isContinuousBoardTool(tool: BoardToolId): boolean;
|
|
8
11
|
declare function isCreationBoardTool(tool: BoardToolId): boolean;
|
|
9
12
|
//#endregion
|
|
10
|
-
export { BOARD_HAND_TAP_SLOP, BoardToolId, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
|
|
13
|
+
export { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, BoardToolId, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
//#region src/board/editor/tool.ts
|
|
2
2
|
const BOARD_HAND_TAP_SLOP = 8;
|
|
3
|
+
const BOARD_MOUSE_TAP_SLOP = 4;
|
|
4
|
+
const BOARD_TAP_MAX_MS = 300;
|
|
5
|
+
function boardTapSlop(pointerType) {
|
|
6
|
+
return pointerType === "mouse" ? 4 : 8;
|
|
7
|
+
}
|
|
3
8
|
function defaultBoardTool(isMobile) {
|
|
4
9
|
return isMobile ? "hand" : "select";
|
|
5
10
|
}
|
|
@@ -16,4 +21,4 @@ function isCreationBoardTool(tool) {
|
|
|
16
21
|
return tool === "text" || tool === "shape" || tool === "draw" || tool === "arrow" || tool === "frame";
|
|
17
22
|
}
|
|
18
23
|
//#endregion
|
|
19
|
-
export { BOARD_HAND_TAP_SLOP, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
|
|
24
|
+
export { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
|
|
@@ -5,7 +5,15 @@ type BoardAssetKeySource = {
|
|
|
5
5
|
kind: "file" | "url";
|
|
6
6
|
media: BoardAssetMedia;
|
|
7
7
|
value: string;
|
|
8
|
+
fit?: "cover";
|
|
8
9
|
};
|
|
10
|
+
/** `src` is a Space file path or a URL; `fit: "cover"` sizes an image by its short edge. */
|
|
11
|
+
type BoardAssetRef = {
|
|
12
|
+
src: string;
|
|
13
|
+
media?: "image" | "video";
|
|
14
|
+
fit?: "contain" | "cover";
|
|
15
|
+
};
|
|
16
|
+
declare function boardAssetRefKey({ src, media, fit }: BoardAssetRef): string | null;
|
|
9
17
|
declare function imageAssetKey(item: BoardSceneItem): string | null;
|
|
10
18
|
declare function boardAssetKey(item: BoardSceneItem): string | null;
|
|
11
19
|
declare function boardAssetKeySource(key: string): BoardAssetKeySource | null;
|
|
@@ -13,4 +21,4 @@ declare function boardTexturePixels(item: BoardSceneItem, scale: number): number
|
|
|
13
21
|
declare const BOARD_THUMBNAIL_EDGE = 64;
|
|
14
22
|
declare function boardTextureEdge(item: BoardSceneItem, scale: number): number;
|
|
15
23
|
//#endregion
|
|
16
|
-
export { BOARD_THUMBNAIL_EDGE, BoardAssetKeySource, BoardAssetMedia, boardAssetKey, boardAssetKeySource, boardTextureEdge, boardTexturePixels, imageAssetKey };
|
|
24
|
+
export { BOARD_THUMBNAIL_EDGE, BoardAssetKeySource, BoardAssetMedia, BoardAssetRef, boardAssetKey, boardAssetKeySource, boardAssetRefKey, boardTextureEdge, boardTexturePixels, imageAssetKey };
|