@lazlon-platform/html-editor 0.5.0 → 0.7.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.
Files changed (64) hide show
  1. package/lib/hooks/actions.ts +136 -87
  2. package/lib/hooks/batch.ts +24 -10
  3. package/lib/hooks/index.ts +7 -6
  4. package/lib/hooks/page.ts +2 -4
  5. package/lib/hooks/pointer/useMovePoint.ts +100 -0
  6. package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +47 -39
  7. package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
  8. package/lib/hooks/pointer/useResize/index.ts +31 -0
  9. package/lib/hooks/pointer/useResize/multi.ts +161 -0
  10. package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
  11. package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
  12. package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
  13. package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
  14. package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
  15. package/lib/hooks/pointer/useRotation.ts +102 -0
  16. package/lib/hooks/pointer/{selector.ts → useSelector.ts} +18 -3
  17. package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
  18. package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
  19. package/lib/hooks/textMarks.ts +30 -21
  20. package/lib/lib/googleFonts.ts +1 -5
  21. package/lib/model/editor.ts +31 -13
  22. package/lib/model/geometry/math.ts +128 -1
  23. package/lib/model/history.ts +10 -13
  24. package/lib/model/index.ts +15 -10
  25. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  26. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  27. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  28. package/lib/model/node/{image.ts → imageNode.ts} +6 -12
  29. package/lib/model/node/lineNode.ts +80 -0
  30. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  31. package/lib/model/node/shapeNode/shape.ts +96 -0
  32. package/lib/model/node/{text.ts → textNode.ts} +9 -24
  33. package/lib/model/node.ts +27 -32
  34. package/lib/model/page.ts +4 -4
  35. package/lib/model/traversal.ts +1 -1
  36. package/lib/ui/extractor.ts +3 -3
  37. package/lib/ui/index.ts +2 -4
  38. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +10 -7
  39. package/lib/ui/node/GroupContent.tsx +1 -1
  40. package/lib/ui/node/ImageContent.tsx +1 -1
  41. package/lib/ui/node/LineContent.tsx +30 -0
  42. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  43. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  44. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  45. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  46. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  47. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  48. package/lib/ui/node/TextContent.tsx +1 -1
  49. package/lib/ui/selection.ts +6 -5
  50. package/package.json +1 -1
  51. package/lib/hooks/pointer/resize.ts +0 -247
  52. package/lib/hooks/pointer/rotation.ts +0 -103
  53. package/lib/model/node/shape/arrow.ts +0 -50
  54. package/lib/model/node/shape/ellipse.ts +0 -26
  55. package/lib/model/node/shape/polygon.ts +0 -130
  56. package/lib/model/node/shape/star.ts +0 -91
  57. package/lib/ui/node/ArrowContent.tsx +0 -60
  58. package/lib/ui/node/EllipseContent.tsx +0 -49
  59. package/lib/ui/node/PolygonContent.tsx +0 -81
  60. package/lib/ui/node/StarContent.tsx +0 -60
  61. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  62. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  63. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  64. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,21 +1,36 @@
1
1
  import { useComputed } from "react-bolt"
2
+ import {
3
+ flattenNodes,
4
+ GroupNode,
5
+ type HistoryAction,
6
+ type Node,
7
+ type Page,
8
+ } from "../model"
2
9
  import type { Editor, NodeConstructor } from "../model/editor"
3
- import type { HistoryAction } from "../model/history"
4
- import type { Node, NodeProps } from "../model/node"
5
- import { GroupNode } from "../model/node/group"
6
- import type { Page } from "../model/page"
7
- import { flattenNodes } from "../model/traversal"
10
+ import {
11
+ boxBounds,
12
+ deg,
13
+ floatNorm,
14
+ rectCenter,
15
+ rotatePoint,
16
+ } from "../model/geometry/math"
8
17
  import { selectionBox } from "../ui/selection"
9
18
  import { useBatchSet } from "./batch"
10
19
  import { useEditor } from "./editor"
11
- import { boxBounds } from "../model/geometry/math"
12
20
 
13
- export function clone(
21
+ function getSelection(editor: Editor) {
22
+ return editor.selection
23
+ .values()
24
+ .toArray()
25
+ .filter((n) => !n.locked)
26
+ }
27
+
28
+ export function clone<N extends Node>(
14
29
  editor: Editor,
15
- node: Node,
16
- props?: ((props: NodeProps) => Partial<NodeProps>) | Partial<NodeProps>,
30
+ node: N,
31
+ props?: ((props: N["props"]) => Partial<N["props"]>) | Partial<N["props"]>,
17
32
  ) {
18
- const copy = node.serialize().props
33
+ const copy = node.props
19
34
  const newProps = typeof props === "function" ? props(copy) : props
20
35
  const newNode = editor.deserializeNode(node.page, {
21
36
  props: { ...copy, ...newProps },
@@ -35,14 +50,15 @@ export function useAddNodeAction(page?: Page) {
35
50
 
36
51
  return function addNode<N extends NodeConstructor>(
37
52
  NodeClass: N,
38
- props?: Partial<ConstructorParameters<N>[2]>,
53
+ props?: Partial<N["prototype"]["props"]>,
39
54
  ) {
40
55
  const [firstPage] = editor.pages.values()
41
56
  const targetPage = page ?? firstPage
42
- if (!targetPage) return
57
+ if (!targetPage) throw Error("missing Page")
58
+
43
59
  const { width, height, nodes } = targetPage
44
60
 
45
- const node = new NodeClass(editor, targetPage, {
61
+ const node = new NodeClass(targetPage, {
46
62
  id: editor.id(),
47
63
  x: Math.round(width / 2) - 50,
48
64
  y: Math.round(height / 2) - 50,
@@ -53,7 +69,7 @@ export function useAddNodeAction(page?: Page) {
53
69
 
54
70
  targetPage.nodes = new Map([...nodes, [node.id, node]])
55
71
  editor.history.push({
56
- redo: ["add-node", [targetPage.id, [node.serialize()]]],
72
+ redo: ["add-node", [targetPage.id, [editor.serializeNode(node)]]],
57
73
  undo: ["delete-node", [targetPage.id, [node.id]]],
58
74
  })
59
75
 
@@ -67,7 +83,7 @@ export function useMoveAction() {
67
83
 
68
84
  return function move(props: { x?: number; y?: number }) {
69
85
  const { x, y } = props
70
- const selection = editor.selection.values().toArray()
86
+ const selection = getSelection(editor)
71
87
  if (x) batchSet(selection, (n) => ({ x: n.x + x }))
72
88
  if (y) batchSet(selection, (n) => ({ y: n.y + y }))
73
89
  }
@@ -77,17 +93,18 @@ export function useTrashAction() {
77
93
  const editor = useEditor()
78
94
 
79
95
  return function trash() {
80
- const { selection, selectionPage: page } = editor
81
- const array = selection.values().toArray()
82
- if (selection.size === 0 || !page) return
96
+ const page = editor.selectionPage
97
+ const selection = getSelection(editor)
98
+ if (selection.length === 0 || !page) return
83
99
 
84
100
  editor.history.push({
85
- redo: ["delete-node", [page.id, array.map((node) => node.id)]],
86
- undo: ["add-node", [page.id, array.map((n) => n.serialize())]],
101
+ redo: ["delete-node", [page.id, selection.map((node) => node.id)]],
102
+ undo: ["add-node", [page.id, selection.map((n) => editor.serializeNode(n))]],
87
103
  })
88
104
 
105
+ const current = editor.selection
89
106
  editor.selection = new Set()
90
- page.nodes = new Map(page.nodes.entries().filter(([, node]) => !selection.has(node)))
107
+ page.nodes = new Map(page.nodes.entries().filter(([, node]) => !current.has(node)))
91
108
  }
92
109
  }
93
110
 
@@ -95,32 +112,38 @@ export function useGroupAction() {
95
112
  const editor = useEditor()
96
113
 
97
114
  function group() {
98
- const { selection, selectionPage: page } = editor
115
+ const page = editor.selectionPage
116
+ const selection = getSelection(editor)
117
+ if (selection.length === 0 || !page) return
118
+
99
119
  const { width, height, x, y } = boxBounds(selectionBox(selection))
100
- if (selection.size === 0 || !page) return
101
120
 
102
- const group = new GroupNode(editor, page, {
121
+ const group = new GroupNode(page, {
103
122
  id: editor.id(),
104
123
  width,
105
124
  height,
106
125
  x,
107
126
  y,
108
- nodes: selection
127
+ })
128
+
129
+ group.nodes = new Set(
130
+ selection
109
131
  .values()
110
132
  .map((node) =>
111
133
  clone(editor, node, (n) => ({
112
134
  x: n.x - x,
113
135
  y: n.y - y,
114
- })).serialize(),
136
+ })),
115
137
  )
116
138
  .toArray(),
117
- })
139
+ )
118
140
 
141
+ const current = editor.selection
119
142
  editor.selection = new Set()
120
143
  page.nodes = new Map([
121
144
  ...page.nodes
122
145
  .values()
123
- .filter((n) => !selection.has(n))
146
+ .filter((n) => !current.has(n))
124
147
  .map((n) => [n.id, n] as const),
125
148
  [group.id, group],
126
149
  ])
@@ -139,7 +162,7 @@ export function useGroupAction() {
139
162
  .toArray(),
140
163
  ],
141
164
  ],
142
- ["add-node", [page.id, [group.serialize()]]],
165
+ ["add-node", [page.id, [editor.serializeNode(group)]]],
143
166
  ],
144
167
  ],
145
168
  undo: [
@@ -152,7 +175,7 @@ export function useGroupAction() {
152
175
  page.id,
153
176
  selection
154
177
  .values()
155
- .map((n) => n.serialize())
178
+ .map((n) => editor.serializeNode(n))
156
179
  .toArray(),
157
180
  ],
158
181
  ],
@@ -162,14 +185,31 @@ export function useGroupAction() {
162
185
  }
163
186
 
164
187
  function ungroup(group: GroupNode) {
188
+ if (group.locked) return
189
+
165
190
  const page = group.page
191
+ const groupCenter = rectCenter(group)
166
192
  const nodes = group.nodes
167
193
  .values()
168
194
  .map((node) =>
169
- clone(editor, node, (n) => ({
170
- x: n.x + group.x,
171
- y: n.y + group.y,
172
- })),
195
+ clone(editor, node, (n) => {
196
+ const center = rotatePoint(
197
+ rectCenter({
198
+ x: n.x + group.x,
199
+ y: n.y + group.y,
200
+ width: n.width,
201
+ height: n.height,
202
+ }),
203
+ groupCenter,
204
+ group.rotation,
205
+ )
206
+
207
+ return {
208
+ x: floatNorm(center.x - n.width / 2),
209
+ y: floatNorm(center.y - n.height / 2),
210
+ rotation: deg((n.rotation ?? 0) + group.rotation),
211
+ }
212
+ }),
173
213
  )
174
214
  .toArray()
175
215
 
@@ -184,14 +224,14 @@ export function useGroupAction() {
184
224
  "batch",
185
225
  [
186
226
  ["delete-node", [page.id, [group.id]]],
187
- ["add-node", [page.id, nodes.map((n) => n.serialize())]],
227
+ ["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
188
228
  ],
189
229
  ],
190
230
  undo: [
191
231
  "batch",
192
232
  [
193
233
  ["delete-node", [page.id, nodes.map((node) => node.id)]],
194
- ["add-node", [page.id, [group.serialize()]]],
234
+ ["add-node", [page.id, [editor.serializeNode(group)]]],
195
235
  ],
196
236
  ],
197
237
  })
@@ -204,30 +244,24 @@ export function useDuplicateAction() {
204
244
  const editor = useEditor()
205
245
 
206
246
  return function duplicate(props?: { page?: Page; nodes?: Set<Node>; offset?: number }) {
207
- const selection = props?.nodes ?? editor.selection
208
247
  const offset = props?.offset ?? 10
209
248
  const page = props?.page ?? editor.selectionPage
210
- if (selection.size === 0 || !page) return
249
+ const nodes = Array.from(props?.nodes ?? getSelection(editor))
250
+ if (nodes.length === 0 || !page) return
211
251
 
212
- const nodes = selection
213
- .values()
214
- .map((node) =>
215
- clone(editor, node, (n) => ({
216
- x: n.x + offset,
217
- y: n.y + offset,
218
- })),
219
- )
220
- .toArray()
252
+ const dup = nodes.map((node) =>
253
+ clone(editor, node, (n) => ({
254
+ x: n.x + offset,
255
+ y: n.y + offset,
256
+ })),
257
+ )
221
258
 
222
- page.nodes = new Map([
223
- ...page.nodes,
224
- ...nodes.map((node) => [node.id, node] as const),
225
- ])
259
+ page.nodes = new Map([...page.nodes, ...dup.map((node) => [node.id, node] as const)])
226
260
 
227
- editor.selection = new Set(nodes)
261
+ editor.selection = new Set(dup)
228
262
  editor.history.push({
229
- redo: ["add-node", [page.id, nodes.map((n) => n.serialize())]],
230
- undo: ["delete-node", [page.id, nodes.map((node) => node.id)]],
263
+ redo: ["add-node", [page.id, dup.map((n) => editor.serializeNode(n))]],
264
+ undo: ["delete-node", [page.id, dup.map((node) => node.id)]],
231
265
  })
232
266
  }
233
267
  }
@@ -237,26 +271,15 @@ export function useToggleLockAction() {
237
271
 
238
272
  return function toggleLock() {
239
273
  const selection = editor.selection.values().toArray()
240
- const isLocked = selection.some((n) => n.locked)
241
- for (const node of selection) {
242
- node.locked = !isLocked
243
- }
244
- }
245
- }
274
+ const nextLocked = selection.some((n) => !n.locked)
246
275
 
247
- export function useAlignAction() {
248
- type AlignProps = { x: number } | { y: number }
249
-
250
- const editor = useEditor()
251
-
252
- function align(fn: (node: Node) => { prev: AlignProps; next: AlignProps }) {
253
276
  const undo: HistoryAction[] = []
254
277
  const redo: HistoryAction[] = []
255
278
 
256
- for (const node of editor.selection) {
257
- const { prev, next } = fn(node)
258
- undo.push(["set-node-props", [node.id, prev]])
259
- redo.push(["set-node-props", [node.id, next]])
279
+ for (const node of selection) {
280
+ undo.push(["set-node-props", [node.id, { locked: node.locked }]])
281
+ redo.push(["set-node-props", [node.id, { locked: nextLocked }]])
282
+ node.locked = nextLocked
260
283
  }
261
284
 
262
285
  editor.history.push({
@@ -264,6 +287,24 @@ export function useAlignAction() {
264
287
  redo: ["batch", redo],
265
288
  })
266
289
  }
290
+ }
291
+
292
+ export function useAlignAction() {
293
+ type AlignProps = { x: number } | { y: number }
294
+
295
+ const editor = useEditor()
296
+
297
+ function align(set: (node: Node) => { prev: AlignProps; next: AlignProps }) {
298
+ editor.pushHistory(
299
+ getSelection(editor).map((node) => {
300
+ const { prev, next } = set(node)
301
+ return {
302
+ undo: ["set-node-props", [node.id, prev]],
303
+ redo: ["set-node-props", [node.id, next]],
304
+ }
305
+ }),
306
+ )
307
+ }
267
308
 
268
309
  function halign(getX: (node: Node) => number) {
269
310
  align((node) => {
@@ -297,20 +338,22 @@ export function useDistributeAction() {
297
338
  const editor = useEditor()
298
339
 
299
340
  function distribute(pos: "x" | "y", size: "width" | "height") {
341
+ const selection = getSelection(editor)
342
+ if (selection.length === 0) return
343
+
300
344
  const rect = boxBounds(selectionBox(editor.selection))
301
- const array = editor.selection.values().toArray()
302
345
 
303
- const undo: HistoryAction[] = array.map((node) => [
346
+ const undo: HistoryAction[] = selection.map((node) => [
304
347
  "set-node-props",
305
348
  [node.id, { [pos]: node[pos] }],
306
349
  ])
307
350
 
308
- const total = array.reduce((sum, it) => sum + it[size], 0)
309
- const gap = (rect[size] - total) / (array.length - 1)
351
+ const total = selection.reduce((sum, it) => sum + it[size], 0)
352
+ const gap = (rect[size] - total) / (selection.length - 1)
310
353
 
311
354
  let cursor = rect[pos]
312
355
 
313
- const redo: HistoryAction[] = array
356
+ const redo: HistoryAction[] = selection
314
357
  .toSorted((a, b) => a[pos] - b[pos])
315
358
  .map((node) => {
316
359
  node[pos] = cursor
@@ -375,15 +418,17 @@ export function useStackOrderAction() {
375
418
  }
376
419
 
377
420
  const canBringBackward = useComputed(() => {
378
- const { selection, selectionPage: page } = editor
379
- if (selection.size !== 0 || !page) return false
421
+ const page = editor.selectionPage
422
+ const selection = getSelection(editor)
423
+ if (selection.length === 0 || !page) return false
380
424
  const nodes = page.nodes.values().toArray()
381
425
  return selection.values().some((n) => n !== nodes.at(0))
382
426
  })
383
427
 
384
428
  const canBringForward = useComputed(() => {
385
- const { selection, selectionPage: page } = editor
386
- if (selection.size !== 0 || !page) return false
429
+ const page = editor.selectionPage
430
+ const selection = getSelection(editor)
431
+ if (selection.length === 0 || !page) return false
387
432
  const nodes = page.nodes.values().toArray()
388
433
  return selection.values().some((n) => n !== nodes.at(-1))
389
434
  })
@@ -392,17 +437,20 @@ export function useStackOrderAction() {
392
437
  canBringBackward,
393
438
  canBringForward,
394
439
  bringBackward() {
395
- const { selection, selectionPage: page } = editor
396
- if (selection.size === 0 || !page) return
397
- order(page, moveBackward(page.nodes.values().toArray(), selection))
440
+ const page = editor.selectionPage
441
+ const selection = getSelection(editor)
442
+ if (selection.length === 0 || !page) return
443
+ order(page, moveBackward(page.nodes.values().toArray(), new Set(selection)))
398
444
  },
399
445
  bringForward() {
400
- const { selection, selectionPage: page } = editor
401
- if (selection.size === 0 || !page) return
402
- order(page, moveForward(page.nodes.values().toArray(), selection))
446
+ const page = editor.selectionPage
447
+ const selection = getSelection(editor)
448
+ if (selection.length === 0 || !page) return
449
+ order(page, moveForward(page.nodes.values().toArray(), new Set(selection)))
403
450
  },
404
451
  bringToBack() {
405
- const { selection, selectionPage: page } = editor
452
+ const page = editor.selectionPage
453
+ const selection = new Set(getSelection(editor))
406
454
  if (selection.size === 0 || !page) return
407
455
  order(page, [
408
456
  ...selection,
@@ -410,7 +458,8 @@ export function useStackOrderAction() {
410
458
  ])
411
459
  },
412
460
  bringToFront() {
413
- const { selection, selectionPage: page } = editor
461
+ const page = editor.selectionPage
462
+ const selection = new Set(getSelection(editor))
414
463
  if (selection.size === 0 || !page) return
415
464
  order(page, [
416
465
  ...page.nodes.values().filter((node) => !selection.has(node)),
@@ -5,27 +5,39 @@ import { type HistoryAction } from "../model/history"
5
5
  import { Node } from "../model/node"
6
6
  import { useEditor } from "./editor"
7
7
 
8
+ export function reduce<T>(values: T[], fallback: T) {
9
+ const [first, ...rest] = values
10
+ return rest.reduce(
11
+ (acc, value) => (isEqual(value, acc) ? acc : fallback),
12
+ values.length > 0 ? first : fallback,
13
+ )
14
+ }
15
+
8
16
  export function useNodeField<N extends Node, K extends keyof N>(
9
17
  nodes: Iterable<N>,
10
18
  key: K,
11
19
  fallback: N[K],
12
20
  ) {
13
- const [first, ...rest] = nodes
14
21
  return useComputed<N[K]>({
15
22
  equals: isEqual,
16
- fn: () =>
17
- rest.reduce(
18
- (acc, node) => (isEqual(node[key], acc) ? acc : fallback),
19
- first ? first[key] : fallback,
20
- ),
23
+ fn: () => {
24
+ const values = Array.from(nodes).map((node) => node[key])
25
+ return reduce(values, fallback)
26
+ },
21
27
  })
22
28
  }
23
29
 
30
+ export interface NodeFieldBatch<N extends Node, T> {
31
+ value: T
32
+ onChange(set: T | ((node: N) => T)): void
33
+ onChangeEnd(set: T | ((node: N) => T)): void
34
+ }
35
+
24
36
  export function useNodeFieldBatch<N extends Node, K extends keyof N>(
25
37
  nodes: N[],
26
38
  key: K,
27
39
  fallback: N[K],
28
- ) {
40
+ ): NodeFieldBatch<N, N[K]> {
29
41
  const { history } = useEditor()
30
42
  const initial = useRef<Map<N, N[K]> | null>(null)
31
43
 
@@ -82,12 +94,10 @@ export function useBatchSet() {
82
94
  nodes: Iterable<N>,
83
95
  set: Props<N> | ((n: N) => Props<N>),
84
96
  ) {
85
- let push = false
86
97
  const prev: HistoryAction[] = []
87
98
  const next: HistoryAction[] = []
88
99
 
89
100
  for (const node of nodes) {
90
- push = true
91
101
  const nextValues = typeof set === "function" ? set(node) : set
92
102
  const keys = Object.keys(nextValues) as WritableKeys<N>[]
93
103
  const prevValues = Object.fromEntries(keys.map((key) => [key, node[key]]))
@@ -96,8 +106,12 @@ export function useBatchSet() {
96
106
  Object.assign(node, nextValues)
97
107
  }
98
108
 
99
- if (push) {
109
+ if (next.length > 1) {
100
110
  history.push({ undo: ["batch", prev], redo: ["batch", next] })
111
+ } else if (next.length > 0) {
112
+ const [undo] = prev
113
+ const [redo] = next
114
+ history.push({ undo, redo })
101
115
  }
102
116
  }
103
117
  }
@@ -12,12 +12,13 @@ export {
12
12
  } from "./actions"
13
13
  export { useBatchSet, useNodeField, useNodeFieldBatch } from "./batch"
14
14
  export { EditorContext, PageContext, useEditor, usePage } from "./editor"
15
- export { useMoveable } from "./pointer/moveable"
16
- export { usePointer } from "./pointer/pointer"
17
- export { useResize } from "./pointer/resize"
18
- export { useRotation } from "./pointer/rotation"
19
- export { useSelectionFrame } from "./pointer/selectionFrame"
20
- export { useSelector } from "./pointer/selector"
15
+ export { useMoveable } from "./pointer/useMoveable"
16
+ export { useMovePoint } from "./pointer/useMovePoint"
17
+ export { usePointer } from "./pointer/usePointer"
18
+ export { useResize } from "./pointer/useResize"
19
+ export { useRotation } from "./pointer/useRotation"
20
+ export { useSelectionFrame } from "./selectionFrame"
21
+ export { useSelector } from "./pointer/useSelector"
21
22
  export { useTextMarks } from "./textMarks"
22
23
  export { useEditPage } from "./page"
23
24
  export { useVisualPositionBatch } from "./node"
package/lib/hooks/page.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { useRef } from "react"
2
- import type { Page } from "../model"
3
- import { useEditor } from "./editor"
4
2
  import { useStore } from "react-bolt"
3
+ import type { Page } from "../model"
5
4
 
6
5
  export function useEditPage<Key extends keyof Page>(page: Page, key: Key) {
7
- const editor = useEditor()
8
6
  const init = useRef(page[key])
9
7
  const value = useStore(page, key)
10
8
 
@@ -14,7 +12,7 @@ export function useEditPage<Key extends keyof Page>(page: Page, key: Key) {
14
12
  page[key] = value
15
13
  },
16
14
  onChangeEnd(value: Page[Key]) {
17
- editor.history.push({
15
+ page.editor.history.push({
18
16
  redo: ["set-page-props", [page.id, { [key]: value }]],
19
17
  undo: ["set-page-props", [page.id, { [key]: init.current }]],
20
18
  })
@@ -0,0 +1,100 @@
1
+ import { useRef } from "react"
2
+ import type { LineNode } from "../../model"
3
+ import {
4
+ deg,
5
+ floatNorm,
6
+ point,
7
+ pointSubtract,
8
+ rect,
9
+ rectCenter,
10
+ rotatePoint,
11
+ type Point,
12
+ } from "../../model/geometry/math"
13
+ import { useEditor } from "../editor"
14
+ import { cursorPosition, usePointer } from "./usePointer"
15
+ import { useSnap } from "./useSnap"
16
+
17
+ export function useMovePoint(props: { lineNode: LineNode; pointIndex: number }) {
18
+ const { lineNode, pointIndex } = props
19
+ const editor = useEditor()
20
+ const page = lineNode.page
21
+ const snap = useSnap(page)
22
+
23
+ const state = useRef({
24
+ cursorOffset: point(),
25
+ baseVertices: [] as Point[],
26
+ basePoints: [] as Point[],
27
+ baseX: 0,
28
+ baseY: 0,
29
+ })
30
+
31
+ function unrotatedPagePoints(vertices: Point[]): Point[] {
32
+ const rotation = lineNode.rotation
33
+ const origin = point()
34
+ const unrotatedBounds = rect(
35
+ ...vertices.map((p) => rotatePoint(p, origin, deg(-rotation))),
36
+ )
37
+ const center = rotatePoint(rectCenter(unrotatedBounds), origin, rotation)
38
+ return vertices.map((p) => rotatePoint(p, center, deg(-rotation)))
39
+ }
40
+
41
+ function setLinePoints(pagePoints: Point[]) {
42
+ const bounds = point(rect(...pagePoints))
43
+ lineNode.x = floatNorm(bounds.x)
44
+ lineNode.y = floatNorm(bounds.y)
45
+ lineNode.points = pagePoints.map((p) => pointSubtract(p, bounds))
46
+ }
47
+
48
+ function redoProps() {
49
+ const { x, y, points } = lineNode
50
+ return { x, y, points }
51
+ }
52
+
53
+ function undoProps() {
54
+ const { baseX: x, baseY: y, basePoints: points } = state.current
55
+ return { x, y, points }
56
+ }
57
+
58
+ return usePointer({
59
+ onDown(event) {
60
+ if (lineNode.locked) return false
61
+
62
+ event.preventDefault()
63
+ event.stopPropagation()
64
+
65
+ state.current = {
66
+ baseVertices: lineNode.vertices,
67
+ basePoints: lineNode.points,
68
+ baseX: lineNode.x,
69
+ baseY: lineNode.y,
70
+ cursorOffset: pointSubtract(
71
+ cursorPosition(event, page),
72
+ lineNode.vertices[pointIndex],
73
+ ),
74
+ }
75
+ },
76
+ onMove(event) {
77
+ const { cursorOffset, baseVertices } = state.current
78
+ const cursor = pointSubtract(cursorPosition(event, page), cursorOffset)
79
+ const result = point(snap(!event.shiftKey, rect(cursor)))
80
+ const vertices = baseVertices.map((p, i) => (i === pointIndex ? result : p))
81
+
82
+ setLinePoints(unrotatedPagePoints(vertices))
83
+
84
+ editor.action = { action: "move", payload: result }
85
+ },
86
+ onCancel() {
87
+ editor.action = {}
88
+ page.snapLines = []
89
+ },
90
+ onEnd() {
91
+ editor.action = {}
92
+ page.snapLines = []
93
+
94
+ editor.pushHistory({
95
+ redo: ["set-node-props", [lineNode.id, redoProps()]],
96
+ undo: ["set-node-props", [lineNode.id, undoProps()]],
97
+ })
98
+ },
99
+ })
100
+ }