@neta-art/cohub 9.0.0 → 10.0.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 +100 -103
- package/dist/board/editor/align.d.ts +11 -0
- package/dist/board/editor/align.js +72 -0
- package/dist/board/editor/animation.d.ts +14 -0
- package/dist/board/editor/animation.js +172 -0
- package/dist/board/editor/camera.d.ts +32 -0
- package/dist/board/editor/camera.js +137 -0
- package/dist/board/editor/clipboard.d.ts +41 -0
- package/dist/board/editor/clipboard.js +122 -0
- package/dist/board/editor/connection-ports.d.ts +19 -0
- package/dist/board/editor/connection-ports.js +85 -0
- package/dist/board/editor/context.d.ts +111 -0
- package/dist/board/editor/context.js +30 -0
- package/dist/board/editor/copy.d.ts +17 -0
- package/dist/board/editor/copy.js +116 -0
- package/dist/board/editor/creation.d.ts +47 -0
- package/dist/board/editor/creation.js +224 -0
- package/dist/board/editor/deletion.d.ts +9 -0
- package/dist/board/editor/deletion.js +90 -0
- package/dist/board/editor/document-edits.d.ts +22 -0
- package/dist/board/editor/document-edits.js +193 -0
- package/dist/board/editor/document.d.ts +35 -0
- package/dist/board/editor/document.js +255 -0
- package/dist/board/editor/editor.d.ts +210 -0
- package/dist/board/editor/editor.js +429 -0
- package/dist/board/editor/index.d.ts +18 -0
- package/dist/board/editor/index.js +12 -0
- package/dist/board/editor/interaction.d.ts +115 -0
- package/dist/board/editor/interaction.js +584 -0
- package/dist/board/editor/query.d.ts +27 -0
- package/dist/board/editor/query.js +201 -0
- package/dist/board/editor/remote.d.ts +11 -0
- package/dist/board/editor/remote.js +55 -0
- package/dist/board/editor/selection-transform.d.ts +24 -0
- package/dist/board/editor/selection-transform.js +81 -0
- package/dist/board/editor/selection.d.ts +33 -0
- package/dist/board/editor/selection.js +196 -0
- package/dist/board/editor/snapping.d.ts +33 -0
- package/dist/board/editor/snapping.js +75 -0
- package/dist/board/editor/snapshots.d.ts +27 -0
- package/dist/board/editor/snapshots.js +132 -0
- package/dist/board/editor/spatial.d.ts +41 -0
- package/dist/board/editor/spatial.js +191 -0
- package/dist/board/editor/state.d.ts +12 -0
- package/dist/board/editor/state.js +49 -0
- package/dist/board/editor/tool.d.ts +10 -0
- package/dist/board/editor/tool.js +19 -0
- package/dist/board/editor/wheel.d.ts +5 -0
- package/dist/board/editor/wheel.js +17 -0
- package/dist/board/export/index.d.ts +5 -4
- package/dist/board/export/index.js +4 -3
- package/dist/board/export/node/index.d.ts +55 -0
- package/dist/board/{headless → export/node}/index.js +12 -12
- package/dist/board/export/node/sketch.d.ts +19 -0
- package/dist/board/{headless → export/node}/sketch.js +6 -6
- package/dist/board/export/scene.d.ts +6 -4
- package/dist/board/export/scene.js +4 -3
- package/dist/board/{animation.d.ts → model/animation.d.ts} +4 -4
- package/dist/board/{animation.js → model/animation.js} +3 -3
- package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
- package/dist/board/{core → model}/arrow-geometry.js +2 -2
- package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
- package/dist/board/{core → model}/draw-geometry.js +1 -1
- package/dist/board/model/draw-input.d.ts +14 -0
- package/dist/board/model/draw-input.js +28 -0
- package/dist/board/{core → model}/export-assets.d.ts +1 -1
- package/dist/board/{core → model}/export-assets.js +1 -1
- package/dist/board/{core → model}/export-plan.d.ts +2 -2
- package/dist/board/{core → model}/export-plan.js +2 -2
- package/dist/board/{core → model}/file-preview.d.ts +1 -1
- package/dist/board/{core → model}/file-preview.js +1 -1
- package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
- package/dist/board/{core → model}/file-snapshot.js +1 -1
- package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
- package/dist/board/{geometry.js → model/geometry.js} +1 -1
- package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
- package/dist/board/{image-key.js → model/image-key.js} +1 -1
- package/dist/board/model/index.d.ts +35 -0
- package/dist/board/{index.js → model/index.js} +26 -22
- package/dist/board/model/items/builtin.d.ts +5 -0
- package/dist/board/model/items/builtin.js +119 -0
- package/dist/board/model/items/create.d.ts +59 -0
- package/dist/board/model/items/create.js +271 -0
- package/dist/board/model/items/definition.d.ts +35 -0
- package/dist/board/model/items/definition.js +18 -0
- package/dist/board/model/items/id.d.ts +5 -0
- package/dist/board/model/items/id.js +11 -0
- package/dist/board/model/items/index.d.ts +6 -0
- package/dist/board/model/items/index.js +6 -0
- package/dist/board/model/items/registry.d.ts +35 -0
- package/dist/board/model/items/registry.js +84 -0
- package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +2 -2
- package/dist/board/{media-playback.js → model/media-playback.js} +1 -1
- package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
- package/dist/board/{media.js → model/media.js} +1 -1
- package/dist/board/{core → model}/palette.d.ts +1 -1
- package/dist/board/{core → model}/palette.js +1 -1
- package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
- package/dist/board/{presets.js → model/presets.js} +1 -1
- package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
- package/dist/board/{replay.js → model/replay.js} +2 -2
- package/dist/board/{core → model}/scene.d.ts +1 -1
- package/dist/board/{core → model}/scene.js +1 -1
- package/dist/board/model/shape-types.d.ts +8 -0
- package/dist/board/model/shape-types.js +8 -0
- package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
- package/dist/board/{task.js → model/task.js} +3 -3
- package/dist/board/{core → model}/text-metrics.d.ts +1 -1
- package/dist/board/{core → model}/text-metrics.js +1 -1
- package/dist/board/{core → model}/tool-styles.d.ts +1 -1
- package/dist/board/{core → model}/tool-styles.js +1 -1
- package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
- package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
- package/dist/board/render/board-background.d.ts +1 -1
- package/dist/board/render/clip.d.ts +1 -1
- package/dist/board/render/index.d.ts +2 -2
- package/dist/board/render/index.js +2 -2
- package/dist/board/render/media-interaction.d.ts +2 -2
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.d.ts +1 -1
- package/dist/board/render/palette.js +1 -1
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +11 -6
- package/dist/board/render/renderers/board-renderer-registry.js +15 -15
- package/dist/board/render/renderers/draw-card-renderer.js +1 -1
- package/dist/board/render/renderers/far-plate.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +2 -2
- package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +1 -1
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
- package/dist/board/render/stroke.d.ts +1 -1
- package/dist/board/render/stroke.js +1 -1
- package/dist/board/render/text-measurement.js +1 -1
- package/dist/board/replica/index.d.ts +70 -0
- package/dist/board/replica/index.js +276 -0
- package/dist/board/stage/assets.d.ts +41 -0
- package/dist/board/stage/assets.js +350 -0
- package/dist/board/stage/index.d.ts +8 -0
- package/dist/board/stage/index.js +8 -0
- package/dist/board/stage/lru.d.ts +32 -0
- package/dist/board/stage/lru.js +29 -0
- package/dist/board/stage/pointer.d.ts +8 -0
- package/dist/board/stage/pointer.js +33 -0
- package/dist/board/stage/scene.d.ts +85 -0
- package/dist/board/stage/scene.js +512 -0
- package/dist/board/stage/stage.d.ts +66 -0
- package/dist/board/stage/stage.js +568 -0
- package/dist/board/stage/texture-pool.d.ts +24 -0
- package/dist/board/stage/texture-pool.js +154 -0
- package/dist/board/stage/views.d.ts +38 -0
- package/dist/board/stage/views.js +93 -0
- package/dist/chunks/environment.d.ts +25 -1
- package/dist/chunks/http.d.ts +99 -66
- package/dist/chunks/http.js +214 -207
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +20 -4
- package/dist/media.d.ts +1 -10
- package/dist/media.js +16 -27
- package/dist/protocol/dist/board-patch.js +1 -1
- package/dist/protocol/dist/board.d.ts +28 -2
- package/dist/protocol/dist/board.js +3 -1
- package/dist/protocol/dist/core/media.d.ts +5 -0
- package/dist/protocol/dist/core/media.js +39 -0
- package/dist/protocol/dist/index.d.ts +5 -5
- package/dist/types.d.ts +1 -1
- package/package.json +26 -14
- package/dist/board/core/shape-definition.d.ts +0 -23
- package/dist/board/core/shape-definition.js +0 -45
- package/dist/board/core/shape-types.d.ts +0 -38
- package/dist/board/core/shape-types.js +0 -22
- package/dist/board/headless/index.d.ts +0 -55
- package/dist/board/headless/sketch.d.ts +0 -19
- package/dist/board/index.d.ts +0 -30
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { clampZoom, normalizeRotation, panBy, rectCenter, rectsIntersect, resizeFrame, rotateFrames, scaleFrames, screenPoint, selectionBounds, worldPoint, zoomAround } from "../model/geometry.js";
|
|
2
|
+
import { resolveSceneArrow } from "../model/arrow-geometry.js";
|
|
3
|
+
import { appendBoardDrawSamples } from "../model/draw-input.js";
|
|
4
|
+
import { createBoardItemId } from "../model/items/id.js";
|
|
5
|
+
import { connectionPortAt, connectionPorts } from "./connection-ports.js";
|
|
6
|
+
import { canTapSelectWithHand, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
|
|
7
|
+
import { selectionTransformControlAt } from "./selection-transform.js";
|
|
8
|
+
import { computeSnap } from "./snapping.js";
|
|
9
|
+
import { memo } from "./state.js";
|
|
10
|
+
//#region src/board/editor/interaction.ts
|
|
11
|
+
const DRAG_THRESHOLD = 3;
|
|
12
|
+
const SNAP_THRESHOLD = 8;
|
|
13
|
+
const NO_IDS = /* @__PURE__ */ new Set();
|
|
14
|
+
function angleFrom(center, point) {
|
|
15
|
+
return Math.atan2(point.y - center.y, point.x - center.x) * 180 / Math.PI;
|
|
16
|
+
}
|
|
17
|
+
function createInteractionModule(ctx) {
|
|
18
|
+
const { state, options, registry } = ctx;
|
|
19
|
+
const activePointers = /* @__PURE__ */ new Map();
|
|
20
|
+
let pinch = null;
|
|
21
|
+
const marquee = memo(() => [state.interaction], (interaction) => {
|
|
22
|
+
if (interaction.type !== "brushing") return null;
|
|
23
|
+
const { start, current } = interaction;
|
|
24
|
+
return {
|
|
25
|
+
x: Math.min(start.x, current.x),
|
|
26
|
+
y: Math.min(start.y, current.y),
|
|
27
|
+
width: Math.abs(current.x - start.x),
|
|
28
|
+
height: Math.abs(current.y - start.y)
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const hoveredControl = memo(() => [
|
|
32
|
+
state.interaction,
|
|
33
|
+
state.tool,
|
|
34
|
+
state.hoverPoint,
|
|
35
|
+
state.hoverPointerType,
|
|
36
|
+
state.camera.zoom,
|
|
37
|
+
ctx.selection.transform()
|
|
38
|
+
], (interaction, tool, hoverPoint, pointerType, zoom, transform) => interaction.type === "idle" && !pinch && tool === "select" && hoverPoint ? selectionTransformControlAt(transform, hoverPoint, zoom, pointerType) : null);
|
|
39
|
+
function gridSnapSize() {
|
|
40
|
+
const grid = state.base.board.grid;
|
|
41
|
+
return grid?.visible === true ? grid.size ?? 0 : 0;
|
|
42
|
+
}
|
|
43
|
+
function snapThreshold() {
|
|
44
|
+
return SNAP_THRESHOLD / Math.max(state.camera.zoom, 1e-4);
|
|
45
|
+
}
|
|
46
|
+
function computeTranslationSnap(moved, skip) {
|
|
47
|
+
const movingBounds = selectionBounds([...moved.values()]);
|
|
48
|
+
if (!movingBounds) return {
|
|
49
|
+
dx: 0,
|
|
50
|
+
dy: 0,
|
|
51
|
+
guides: []
|
|
52
|
+
};
|
|
53
|
+
const view = ctx.camera.visibleRect();
|
|
54
|
+
const targets = [];
|
|
55
|
+
for (const id of view ? ctx.query.idsInRect(view) : state.scene.items.map((item) => item.id)) {
|
|
56
|
+
if (skip.has(id)) continue;
|
|
57
|
+
const item = state.scene.get(id);
|
|
58
|
+
if (item && registry.capabilities(item).canSnap) targets.push(registry.bounds(item));
|
|
59
|
+
}
|
|
60
|
+
return computeSnap(movingBounds, targets, {
|
|
61
|
+
threshold: snapThreshold(),
|
|
62
|
+
gridSize: gridSnapSize()
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function reparentDropped(ids) {
|
|
66
|
+
const changes = /* @__PURE__ */ new Map();
|
|
67
|
+
const moving = ctx.query.withSubtrees(ids);
|
|
68
|
+
for (const id of ids) {
|
|
69
|
+
const item = state.scene.get(id);
|
|
70
|
+
if (!item || item.type === "arrow") continue;
|
|
71
|
+
const parent = ctx.query.frameAt(rectCenter(item.frame), moving)?.id;
|
|
72
|
+
if (parent === item.parent) continue;
|
|
73
|
+
const stored = ctx.document.displayItem(id);
|
|
74
|
+
if (!stored) continue;
|
|
75
|
+
changes.set(id, {
|
|
76
|
+
...ctx.document.reparent(stored, item.frame, parent, state.scene.layout.box(id)),
|
|
77
|
+
z: ctx.document.topZ(parent) + changes.size
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return changes;
|
|
81
|
+
}
|
|
82
|
+
function portItemId() {
|
|
83
|
+
const { tool, editingId, interaction, selection, hoverId, scene } = state;
|
|
84
|
+
if (options.readonly || tool !== "select" || editingId) return null;
|
|
85
|
+
if (interaction.type !== "idle" && interaction.type !== "creatingArrow") return null;
|
|
86
|
+
const hoverReveals = !canTapSelectWithHand(state.hoverPointerType);
|
|
87
|
+
const candidateId = selection.length === 1 ? selection[0] : hoverReveals ? hoverId : null;
|
|
88
|
+
if (!candidateId) return null;
|
|
89
|
+
const item = scene.get(candidateId);
|
|
90
|
+
if (!item || ctx.query.isLocked(item) || item.type === "arrow") return null;
|
|
91
|
+
return registry.capabilities(item).canConnect ? item.id : null;
|
|
92
|
+
}
|
|
93
|
+
function visiblePorts() {
|
|
94
|
+
const itemId = portItemId();
|
|
95
|
+
const item = itemId ? state.scene.get(itemId) : null;
|
|
96
|
+
if (!itemId || !item) return [];
|
|
97
|
+
return connectionPorts(item.frame, state.camera.zoom).map((port) => ({
|
|
98
|
+
...port,
|
|
99
|
+
itemId
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function connectionPortAt$1(point, pointerType) {
|
|
103
|
+
const itemId = portItemId();
|
|
104
|
+
const item = itemId ? state.scene.get(itemId) : null;
|
|
105
|
+
if (!itemId || !item) return null;
|
|
106
|
+
const port = connectionPortAt(item.frame, point, state.camera.zoom, pointerType);
|
|
107
|
+
return port ? {
|
|
108
|
+
...port,
|
|
109
|
+
itemId
|
|
110
|
+
} : null;
|
|
111
|
+
}
|
|
112
|
+
function beginPinch() {
|
|
113
|
+
const [a, b] = [...activePointers.values()];
|
|
114
|
+
if (!a || !b) return;
|
|
115
|
+
pinch = {
|
|
116
|
+
distance: Math.max(1, Math.hypot(a.x - b.x, a.y - b.y)),
|
|
117
|
+
midpoint: screenPoint((a.x + b.x) / 2, (a.y + b.y) / 2),
|
|
118
|
+
zoom: state.camera.zoom
|
|
119
|
+
};
|
|
120
|
+
state.interaction = { type: "idle" };
|
|
121
|
+
}
|
|
122
|
+
function brush(event, additive) {
|
|
123
|
+
state.interaction = {
|
|
124
|
+
type: "brushing",
|
|
125
|
+
start: event.world,
|
|
126
|
+
current: event.world,
|
|
127
|
+
additive,
|
|
128
|
+
baseSelection: state.selection
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function arrowGesture(start, current, startItemId, startSide) {
|
|
132
|
+
const { arrow } = state.toolStyles;
|
|
133
|
+
return {
|
|
134
|
+
type: "creatingArrow",
|
|
135
|
+
id: createBoardItemId(),
|
|
136
|
+
start,
|
|
137
|
+
current,
|
|
138
|
+
startItemId,
|
|
139
|
+
startSide,
|
|
140
|
+
targetItemId: null,
|
|
141
|
+
color: arrow.color,
|
|
142
|
+
size: arrow.size
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function pointerDown(event) {
|
|
146
|
+
ctx.camera.cancelAnimation();
|
|
147
|
+
state.hoverPoint = event.world;
|
|
148
|
+
state.hoverPointerType = event.pointerType;
|
|
149
|
+
const { interaction, tool, spaceHeld, toolStyles, camera } = state;
|
|
150
|
+
if (interaction.type === "drawing" && interaction.pointerId !== event.pointerId) return;
|
|
151
|
+
activePointers.set(event.pointerId, event.screen);
|
|
152
|
+
if (activePointers.size === 2) {
|
|
153
|
+
beginPinch();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (activePointers.size > 2) return;
|
|
157
|
+
const additive = event.shiftKey || event.metaKey || event.ctrlKey;
|
|
158
|
+
if (tool === "hand" || spaceHeld || event.button === 1) {
|
|
159
|
+
const tapSelection = tool === "hand" && !spaceHeld && event.button !== 1 && canTapSelectWithHand(event.pointerType) ? { targetId: ctx.query.itemAt(event.world)?.id ?? null } : null;
|
|
160
|
+
state.interaction = {
|
|
161
|
+
type: "panning",
|
|
162
|
+
start: event.screen,
|
|
163
|
+
origin: { ...camera },
|
|
164
|
+
moved: false,
|
|
165
|
+
tapSelection
|
|
166
|
+
};
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (options.readonly) {
|
|
170
|
+
const hit = ctx.query.itemAt(event.world);
|
|
171
|
+
if (!hit) return brush(event, additive);
|
|
172
|
+
if (additive) ctx.selection.toggle(hit.id);
|
|
173
|
+
else state.selection = [hit.id];
|
|
174
|
+
state.interaction = { type: "idle" };
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
switch (tool) {
|
|
178
|
+
case "draw": {
|
|
179
|
+
const points = [];
|
|
180
|
+
appendBoardDrawSamples(points, [event], camera.zoom);
|
|
181
|
+
state.interaction = {
|
|
182
|
+
type: "drawing",
|
|
183
|
+
id: createBoardItemId(),
|
|
184
|
+
pointerId: event.pointerId,
|
|
185
|
+
points,
|
|
186
|
+
color: toolStyles.draw.color,
|
|
187
|
+
size: toolStyles.draw.size
|
|
188
|
+
};
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case "arrow":
|
|
192
|
+
state.interaction = arrowGesture(event.world, event.world, ctx.query.connectTargetAt(event.world), null);
|
|
193
|
+
return;
|
|
194
|
+
case "shape":
|
|
195
|
+
case "frame":
|
|
196
|
+
state.interaction = {
|
|
197
|
+
type: "creatingBox",
|
|
198
|
+
id: createBoardItemId(),
|
|
199
|
+
kind: tool,
|
|
200
|
+
start: event.world,
|
|
201
|
+
current: event.world,
|
|
202
|
+
color: tool === "shape" ? toolStyles.shape.color : toolStyles.frame.color,
|
|
203
|
+
geometry: toolStyles.shape.geometry
|
|
204
|
+
};
|
|
205
|
+
return;
|
|
206
|
+
case "text":
|
|
207
|
+
ctx.creation.beginTextDraft(event.world);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const port = connectionPortAt$1(event.world, event.pointerType);
|
|
211
|
+
if (port) {
|
|
212
|
+
state.interaction = arrowGesture(port.point, event.world, port.itemId, port.side);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const arrowHandle = ctx.query.arrowHandleAt(event.world);
|
|
216
|
+
const arrow = ctx.selection.items()[0];
|
|
217
|
+
if (arrowHandle && arrow?.type === "arrow") {
|
|
218
|
+
const { id: _id, frame: _frame, ...origin } = arrow;
|
|
219
|
+
state.interaction = {
|
|
220
|
+
type: "draggingArrowHandle",
|
|
221
|
+
arrowId: arrow.id,
|
|
222
|
+
which: arrowHandle,
|
|
223
|
+
origin,
|
|
224
|
+
targetItemId: null,
|
|
225
|
+
moved: false
|
|
226
|
+
};
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const bounds = ctx.selection.bounds();
|
|
230
|
+
const control = selectionTransformControlAt(ctx.selection.transform(), event.world, camera.zoom, event.pointerType);
|
|
231
|
+
if (control?.kind === "rotate" && bounds) {
|
|
232
|
+
const pivot = rectCenter(bounds);
|
|
233
|
+
state.interaction = {
|
|
234
|
+
type: "rotating",
|
|
235
|
+
pivot,
|
|
236
|
+
startAngle: angleFrom(pivot, event.world),
|
|
237
|
+
current: event.world,
|
|
238
|
+
origin: ctx.selection.framesFor(ctx.selection.movable()),
|
|
239
|
+
moved: false
|
|
240
|
+
};
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (control?.kind === "resize" && bounds) {
|
|
244
|
+
const origin = ctx.selection.framesFor(ctx.selection.movable());
|
|
245
|
+
const single = origin.size === 1 ? { ...[...origin.values()][0] } : null;
|
|
246
|
+
state.interaction = {
|
|
247
|
+
type: "resizing",
|
|
248
|
+
handle: control.handle,
|
|
249
|
+
single,
|
|
250
|
+
bounds,
|
|
251
|
+
origin,
|
|
252
|
+
moved: false
|
|
253
|
+
};
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const item = ctx.query.itemAt(event.world);
|
|
257
|
+
if (!item) return brush(event, additive);
|
|
258
|
+
if (additive) ctx.selection.toggle(item.id);
|
|
259
|
+
else if (!state.selection.includes(item.id)) state.selection = [item.id];
|
|
260
|
+
const movable = ctx.selection.movable();
|
|
261
|
+
state.interaction = movable.length === 0 ? { type: "idle" } : {
|
|
262
|
+
type: "translating",
|
|
263
|
+
start: event.world,
|
|
264
|
+
origin: ctx.selection.framesFor(movable),
|
|
265
|
+
moved: false,
|
|
266
|
+
duplicate: event.altKey
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function pinchMove() {
|
|
270
|
+
const [a, b] = [...activePointers.values()];
|
|
271
|
+
if (!a || !b || !pinch) return;
|
|
272
|
+
const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
273
|
+
const midpoint = screenPoint((a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
274
|
+
const zoomed = zoomAround(state.camera, midpoint, clampZoom(pinch.zoom * (distance / pinch.distance)));
|
|
275
|
+
ctx.camera.setCamera(panBy(zoomed, midpoint.x - pinch.midpoint.x, midpoint.y - pinch.midpoint.y));
|
|
276
|
+
pinch = {
|
|
277
|
+
...pinch,
|
|
278
|
+
midpoint
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function translate(gesture, event) {
|
|
282
|
+
let dx = event.world.x - gesture.start.x;
|
|
283
|
+
let dy = event.world.y - gesture.start.y;
|
|
284
|
+
let current = gesture;
|
|
285
|
+
if (!current.moved) {
|
|
286
|
+
if (Math.hypot(dx, dy) <= DRAG_THRESHOLD / state.camera.zoom) return;
|
|
287
|
+
current = {
|
|
288
|
+
...current,
|
|
289
|
+
moved: true
|
|
290
|
+
};
|
|
291
|
+
if (gesture.duplicate) {
|
|
292
|
+
const clones = ctx.copy.duplicates([...gesture.origin.keys()], 0);
|
|
293
|
+
if (clones.size > 0) {
|
|
294
|
+
ctx.document.writeDraft(clones, true);
|
|
295
|
+
const roots = ctx.copy.rootsOfCopies(clones);
|
|
296
|
+
state.selection = roots;
|
|
297
|
+
current = {
|
|
298
|
+
...current,
|
|
299
|
+
origin: ctx.selection.framesFor(roots),
|
|
300
|
+
duplicate: false
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
state.interaction = current;
|
|
305
|
+
}
|
|
306
|
+
if (event.shiftKey) {
|
|
307
|
+
if (Math.abs(dx) >= Math.abs(dy)) dy = 0;
|
|
308
|
+
else dx = 0;
|
|
309
|
+
}
|
|
310
|
+
const shift = (tx, ty) => {
|
|
311
|
+
const frames = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const [id, frame] of current.origin) frames.set(id, {
|
|
313
|
+
...frame,
|
|
314
|
+
x: frame.x + tx,
|
|
315
|
+
y: frame.y + ty
|
|
316
|
+
});
|
|
317
|
+
return frames;
|
|
318
|
+
};
|
|
319
|
+
let tx = dx;
|
|
320
|
+
let ty = dy;
|
|
321
|
+
if (!event.metaKey && !event.ctrlKey) {
|
|
322
|
+
const snap = computeTranslationSnap(shift(dx, dy), ctx.query.withSubtrees(current.origin.keys()));
|
|
323
|
+
tx += snap.dx;
|
|
324
|
+
ty += snap.dy;
|
|
325
|
+
state.snapGuides = snap.guides;
|
|
326
|
+
} else state.snapGuides = [];
|
|
327
|
+
ctx.document.writeDraft(ctx.selection.placed(shift(tx, ty)));
|
|
328
|
+
}
|
|
329
|
+
function resize(gesture, event) {
|
|
330
|
+
if (!gesture.moved) state.interaction = {
|
|
331
|
+
...gesture,
|
|
332
|
+
moved: true
|
|
333
|
+
};
|
|
334
|
+
const frames = /* @__PURE__ */ new Map();
|
|
335
|
+
if (gesture.single) {
|
|
336
|
+
const id = [...gesture.origin.keys()][0];
|
|
337
|
+
const item = state.scene.get(id);
|
|
338
|
+
const keepAspect = item ? registry.capabilities(item).aspectLocked || event.shiftKey : event.shiftKey;
|
|
339
|
+
frames.set(id, resizeFrame(gesture.single, gesture.handle, event.world, void 0, keepAspect));
|
|
340
|
+
} else {
|
|
341
|
+
const scaled = scaleFrames([...gesture.origin.values()], gesture.bounds, gesture.handle, event.world);
|
|
342
|
+
[...gesture.origin.keys()].forEach((id, index) => {
|
|
343
|
+
const frame = scaled[index];
|
|
344
|
+
if (frame) frames.set(id, frame);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
ctx.document.writeDraft(ctx.selection.placed(frames));
|
|
348
|
+
}
|
|
349
|
+
function rotate(gesture, event) {
|
|
350
|
+
state.interaction = {
|
|
351
|
+
...gesture,
|
|
352
|
+
moved: true,
|
|
353
|
+
current: event.world
|
|
354
|
+
};
|
|
355
|
+
let delta = angleFrom(gesture.pivot, event.world) - gesture.startAngle;
|
|
356
|
+
if (event.shiftKey) delta = Math.round(delta / 15) * 15;
|
|
357
|
+
const rotated = rotateFrames([...gesture.origin.values()], gesture.pivot, delta);
|
|
358
|
+
const frames = /* @__PURE__ */ new Map();
|
|
359
|
+
[...gesture.origin.keys()].forEach((id, index) => {
|
|
360
|
+
const frame = rotated[index];
|
|
361
|
+
if (frame) frames.set(id, {
|
|
362
|
+
...frame,
|
|
363
|
+
rotation: normalizeRotation(frame.rotation)
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
ctx.document.writeDraft(ctx.selection.placed(frames));
|
|
367
|
+
}
|
|
368
|
+
function dragArrowHandle(gesture, event) {
|
|
369
|
+
if (!gesture.moved) state.interaction = {
|
|
370
|
+
...gesture,
|
|
371
|
+
moved: true
|
|
372
|
+
};
|
|
373
|
+
const { arrowId, which, origin } = gesture;
|
|
374
|
+
const { scene } = state;
|
|
375
|
+
const arrow = scene.get(arrowId);
|
|
376
|
+
if (!arrow) return;
|
|
377
|
+
if (which === "mid") {
|
|
378
|
+
const resolved = resolveSceneArrow({
|
|
379
|
+
...arrow,
|
|
380
|
+
props: origin.props
|
|
381
|
+
}, scene);
|
|
382
|
+
const start = resolved.start.point;
|
|
383
|
+
const end = resolved.end.point;
|
|
384
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y) || 1;
|
|
385
|
+
const mid = worldPoint((start.x + end.x) / 2, (start.y + end.y) / 2);
|
|
386
|
+
const bend = ((event.world.x - mid.x) * -(end.y - start.y) + (event.world.y - mid.y) * (end.x - start.x)) / (length * length);
|
|
387
|
+
const route = origin.props.route === "straight" ? "curve" : origin.props.route;
|
|
388
|
+
const props = {
|
|
389
|
+
...origin.props,
|
|
390
|
+
route,
|
|
391
|
+
bend: Math.max(-.85, Math.min(.85, bend))
|
|
392
|
+
};
|
|
393
|
+
ctx.document.writeDraft(/* @__PURE__ */ new Map([[arrowId, {
|
|
394
|
+
...origin,
|
|
395
|
+
props
|
|
396
|
+
}]]));
|
|
397
|
+
state.snapGuides = [];
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const targetItemId = ctx.query.connectTargetAt(event.world, /* @__PURE__ */ new Set([arrowId]));
|
|
401
|
+
let point = event.world;
|
|
402
|
+
if (!targetItemId && !event.metaKey && !event.ctrlKey) {
|
|
403
|
+
const snap = computeSnap({
|
|
404
|
+
x: point.x,
|
|
405
|
+
y: point.y,
|
|
406
|
+
width: 0,
|
|
407
|
+
height: 0
|
|
408
|
+
}, scene.items.filter((item) => item.id !== arrowId && registry.capabilities(item).canSnap).map((item) => registry.bounds(item)), {
|
|
409
|
+
threshold: snapThreshold(),
|
|
410
|
+
gridSize: gridSnapSize()
|
|
411
|
+
});
|
|
412
|
+
point = worldPoint(point.x + snap.dx, point.y + snap.dy);
|
|
413
|
+
state.snapGuides = snap.guides;
|
|
414
|
+
} else state.snapGuides = [];
|
|
415
|
+
state.interaction = {
|
|
416
|
+
...state.interaction,
|
|
417
|
+
targetItemId
|
|
418
|
+
};
|
|
419
|
+
const end = ctx.creation.arrowEnd(point, targetItemId, scene.layout.matrix(arrowId));
|
|
420
|
+
ctx.document.writeDraft(/* @__PURE__ */ new Map([[arrowId, {
|
|
421
|
+
...origin,
|
|
422
|
+
props: {
|
|
423
|
+
...origin.props,
|
|
424
|
+
[which]: end
|
|
425
|
+
}
|
|
426
|
+
}]]), true);
|
|
427
|
+
}
|
|
428
|
+
function pointerMove(event) {
|
|
429
|
+
state.hoverPoint = event.world;
|
|
430
|
+
state.hoverPointerType = event.pointerType;
|
|
431
|
+
if (activePointers.has(event.pointerId)) activePointers.set(event.pointerId, event.screen);
|
|
432
|
+
if (pinch && activePointers.size >= 2) return pinchMove();
|
|
433
|
+
const interaction = state.interaction;
|
|
434
|
+
switch (interaction.type) {
|
|
435
|
+
case "idle":
|
|
436
|
+
state.hoverId = isCreationBoardTool(state.tool) || hoveredControl() ? null : ctx.query.itemAt(event.world)?.id ?? null;
|
|
437
|
+
return;
|
|
438
|
+
case "panning": {
|
|
439
|
+
const dx = event.screen.x - interaction.start.x;
|
|
440
|
+
const dy = event.screen.y - interaction.start.y;
|
|
441
|
+
if (interaction.tapSelection && !interaction.moved && isWithinHandTapSlop(dx, dy)) return;
|
|
442
|
+
if (!interaction.moved) state.interaction = {
|
|
443
|
+
...interaction,
|
|
444
|
+
moved: true
|
|
445
|
+
};
|
|
446
|
+
ctx.camera.setCamera(panBy(interaction.origin, dx, dy));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
case "translating": return translate(interaction, event);
|
|
450
|
+
case "resizing": return resize(interaction, event);
|
|
451
|
+
case "rotating": return rotate(interaction, event);
|
|
452
|
+
case "draggingArrowHandle": return dragArrowHandle(interaction, event);
|
|
453
|
+
case "brushing": {
|
|
454
|
+
state.interaction = {
|
|
455
|
+
...interaction,
|
|
456
|
+
current: event.world
|
|
457
|
+
};
|
|
458
|
+
const rect = marquee();
|
|
459
|
+
if (!rect) return;
|
|
460
|
+
const hits = ctx.query.idsInRect(rect).filter((id) => {
|
|
461
|
+
const item = state.scene.get(id);
|
|
462
|
+
return item && !item.parent && rectsIntersect(ctx.query.boundsOf(item), rect);
|
|
463
|
+
});
|
|
464
|
+
state.selection = interaction.additive ? [.../* @__PURE__ */ new Set([...interaction.baseSelection, ...hits])] : hits;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
case "drawing":
|
|
468
|
+
if (event.pointerId !== interaction.pointerId || (event.buttons & 1) === 0) return;
|
|
469
|
+
if (appendBoardDrawSamples(interaction.points, event.samples ?? [event], state.camera.zoom)) state.interaction = { ...interaction };
|
|
470
|
+
return;
|
|
471
|
+
case "creatingArrow": {
|
|
472
|
+
const exclude = interaction.startItemId ? /* @__PURE__ */ new Set([interaction.startItemId]) : NO_IDS;
|
|
473
|
+
state.interaction = {
|
|
474
|
+
...interaction,
|
|
475
|
+
current: event.world,
|
|
476
|
+
targetItemId: ctx.query.connectTargetAt(event.world, exclude)
|
|
477
|
+
};
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
case "creatingBox":
|
|
481
|
+
state.interaction = {
|
|
482
|
+
...interaction,
|
|
483
|
+
current: event.world
|
|
484
|
+
};
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function pointerUp(event) {
|
|
489
|
+
state.hoverPoint = event.world;
|
|
490
|
+
state.hoverPointerType = event.pointerType;
|
|
491
|
+
activePointers.delete(event.pointerId);
|
|
492
|
+
if (activePointers.size < 2) pinch = null;
|
|
493
|
+
if (activePointers.size > 0) return;
|
|
494
|
+
const gesture = state.interaction;
|
|
495
|
+
if (gesture.type === "drawing" && gesture.pointerId !== event.pointerId) return;
|
|
496
|
+
state.snapGuides = [];
|
|
497
|
+
state.interaction = { type: "idle" };
|
|
498
|
+
switch (gesture.type) {
|
|
499
|
+
case "panning":
|
|
500
|
+
if (gesture.tapSelection && !gesture.moved && !event.cancelled) {
|
|
501
|
+
const targetId = gesture.tapSelection.targetId;
|
|
502
|
+
state.selection = targetId && state.scene.get(targetId) ? [targetId] : [];
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
case "translating":
|
|
506
|
+
if (gesture.moved) {
|
|
507
|
+
for (const [id, item] of reparentDropped(ctx.query.rootsOf(state.selection))) ctx.document.setDraftItem(id, item);
|
|
508
|
+
ctx.document.commit();
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
case "resizing":
|
|
512
|
+
case "rotating":
|
|
513
|
+
case "draggingArrowHandle":
|
|
514
|
+
if (gesture.moved) ctx.document.commit();
|
|
515
|
+
break;
|
|
516
|
+
case "brushing":
|
|
517
|
+
if (!gesture.additive && Math.hypot(gesture.current.x - gesture.start.x, gesture.current.y - gesture.start.y) <= 1 / state.camera.zoom) state.selection = [];
|
|
518
|
+
break;
|
|
519
|
+
case "drawing": {
|
|
520
|
+
const pressure = gesture.points.at(-1)?.p ?? event.pressure;
|
|
521
|
+
appendBoardDrawSamples(gesture.points, [{
|
|
522
|
+
world: event.world,
|
|
523
|
+
pressure
|
|
524
|
+
}], state.camera.zoom);
|
|
525
|
+
ctx.creation.commitDraw(gesture.id, gesture.points, gesture.color, gesture.size);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case "creatingArrow":
|
|
529
|
+
ctx.creation.commitArrow(gesture, event.cancelled);
|
|
530
|
+
break;
|
|
531
|
+
case "creatingBox": ctx.creation.commitBox(gesture);
|
|
532
|
+
}
|
|
533
|
+
ctx.remote.flushPending();
|
|
534
|
+
}
|
|
535
|
+
function pointerLeave() {
|
|
536
|
+
if (state.interaction.type !== "idle") return;
|
|
537
|
+
state.hoverPoint = null;
|
|
538
|
+
state.hoverId = null;
|
|
539
|
+
}
|
|
540
|
+
function cancel() {
|
|
541
|
+
activePointers.clear();
|
|
542
|
+
pinch = null;
|
|
543
|
+
state.snapGuides = [];
|
|
544
|
+
state.interaction = { type: "idle" };
|
|
545
|
+
ctx.document.discardDraft();
|
|
546
|
+
ctx.remote.flushPending();
|
|
547
|
+
}
|
|
548
|
+
const ports = memo(() => [
|
|
549
|
+
state.scene,
|
|
550
|
+
state.selection,
|
|
551
|
+
state.hoverId,
|
|
552
|
+
state.hoverPointerType,
|
|
553
|
+
state.interaction,
|
|
554
|
+
state.tool,
|
|
555
|
+
state.editingId,
|
|
556
|
+
state.camera.zoom
|
|
557
|
+
], (..._inputs) => visiblePorts().map((port) => ({
|
|
558
|
+
itemId: port.itemId,
|
|
559
|
+
x: port.point.x,
|
|
560
|
+
y: port.point.y,
|
|
561
|
+
radius: 4
|
|
562
|
+
})));
|
|
563
|
+
return {
|
|
564
|
+
marquee,
|
|
565
|
+
hoveredControl,
|
|
566
|
+
ports,
|
|
567
|
+
hoveredPort: memo(() => [ports(), state.hoverPoint], (_ports, hoverPoint) => {
|
|
568
|
+
if (!hoverPoint) return null;
|
|
569
|
+
const port = connectionPortAt$1(hoverPoint, state.hoverPointerType);
|
|
570
|
+
return port ? {
|
|
571
|
+
x: port.point.x,
|
|
572
|
+
y: port.point.y
|
|
573
|
+
} : null;
|
|
574
|
+
}),
|
|
575
|
+
pointerDown,
|
|
576
|
+
pointerMove,
|
|
577
|
+
pointerUp,
|
|
578
|
+
pointerLeave,
|
|
579
|
+
cancel,
|
|
580
|
+
destroy: () => activePointers.clear()
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
export { createInteractionModule };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Rect, WorldPoint } from "../model/geometry.js";
|
|
2
|
+
import { BoardSceneItem } from "../model/scene.js";
|
|
3
|
+
import "../model/index.js";
|
|
4
|
+
import { EditorContext } from "./context.js";
|
|
5
|
+
//#region src/board/editor/query.d.ts
|
|
6
|
+
declare function createQueryModule(ctx: EditorContext): {
|
|
7
|
+
invalidate: (ids?: readonly string[]) => void;
|
|
8
|
+
boundsOf: (item: BoardSceneItem) => Rect;
|
|
9
|
+
itemById: (id: string) => BoardSceneItem | null;
|
|
10
|
+
isLocked: (item: {
|
|
11
|
+
locked?: boolean;
|
|
12
|
+
}) => boolean;
|
|
13
|
+
unlockedIds: (ids: Iterable<string>) => string[];
|
|
14
|
+
rootsOf: (ids: Iterable<string>) => string[];
|
|
15
|
+
withSubtrees: (ids: Iterable<string>) => Set<string>;
|
|
16
|
+
mediaIdsForPath: (path: string) => ReadonlySet<string>;
|
|
17
|
+
contentBounds: (ids?: Iterable<string>) => Rect | null;
|
|
18
|
+
idsInRect: (rect: Rect) => string[];
|
|
19
|
+
frameAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
|
|
20
|
+
itemAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
|
|
21
|
+
labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
|
|
22
|
+
arrowHandleAt: (point: WorldPoint) => "start" | "end" | "mid" | null;
|
|
23
|
+
connectTargetAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => string | null;
|
|
24
|
+
};
|
|
25
|
+
type QueryModule = ReturnType<typeof createQueryModule>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { QueryModule, createQueryModule };
|