@lazlon-platform/html-editor 0.4.0 → 0.6.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 (59) hide show
  1. package/lib/hooks/actions.ts +54 -26
  2. package/lib/hooks/batch.ts +17 -7
  3. package/lib/hooks/index.ts +1 -0
  4. package/lib/hooks/node.ts +14 -6
  5. package/lib/hooks/pointer/movePoint.ts +75 -0
  6. package/lib/hooks/pointer/moveable.ts +92 -57
  7. package/lib/hooks/pointer/pointer.ts +21 -11
  8. package/lib/hooks/pointer/resize.ts +176 -210
  9. package/lib/hooks/pointer/rotation.ts +89 -68
  10. package/lib/hooks/pointer/selectionFrame.ts +8 -11
  11. package/lib/hooks/pointer/selector.ts +62 -40
  12. package/lib/hooks/pointer/snap.ts +23 -23
  13. package/lib/hooks/textMarks.ts +1 -3
  14. package/lib/lib/googleFonts.ts +1 -5
  15. package/lib/model/editor.ts +13 -9
  16. package/lib/model/geometry/math.ts +623 -0
  17. package/lib/model/geometry/svg.ts +55 -0
  18. package/lib/model/history.ts +10 -13
  19. package/lib/model/index.ts +7 -10
  20. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  21. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  22. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  23. package/lib/model/node/{image.ts → imageNode.ts} +5 -11
  24. package/lib/model/node/lineNode.ts +59 -0
  25. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  26. package/lib/model/node/shapeNode/shape.ts +96 -0
  27. package/lib/model/node/{text.ts → textNode.ts} +19 -21
  28. package/lib/model/node.ts +11 -29
  29. package/lib/model/page.ts +4 -3
  30. package/lib/model/traversal.ts +1 -1
  31. package/lib/ui/extractor.ts +3 -3
  32. package/lib/ui/index.ts +2 -4
  33. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
  34. package/lib/ui/node/GroupContent.tsx +1 -1
  35. package/lib/ui/node/ImageContent.tsx +1 -1
  36. package/lib/ui/node/LineContent.tsx +32 -0
  37. package/lib/ui/node/NodeView.tsx +1 -13
  38. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  39. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  40. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  41. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  42. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  43. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  44. package/lib/ui/node/TextContent.tsx +1 -1
  45. package/lib/ui/selection.ts +9 -26
  46. package/package.json +34 -34
  47. package/lib/model/geometry.ts +0 -247
  48. package/lib/model/node/shape/arrow.ts +0 -50
  49. package/lib/model/node/shape/ellipse.ts +0 -26
  50. package/lib/model/node/shape/polygon.ts +0 -108
  51. package/lib/model/node/shape/star.ts +0 -63
  52. package/lib/ui/node/ArrowContent.tsx +0 -60
  53. package/lib/ui/node/EllipseContent.tsx +0 -49
  54. package/lib/ui/node/PolygonContent.tsx +0 -81
  55. package/lib/ui/node/StarContent.tsx +0 -60
  56. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  57. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  58. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  59. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,11 +1,21 @@
1
1
  import { useComputed } from "react-bolt"
2
+ import {
3
+ flattenNodes,
4
+ GroupNode,
5
+ type HistoryAction,
6
+ type Node,
7
+ type NodeProps,
8
+ type Page,
9
+ } from "../model"
2
10
  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"
8
- import { getTargetRect } from "../ui/selection"
11
+ import {
12
+ boxBounds,
13
+ deg,
14
+ floatNorm,
15
+ rectCenter,
16
+ rotatePoint,
17
+ } from "../model/geometry/math"
18
+ import { selectionBox } from "../ui/selection"
9
19
  import { useBatchSet } from "./batch"
10
20
  import { useEditor } from "./editor"
11
21
 
@@ -14,7 +24,7 @@ export function clone(
14
24
  node: Node,
15
25
  props?: ((props: NodeProps) => Partial<NodeProps>) | Partial<NodeProps>,
16
26
  ) {
17
- const copy = node.serialize().props
27
+ const copy = node.props
18
28
  const newProps = typeof props === "function" ? props(copy) : props
19
29
  const newNode = editor.deserializeNode(node.page, {
20
30
  props: { ...copy, ...newProps },
@@ -34,14 +44,14 @@ export function useAddNodeAction(page?: Page) {
34
44
 
35
45
  return function addNode<N extends NodeConstructor>(
36
46
  NodeClass: N,
37
- props?: Partial<ConstructorParameters<N>[2]>,
47
+ props?: Partial<N["prototype"]["props"]>,
38
48
  ) {
39
49
  const [firstPage] = editor.pages.values()
40
50
  const targetPage = page ?? firstPage
41
51
  if (!targetPage) return
42
52
  const { width, height, nodes } = targetPage
43
53
 
44
- const node = new NodeClass(editor, targetPage, {
54
+ const node = new NodeClass(targetPage, {
45
55
  id: editor.id(),
46
56
  x: Math.round(width / 2) - 50,
47
57
  y: Math.round(height / 2) - 50,
@@ -52,7 +62,7 @@ export function useAddNodeAction(page?: Page) {
52
62
 
53
63
  targetPage.nodes = new Map([...nodes, [node.id, node]])
54
64
  editor.history.push({
55
- redo: ["add-node", [targetPage.id, [node.serialize()]]],
65
+ redo: ["add-node", [targetPage.id, [editor.serializeNode(node)]]],
56
66
  undo: ["delete-node", [targetPage.id, [node.id]]],
57
67
  })
58
68
 
@@ -82,7 +92,7 @@ export function useTrashAction() {
82
92
 
83
93
  editor.history.push({
84
94
  redo: ["delete-node", [page.id, array.map((node) => node.id)]],
85
- undo: ["add-node", [page.id, array.map((n) => n.serialize())]],
95
+ undo: ["add-node", [page.id, array.map((n) => editor.serializeNode(n))]],
86
96
  })
87
97
 
88
98
  editor.selection = new Set()
@@ -95,25 +105,28 @@ export function useGroupAction() {
95
105
 
96
106
  function group() {
97
107
  const { selection, selectionPage: page } = editor
98
- const { width, height, x, y } = getTargetRect(selection)
108
+ const { width, height, x, y } = boxBounds(selectionBox(selection))
99
109
  if (selection.size === 0 || !page) return
100
110
 
101
- const group = new GroupNode(editor, page, {
111
+ const group = new GroupNode(page, {
102
112
  id: editor.id(),
103
113
  width,
104
114
  height,
105
115
  x,
106
116
  y,
107
- nodes: selection
117
+ })
118
+
119
+ group.nodes = new Set(
120
+ selection
108
121
  .values()
109
122
  .map((node) =>
110
123
  clone(editor, node, (n) => ({
111
124
  x: n.x - x,
112
125
  y: n.y - y,
113
- })).serialize(),
126
+ })),
114
127
  )
115
128
  .toArray(),
116
- })
129
+ )
117
130
 
118
131
  editor.selection = new Set()
119
132
  page.nodes = new Map([
@@ -138,7 +151,7 @@ export function useGroupAction() {
138
151
  .toArray(),
139
152
  ],
140
153
  ],
141
- ["add-node", [page.id, [group.serialize()]]],
154
+ ["add-node", [page.id, [editor.serializeNode(group)]]],
142
155
  ],
143
156
  ],
144
157
  undo: [
@@ -151,7 +164,7 @@ export function useGroupAction() {
151
164
  page.id,
152
165
  selection
153
166
  .values()
154
- .map((n) => n.serialize())
167
+ .map((n) => editor.serializeNode(n))
155
168
  .toArray(),
156
169
  ],
157
170
  ],
@@ -162,13 +175,28 @@ export function useGroupAction() {
162
175
 
163
176
  function ungroup(group: GroupNode) {
164
177
  const page = group.page
178
+ const groupCenter = rectCenter(group)
165
179
  const nodes = group.nodes
166
180
  .values()
167
181
  .map((node) =>
168
- clone(editor, node, (n) => ({
169
- x: n.x + group.x,
170
- y: n.y + group.y,
171
- })),
182
+ clone(editor, node, (n) => {
183
+ const center = rotatePoint(
184
+ rectCenter({
185
+ x: n.x + group.x,
186
+ y: n.y + group.y,
187
+ width: n.width,
188
+ height: n.height,
189
+ }),
190
+ groupCenter,
191
+ group.rotation,
192
+ )
193
+
194
+ return {
195
+ x: floatNorm(center.x - n.width / 2),
196
+ y: floatNorm(center.y - n.height / 2),
197
+ rotation: deg((n.rotation ?? 0) + group.rotation),
198
+ }
199
+ }),
172
200
  )
173
201
  .toArray()
174
202
 
@@ -183,14 +211,14 @@ export function useGroupAction() {
183
211
  "batch",
184
212
  [
185
213
  ["delete-node", [page.id, [group.id]]],
186
- ["add-node", [page.id, nodes.map((n) => n.serialize())]],
214
+ ["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
187
215
  ],
188
216
  ],
189
217
  undo: [
190
218
  "batch",
191
219
  [
192
220
  ["delete-node", [page.id, nodes.map((node) => node.id)]],
193
- ["add-node", [page.id, [group.serialize()]]],
221
+ ["add-node", [page.id, [editor.serializeNode(group)]]],
194
222
  ],
195
223
  ],
196
224
  })
@@ -225,7 +253,7 @@ export function useDuplicateAction() {
225
253
 
226
254
  editor.selection = new Set(nodes)
227
255
  editor.history.push({
228
- redo: ["add-node", [page.id, nodes.map((n) => n.serialize())]],
256
+ redo: ["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
229
257
  undo: ["delete-node", [page.id, nodes.map((node) => node.id)]],
230
258
  })
231
259
  }
@@ -296,7 +324,7 @@ export function useDistributeAction() {
296
324
  const editor = useEditor()
297
325
 
298
326
  function distribute(pos: "x" | "y", size: "width" | "height") {
299
- const rect = getTargetRect(editor.selection)
327
+ const rect = boxBounds(selectionBox(editor.selection))
300
328
  const array = editor.selection.values().toArray()
301
329
 
302
330
  const undo: HistoryAction[] = array.map((node) => [
@@ -5,27 +5,37 @@ 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
21
+ const values = Array.from(nodes).map((node) => node[key])
14
22
  return useComputed<N[K]>({
15
23
  equals: isEqual,
16
- fn: () =>
17
- rest.reduce(
18
- (acc, node) => (isEqual(node[key], acc) ? acc : fallback),
19
- first ? first[key] : fallback,
20
- ),
24
+ fn: () => reduce(values, fallback),
21
25
  })
22
26
  }
23
27
 
28
+ export interface NodeFieldBatch<N extends Node, T> {
29
+ value: T
30
+ onChange(set: T | ((node: N) => T)): void
31
+ onChangeEnd(set: T | ((node: N) => T)): void
32
+ }
33
+
24
34
  export function useNodeFieldBatch<N extends Node, K extends keyof N>(
25
35
  nodes: N[],
26
36
  key: K,
27
37
  fallback: N[K],
28
- ) {
38
+ ): NodeFieldBatch<N, N[K]> {
29
39
  const { history } = useEditor()
30
40
  const initial = useRef<Map<N, N[K]> | null>(null)
31
41
 
@@ -13,6 +13,7 @@ export {
13
13
  export { useBatchSet, useNodeField, useNodeFieldBatch } from "./batch"
14
14
  export { EditorContext, PageContext, useEditor, usePage } from "./editor"
15
15
  export { useMoveable } from "./pointer/moveable"
16
+ export { useMovePoint } from "./pointer/movePoint"
16
17
  export { usePointer } from "./pointer/pointer"
17
18
  export { useResize } from "./pointer/resize"
18
19
  export { useRotation } from "./pointer/rotation"
package/lib/hooks/node.ts CHANGED
@@ -1,31 +1,39 @@
1
1
  import type { Node } from "../model"
2
- import { rotatedAABB } from "../model/geometry"
2
+ import { box, boxBounds, deg } from "../model/geometry/math"
3
3
  import { useNodeField, useNodeFieldBatch } from "./batch"
4
4
 
5
5
  export function useVisualPositionBatch(nodes: Node[]) {
6
- const rotation = useNodeField(nodes, "rotation", 0)
6
+ const rotation = useNodeField(nodes, "rotation", deg(0))
7
7
  const width = useNodeField(nodes, "width", NaN)
8
8
  const height = useNodeField(nodes, "height", NaN)
9
9
  const x = useNodeFieldBatch(nodes, "x", NaN)
10
10
  const y = useNodeFieldBatch(nodes, "y", NaN)
11
11
 
12
- const box = rotatedAABB({ x: x.value, y: y.value, width, height }, rotation)
12
+ const bounds = boxBounds(
13
+ box({
14
+ x: x.value,
15
+ y: y.value,
16
+ width,
17
+ height,
18
+ rotation,
19
+ }),
20
+ )
13
21
 
14
22
  function withBoundingBox(axis: "x" | "y", fn: (value: number) => void) {
15
23
  return function (value: number): void {
16
- const delta = value - box[axis]
24
+ const delta = value - bounds[axis]
17
25
  return fn(axis === "x" ? x.value + delta : y.value + delta)
18
26
  }
19
27
  }
20
28
 
21
29
  return {
22
30
  x: {
23
- value: box.x,
31
+ value: bounds.x,
24
32
  onChange: withBoundingBox("x", x.onChange),
25
33
  onChangeEnd: withBoundingBox("x", x.onChangeEnd),
26
34
  },
27
35
  y: {
28
- value: box.y,
36
+ value: bounds.y,
29
37
  onChange: withBoundingBox("y", y.onChange),
30
38
  onChangeEnd: withBoundingBox("y", y.onChangeEnd),
31
39
  },
@@ -0,0 +1,75 @@
1
+ import { useRef } from "react"
2
+ import type { LineNode } from "../../model"
3
+ import {
4
+ point,
5
+ pointAdd,
6
+ pointSubtract,
7
+ rect,
8
+ type Point,
9
+ } from "../../model/geometry/math"
10
+ import { useEditor } from "../editor"
11
+ import { cursorPosition, usePointer } from "./pointer"
12
+ import { useSnap } from "./snap"
13
+
14
+ export function useMovePoint(props: { lineNode: LineNode; pointIndex: number }) {
15
+ const editor = useEditor()
16
+ const { lineNode, pointIndex } = props
17
+ const page = lineNode.page
18
+ const snap = useSnap(page)
19
+
20
+ const state = useRef({
21
+ cursorOffset: point(),
22
+ initialAnchor: point(),
23
+ initialPoints: [] as Point[],
24
+ })
25
+
26
+ return usePointer({
27
+ onDown(event) {
28
+ const cursor = cursorPosition(event, page)
29
+ const linePoint = lineNode.points[pointIndex]
30
+ const pointPosition = pointAdd(lineNode, linePoint)
31
+
32
+ event.preventDefault()
33
+ event.stopPropagation()
34
+
35
+ state.current = {
36
+ cursorOffset: pointSubtract(cursor, pointPosition),
37
+ initialAnchor: point(lineNode),
38
+ initialPoints: Array.from(lineNode.points),
39
+ }
40
+ },
41
+ onMove(event) {
42
+ const { cursorOffset, initialPoints, initialAnchor } = state.current
43
+ const cursor = pointSubtract(cursorPosition(event, page), cursorOffset)
44
+ const snapped = snap(
45
+ !event.shiftKey,
46
+ rect({ x: cursor.x, y: cursor.y, width: 0, height: 0 }),
47
+ )
48
+
49
+ const points = initialPoints.map((p, i) =>
50
+ i === pointIndex ? snapped : pointAdd(initialAnchor, p),
51
+ )
52
+
53
+ const bounds = point(rect(...points))
54
+
55
+ lineNode.x = bounds.x
56
+ lineNode.y = bounds.y
57
+ lineNode.points = points.map((p) => pointSubtract(p, bounds))
58
+
59
+ editor.action = { action: "move", payload: point(snapped) }
60
+ },
61
+ onCancel() {
62
+ editor.action = {}
63
+ page.snapLines = []
64
+ },
65
+ onEnd() {
66
+ editor.action = {}
67
+ page.snapLines = []
68
+
69
+ lineNode.page.editor.history.push<LineNode>({
70
+ redo: ["set-node-props", [lineNode.id, { points: lineNode.points }]],
71
+ undo: ["set-node-props", [lineNode.id, { points: state.current.initialPoints }]],
72
+ })
73
+ },
74
+ })
75
+ }
@@ -1,73 +1,108 @@
1
1
  import { useRef } from "react"
2
- import type { Point } from "../../model/geometry"
2
+ import {
3
+ accessibleLine,
4
+ box,
5
+ boxBounds,
6
+ boxContainsPoint,
7
+ lineContainsPoint,
8
+ type Point,
9
+ point,
10
+ pointSubtract,
11
+ rect,
12
+ } from "../../model/geometry/math"
3
13
  import type { HistoryEntry } from "../../model/history"
4
14
  import type { Node } from "../../model/node"
5
- import { getTargetRect, isPointerInSelectionRect } from "../../ui/selection"
15
+ import { selectionBox } from "../../ui/selection"
6
16
  import { useEditor, usePage } from "../editor"
7
- import { usePointer } from "./pointer"
17
+ import { cursorPosition, usePointer } from "./pointer"
8
18
  import { useSnap } from "./snap"
9
-
10
- type StartPoints = {
11
- node: Node
12
- start: Point
13
- offset: Point
14
- }
15
-
16
- const moveInit = {
17
- start: { x: 0, y: 0, width: 0, height: 0 },
18
- grab: { x: 0, y: 0 },
19
- }
19
+ import { LineNode } from "@lazlon/html-editor/model"
20
20
 
21
21
  export function useMoveable() {
22
22
  const editor = useEditor()
23
23
  const page = usePage()
24
- const startpoints = useRef(Array<StartPoints>())
25
- const move = useRef(moveInit)
26
- const snap = useSnap()
24
+ const snap = useSnap(page)
25
+
26
+ const state = useRef({
27
+ initialSelectionRect: rect(),
28
+ cursorOffset: point(),
29
+ nodes: Array<{
30
+ node: Node
31
+ startingPoint: Point
32
+ selectionOffset: Point
33
+ }>(),
34
+ })
35
+
36
+ function isCursorInSelection(cursor: Point) {
37
+ const [firstNode, ...rest] = editor.selection
38
+ if (rest.length === 0 && firstNode && firstNode instanceof LineNode) {
39
+ return lineContainsPoint(accessibleLine(firstNode), cursor)
40
+ }
41
+ return boxContainsPoint(selectionBox(editor.selection), cursor)
42
+ }
43
+
44
+ function isNodeHit(node: Node, cursor: Point) {
45
+ if (node instanceof LineNode) {
46
+ return lineContainsPoint(accessibleLine(node), cursor)
47
+ }
48
+ return boxContainsPoint(box(node), cursor)
49
+ }
27
50
 
28
51
  return usePointer({
29
52
  onDown(event) {
30
- const { selection } = editor
31
- const rect = getTargetRect(selection)
32
-
33
- startpoints.current = selection
34
- .values()
35
- .map((n) => ({
36
- node: n,
37
- start: { x: n.x, y: n.y },
38
- offset: { x: n.x - rect.x, y: n.y - rect.y },
39
- }))
40
- .toArray()
41
-
42
- move.current = {
43
- start: rect,
44
- grab: {
45
- x: Math.round(event.clientX / editor.zoom - rect.x),
46
- y: Math.round(event.clientY / editor.zoom - rect.y),
47
- },
53
+ const cursor = cursorPosition(event, page)
54
+
55
+ // clicked outside of selection, try grabbing nodes under the cursor
56
+ if (!isCursorInSelection(cursor)) {
57
+ const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
58
+ for (const node of stackOrderedNodes) {
59
+ if (isNodeHit(node, cursor)) {
60
+ editor.selection = new Set([node])
61
+ break
62
+ }
63
+ }
48
64
  }
49
65
 
50
- for (const node of selection) {
51
- if (node.blockMove(event)) return false
66
+ if (editor.selection.size === 0) return false
67
+
68
+ const initialSelectionBox = selectionBox(editor.selection)
69
+ const initialSelectionRect = boxBounds(initialSelectionBox)
70
+
71
+ if (isCursorInSelection(cursor)) {
72
+ state.current = {
73
+ initialSelectionRect,
74
+ cursorOffset: pointSubtract(cursor, initialSelectionRect),
75
+ nodes: editor.selection
76
+ .values()
77
+ .map((node) => ({
78
+ node,
79
+ startingPoint: point(node),
80
+ selectionOffset: pointSubtract(point(node), initialSelectionRect),
81
+ }))
82
+ .toArray(),
83
+ }
84
+ } else {
85
+ return false
52
86
  }
53
-
54
- return isPointerInSelectionRect(selection, event)
55
87
  },
56
88
 
57
- onMove({ event }) {
58
- const px = Math.round(event.clientX / editor.zoom - move.current.grab.x)
59
- const py = Math.round(event.clientY / editor.zoom - move.current.grab.y)
60
-
61
- const { x, y } = snap(!event.shiftKey, {
62
- x: px,
63
- y: py,
64
- width: move.current.start.width,
65
- height: move.current.start.height,
66
- })
67
-
68
- for (const { node, offset } of startpoints.current) {
69
- node.x = x + offset.x
70
- node.y = y + offset.y
89
+ onMove(event) {
90
+ const cursor = cursorPosition(event, page)
91
+ const target = pointSubtract(cursor, state.current.cursorOffset)
92
+
93
+ const { x, y } = snap(
94
+ !event.shiftKey,
95
+ rect({
96
+ x: target.x,
97
+ y: target.y,
98
+ width: state.current.initialSelectionRect.width,
99
+ height: state.current.initialSelectionRect.height,
100
+ }),
101
+ )
102
+
103
+ for (const { node, selectionOffset } of state.current.nodes) {
104
+ node.x = x + selectionOffset.x
105
+ node.y = y + selectionOffset.y
71
106
  }
72
107
 
73
108
  editor.action = { action: "move", payload: { x, y } }
@@ -82,12 +117,12 @@ export function useMoveable() {
82
117
  editor.action = {}
83
118
  page.snapLines = []
84
119
 
85
- const entries = startpoints.current.map(({ node, start }): HistoryEntry => {
86
- return {
120
+ const entries: HistoryEntry[] = state.current.nodes.map(
121
+ ({ node, startingPoint: start }) => ({
87
122
  redo: ["set-node-props", [node.id, { x: node.x, y: node.y }]],
88
123
  undo: ["set-node-props", [node.id, { x: start.x, y: start.y }]],
89
- }
90
- })
124
+ }),
125
+ )
91
126
 
92
127
  editor.history.push({
93
128
  redo: ["batch", entries.map((e) => e.redo)],
@@ -1,15 +1,25 @@
1
+ import { Page } from "@lazlon/html-editor/model"
1
2
  import { useCallback, useRef } from "react"
2
-
3
- type MoveEvent = {
4
- start: React.PointerEvent
5
- event: globalThis.PointerEvent
6
- }
3
+ import { floatNorm, type Point } from "../../model/geometry/math"
7
4
 
8
5
  export type UsePointerProps = {
9
6
  onDown?(event: React.PointerEvent): boolean | void
10
- onMove?(event: MoveEvent): void
11
- onEnd?(event: MoveEvent): void
12
- onCancel?(event: MoveEvent): void
7
+ onMove?(event: globalThis.PointerEvent): void
8
+ onEnd?(event: globalThis.PointerEvent): void
9
+ onCancel?(event: globalThis.PointerEvent): void
10
+ }
11
+
12
+ export function cursorPosition(
13
+ event: { clientX: number; clientY: number },
14
+ page: Page,
15
+ ): Point {
16
+ const { zoom } = page.editor
17
+ const { x, y } = page.ref!.getBoundingClientRect()
18
+
19
+ return {
20
+ x: floatNorm(event.clientX / zoom - x),
21
+ y: floatNorm(event.clientY / zoom - y),
22
+ }
13
23
  }
14
24
 
15
25
  export function usePointer(props: UsePointerProps) {
@@ -23,7 +33,7 @@ export function usePointer(props: UsePointerProps) {
23
33
 
24
34
  function onPointerMove(event: globalThis.PointerEvent) {
25
35
  isMovingRef.current = true
26
- onMove?.({ start, event })
36
+ onMove?.(event)
27
37
  }
28
38
 
29
39
  function onPointerUp(event: globalThis.PointerEvent) {
@@ -32,9 +42,9 @@ export function usePointer(props: UsePointerProps) {
32
42
  removeEventListener("pointercancel", onPointerUp)
33
43
 
34
44
  if (isMovingRef.current) {
35
- onEnd?.({ start, event })
45
+ onEnd?.(event)
36
46
  } else {
37
- onCancel?.({ start, event })
47
+ onCancel?.(event)
38
48
  }
39
49
 
40
50
  isMovingRef.current = false