@lazlon-platform/html-editor 0.6.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.
- package/lib/hooks/actions.ts +89 -67
- package/lib/hooks/batch.ts +9 -5
- package/lib/hooks/index.ts +7 -7
- package/lib/hooks/page.ts +2 -4
- package/lib/hooks/pointer/useMovePoint.ts +100 -0
- package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +30 -36
- package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +2 -2
- package/lib/hooks/pointer/useResize/index.ts +31 -0
- package/lib/hooks/pointer/useResize/multi.ts +161 -0
- package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
- package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
- package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
- package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
- package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
- package/lib/hooks/pointer/useRotation.ts +102 -0
- package/lib/hooks/pointer/{selector.ts → useSelector.ts} +2 -1
- package/lib/hooks/pointer/{snap.ts → useSnap.ts} +3 -2
- package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
- package/lib/hooks/textMarks.ts +30 -19
- package/lib/model/editor.ts +18 -4
- package/lib/model/geometry/math.ts +8 -20
- package/lib/model/index.ts +9 -1
- package/lib/model/node/imageNode.ts +1 -1
- package/lib/model/node/lineNode.ts +41 -20
- package/lib/model/node/textNode.ts +5 -14
- package/lib/model/node.ts +18 -9
- package/lib/model/page.ts +3 -2
- package/lib/ui/node/EditableContent/index.tsx +6 -4
- package/lib/ui/node/LineContent.tsx +1 -3
- package/lib/ui/selection.ts +6 -5
- package/package.json +1 -1
- package/lib/hooks/pointer/movePoint.ts +0 -75
- package/lib/hooks/pointer/resize.ts +0 -247
- package/lib/hooks/pointer/rotation.ts +0 -138
|
@@ -18,6 +18,7 @@ export function EditableContent(props: {
|
|
|
18
18
|
const editor = useEditor()
|
|
19
19
|
const selection = useStore(editor, "selection")
|
|
20
20
|
const { action } = useStore(editor, "action")
|
|
21
|
+
const locked = useStore(node, "locked")
|
|
21
22
|
const prevContent = useRef(node.tiptap.getJSON())
|
|
22
23
|
|
|
23
24
|
// start with selection.has(node) so that newly added editable nodes can be focused
|
|
@@ -25,9 +26,10 @@ export function EditableContent(props: {
|
|
|
25
26
|
|
|
26
27
|
const onDoubleClick = useDoubleClick(
|
|
27
28
|
useCallback(() => {
|
|
29
|
+
if (locked) return
|
|
28
30
|
setClicked(true)
|
|
29
31
|
node.tiptap.commands.focus()
|
|
30
|
-
}, [node, setClicked]),
|
|
32
|
+
}, [node, locked, setClicked]),
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
const html = useEditorState({
|
|
@@ -36,12 +38,12 @@ export function EditableContent(props: {
|
|
|
36
38
|
})
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
39
|
-
if (!selection.has(node)) {
|
|
41
|
+
if (!selection.has(node) || locked) {
|
|
40
42
|
node.blur()
|
|
41
43
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
42
44
|
setClicked(false)
|
|
43
45
|
}
|
|
44
|
-
}, [selection, node])
|
|
46
|
+
}, [selection, node, locked])
|
|
45
47
|
|
|
46
48
|
useEffect(() => {
|
|
47
49
|
const onUpdate = debounce(() => {
|
|
@@ -71,7 +73,7 @@ export function EditableContent(props: {
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
const lineHeight = useStore(node, "lineHeight")
|
|
74
|
-
const showEditor = !isStatic && selection.has(node) && !action && clicked
|
|
76
|
+
const showEditor = !isStatic && selection.has(node) && !action && clicked && !locked
|
|
75
77
|
|
|
76
78
|
return (
|
|
77
79
|
<div
|
|
@@ -11,8 +11,6 @@ export function LineContent(props: { node: LineNode }) {
|
|
|
11
11
|
"strokeColor",
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
const polylinePoints = points.map(({ x, y }) => `${x},${y}`).join(" ")
|
|
15
|
-
|
|
16
14
|
return (
|
|
17
15
|
<svg
|
|
18
16
|
className="absolute inset-0 overflow-visible"
|
|
@@ -21,7 +19,7 @@ export function LineContent(props: { node: LineNode }) {
|
|
|
21
19
|
fill="none"
|
|
22
20
|
>
|
|
23
21
|
<polyline
|
|
24
|
-
points={
|
|
22
|
+
points={points.map(({ x, y }) => `${x},${y}`).join(" ")}
|
|
25
23
|
stroke={strokeColor}
|
|
26
24
|
strokeWidth={strokeWidth}
|
|
27
25
|
strokeLinecap="round"
|
package/lib/ui/selection.ts
CHANGED
|
@@ -11,11 +11,12 @@ export function selectionDOMRect(selection: Set<Node>): Rect {
|
|
|
11
11
|
return rect(...rects)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function selectionBox(selection:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export function selectionBox(selection: Iterable<Node>): Box {
|
|
15
|
+
const arr = Array.from(selection)
|
|
16
|
+
|
|
17
|
+
if (arr.length === 1) {
|
|
18
|
+
return box(arr[0])
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
return box(boxBounds(...
|
|
21
|
+
return box(boxBounds(...arr.map(box)))
|
|
21
22
|
}
|
package/package.json
CHANGED
|
@@ -1,75 +0,0 @@
|
|
|
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,247 +0,0 @@
|
|
|
1
|
-
import { TextNode, type HistoryEntry, type Node } from "@lazlon/html-editor/model"
|
|
2
|
-
import { pick } from "es-toolkit"
|
|
3
|
-
import { useRef } from "react"
|
|
4
|
-
import { useStore } from "react-bolt"
|
|
5
|
-
import {
|
|
6
|
-
box,
|
|
7
|
-
boxRect,
|
|
8
|
-
deg,
|
|
9
|
-
floatNorm,
|
|
10
|
-
perpDistance,
|
|
11
|
-
point,
|
|
12
|
-
pointNorm,
|
|
13
|
-
pointSubtract,
|
|
14
|
-
rect,
|
|
15
|
-
resizeBox,
|
|
16
|
-
rotatePoint,
|
|
17
|
-
scaleBox,
|
|
18
|
-
vectorDotProd,
|
|
19
|
-
type Box,
|
|
20
|
-
type Corner,
|
|
21
|
-
type Edge,
|
|
22
|
-
type Point,
|
|
23
|
-
type Rect,
|
|
24
|
-
type Size,
|
|
25
|
-
} from "../../model/geometry/math"
|
|
26
|
-
import { selectionBox } from "../../ui/selection"
|
|
27
|
-
import { useEditor } from "../editor"
|
|
28
|
-
import { cursorPosition, usePointer } from "./pointer"
|
|
29
|
-
|
|
30
|
-
const rectProps = ["x", "y", "width", "height"] as const
|
|
31
|
-
|
|
32
|
-
function setNodeSize(node: Node, size: Point & Size) {
|
|
33
|
-
node.x = floatNorm(size.x)
|
|
34
|
-
node.y = floatNorm(size.y)
|
|
35
|
-
node.width = floatNorm(size.width)
|
|
36
|
-
node.height = floatNorm(size.height)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function setTextNodeSize(node: TextNode, size: Point & Size) {
|
|
40
|
-
const target = Math.max(1, size.height)
|
|
41
|
-
node.size = (node.size * target) / node.contentHeight
|
|
42
|
-
node.contentHeight = target
|
|
43
|
-
setNodeSize(node, size)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function scaleDistance(
|
|
47
|
-
base: Box,
|
|
48
|
-
direction: Edge | Corner,
|
|
49
|
-
anchor: Point,
|
|
50
|
-
cursor: Point,
|
|
51
|
-
) {
|
|
52
|
-
const vector = pointSubtract(cursor, anchor)
|
|
53
|
-
const localDirection = pointNorm({
|
|
54
|
-
x: direction.includes("w") ? -base.width : direction.includes("e") ? base.width : 0,
|
|
55
|
-
y: direction.includes("n") ? -base.height : direction.includes("s") ? base.height : 0,
|
|
56
|
-
})
|
|
57
|
-
const worldDirection = rotatePoint(localDirection, point(), base.rotation)
|
|
58
|
-
const distance = vectorDotProd(vector, worldDirection)
|
|
59
|
-
|
|
60
|
-
if (direction.length === 1) return distance
|
|
61
|
-
|
|
62
|
-
const diagonal = Math.hypot(base.width, base.height)
|
|
63
|
-
return diagonal === 0 ? 0 : (distance * base.height) / diagonal
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function useSingleResize(direction: Edge | Corner) {
|
|
67
|
-
const editor = useEditor()
|
|
68
|
-
const edges = direction.split("") as Edge[]
|
|
69
|
-
const [node] = useStore(editor, "selection")
|
|
70
|
-
|
|
71
|
-
const state = useRef({
|
|
72
|
-
baseSize: box(),
|
|
73
|
-
anchorPoint: point(),
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
return usePointer({
|
|
77
|
-
onDown(event) {
|
|
78
|
-
state.current = {
|
|
79
|
-
baseSize: box(node),
|
|
80
|
-
anchorPoint: cursorPosition(event, node.page),
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
onMove(event) {
|
|
84
|
-
const cursor = cursorPosition(event, node.page)
|
|
85
|
-
|
|
86
|
-
if (node instanceof TextNode && direction !== "w" && direction !== "e") {
|
|
87
|
-
const size = scaleBox(
|
|
88
|
-
state.current.baseSize,
|
|
89
|
-
direction,
|
|
90
|
-
scaleDistance(
|
|
91
|
-
state.current.baseSize,
|
|
92
|
-
direction,
|
|
93
|
-
state.current.anchorPoint,
|
|
94
|
-
cursor,
|
|
95
|
-
),
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
setTextNodeSize(node, boxRect(size))
|
|
99
|
-
} else {
|
|
100
|
-
let size = state.current.baseSize
|
|
101
|
-
|
|
102
|
-
for (const edge of edges) {
|
|
103
|
-
const r = edge === "w" ? 90 : edge === "e" ? -90 : edge === "n" ? 180 : 0
|
|
104
|
-
size = resizeBox(
|
|
105
|
-
size,
|
|
106
|
-
edge,
|
|
107
|
-
perpDistance(state.current.anchorPoint, cursor, deg(node.rotation + r)),
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
setNodeSize(node, boxRect(size))
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
onCancel() {
|
|
115
|
-
editor.action = {}
|
|
116
|
-
},
|
|
117
|
-
onEnd() {
|
|
118
|
-
editor.action = {}
|
|
119
|
-
|
|
120
|
-
const prev = boxRect(state.current.baseSize)
|
|
121
|
-
const next = rect(node)
|
|
122
|
-
|
|
123
|
-
editor.history.push({
|
|
124
|
-
redo: ["set-node-props", [node.id, pick(prev, rectProps)]],
|
|
125
|
-
undo: ["set-node-props", [node.id, pick(next, rectProps)]],
|
|
126
|
-
})
|
|
127
|
-
},
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function useMultiResize(direction: Edge | Corner) {
|
|
132
|
-
const editor = useEditor()
|
|
133
|
-
const edges = direction.split("") as Edge[]
|
|
134
|
-
|
|
135
|
-
const state = useRef({
|
|
136
|
-
initialTargetRect: rect(),
|
|
137
|
-
initialCursorPos: point(),
|
|
138
|
-
nodes: Array<{
|
|
139
|
-
node: Node
|
|
140
|
-
baseSize: Box
|
|
141
|
-
relative: Rect
|
|
142
|
-
}>(),
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
return usePointer({
|
|
146
|
-
onDown(event) {
|
|
147
|
-
const { x, y, width, height } = boxRect(selectionBox(editor.selection))
|
|
148
|
-
|
|
149
|
-
state.current = {
|
|
150
|
-
initialTargetRect: rect({ x, y, width, height }),
|
|
151
|
-
initialCursorPos: cursorPosition(event, editor.selectionPage!),
|
|
152
|
-
nodes: editor.selection
|
|
153
|
-
.values()
|
|
154
|
-
.toArray()
|
|
155
|
-
.map((node) => ({
|
|
156
|
-
node,
|
|
157
|
-
baseSize: box(node),
|
|
158
|
-
relative: rect({
|
|
159
|
-
x: (node.x - x) / width,
|
|
160
|
-
y: (node.y - y) / height,
|
|
161
|
-
width: node.width / width,
|
|
162
|
-
height: node.height / height,
|
|
163
|
-
}),
|
|
164
|
-
})),
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
onMove(event) {
|
|
169
|
-
const r = { ...state.current.initialTargetRect }
|
|
170
|
-
const p = pointSubtract(
|
|
171
|
-
cursorPosition(event, editor.selectionPage!),
|
|
172
|
-
state.current.initialCursorPos,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
/* mutate rect */ {
|
|
176
|
-
const { x, y, height, width } = state.current.initialTargetRect
|
|
177
|
-
|
|
178
|
-
if (edges.includes("n") && height - p.y > 0) {
|
|
179
|
-
r.height = height - p.y
|
|
180
|
-
r.y = y + p.y
|
|
181
|
-
}
|
|
182
|
-
if (edges.includes("s")) {
|
|
183
|
-
r.height = height + p.y
|
|
184
|
-
}
|
|
185
|
-
if (edges.includes("w") && width - p.x > 0) {
|
|
186
|
-
r.width = width - p.x
|
|
187
|
-
r.x = x + p.x
|
|
188
|
-
}
|
|
189
|
-
if (edges.includes("e")) {
|
|
190
|
-
r.width = width + p.x
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/* mutate nodes according to rect */ {
|
|
195
|
-
for (const { node, baseSize, relative } of state.current.nodes) {
|
|
196
|
-
const size = {
|
|
197
|
-
x: relative.x * r.width + r.x,
|
|
198
|
-
y: relative.y * r.height + r.y,
|
|
199
|
-
width: r.width * relative.width,
|
|
200
|
-
height: r.height * relative.height,
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (node instanceof TextNode && direction !== "w" && direction !== "e") {
|
|
204
|
-
const scaled = scaleBox(baseSize, direction, size.height - baseSize.height)
|
|
205
|
-
|
|
206
|
-
setTextNodeSize(node, {
|
|
207
|
-
x: size.x + size.width / 2 - scaled.width / 2,
|
|
208
|
-
y: size.y + size.height / 2 - scaled.height / 2,
|
|
209
|
-
width: scaled.width,
|
|
210
|
-
height: scaled.height,
|
|
211
|
-
})
|
|
212
|
-
} else {
|
|
213
|
-
setNodeSize(node, size)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
editor.action = { action: "resize", payload: r }
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
onCancel() {
|
|
222
|
-
editor.action = {}
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
onEnd() {
|
|
226
|
-
editor.action = {}
|
|
227
|
-
|
|
228
|
-
const entries: HistoryEntry[] = state.current.nodes.map(({ node, baseSize }) => ({
|
|
229
|
-
redo: ["set-node-props", [node.id, pick(node, rectProps)]],
|
|
230
|
-
undo: ["set-node-props", [node.id, pick(boxRect(baseSize), rectProps)]],
|
|
231
|
-
}))
|
|
232
|
-
|
|
233
|
-
editor.history.push({
|
|
234
|
-
redo: ["batch", entries.map((e) => e.redo)],
|
|
235
|
-
undo: ["batch", entries.map((e) => e.undo)],
|
|
236
|
-
})
|
|
237
|
-
},
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function useResize(direction: Edge | Corner) {
|
|
242
|
-
const editor = useEditor()
|
|
243
|
-
const nodes = useStore(editor, "selection")
|
|
244
|
-
const single = useSingleResize(direction)
|
|
245
|
-
const multi = useMultiResize(direction)
|
|
246
|
-
return nodes.size === 1 ? single : multi
|
|
247
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { pick } from "es-toolkit"
|
|
2
|
-
import { useRef } from "react"
|
|
3
|
-
import {
|
|
4
|
-
angle,
|
|
5
|
-
box,
|
|
6
|
-
boxRect,
|
|
7
|
-
deg,
|
|
8
|
-
floatNorm,
|
|
9
|
-
point,
|
|
10
|
-
rotatePoint,
|
|
11
|
-
type Box,
|
|
12
|
-
} from "../../model/geometry/math"
|
|
13
|
-
import type { HistoryAction } from "../../model/history"
|
|
14
|
-
import type { Node } from "../../model/node"
|
|
15
|
-
import type { Page } from "../../model/page"
|
|
16
|
-
import { selectionBox } from "../../ui/selection"
|
|
17
|
-
import { useEditor } from "../editor"
|
|
18
|
-
import { cursorPosition, usePointer } from "./pointer"
|
|
19
|
-
|
|
20
|
-
function snappedDeg(startDeg: number, delta: number, shiftKey: boolean) {
|
|
21
|
-
const rotation = startDeg + delta
|
|
22
|
-
return deg(shiftKey ? Math.round(rotation / 45) * 45 : rotation)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function useRotation() {
|
|
26
|
-
const editor = useEditor()
|
|
27
|
-
|
|
28
|
-
const state = useRef({
|
|
29
|
-
initialDeg: deg(0),
|
|
30
|
-
page: null as Page | null,
|
|
31
|
-
selectionCenter: point(),
|
|
32
|
-
nodes: Array<{
|
|
33
|
-
node: Node
|
|
34
|
-
base: Box
|
|
35
|
-
}>(),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
function applyRotation(delta: number, shiftKey: boolean) {
|
|
39
|
-
const { initialDeg, nodes, selectionCenter } = state.current
|
|
40
|
-
|
|
41
|
-
if (nodes.length > 1) {
|
|
42
|
-
const groupDelta = snappedDeg(initialDeg, delta, shiftKey) - initialDeg
|
|
43
|
-
|
|
44
|
-
for (const { node, base } of nodes) {
|
|
45
|
-
const center = rotatePoint(base.center, selectionCenter, deg(groupDelta))
|
|
46
|
-
|
|
47
|
-
node.x = floatNorm(center.x - base.width / 2)
|
|
48
|
-
node.y = floatNorm(center.y - base.height / 2)
|
|
49
|
-
node.rotation = deg(base.rotation + groupDelta)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return snappedDeg(initialDeg, delta, shiftKey)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const { node, base } of nodes) {
|
|
56
|
-
node.rotation = snappedDeg(base.rotation, delta, shiftKey)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return nodes[0]?.node.rotation ?? deg(0)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return usePointer({
|
|
63
|
-
onDown(event) {
|
|
64
|
-
const nodes = editor.selection.values().toArray()
|
|
65
|
-
const page = editor.selectionPage
|
|
66
|
-
if (nodes.length === 0 || !page) return false
|
|
67
|
-
|
|
68
|
-
const { center: selectionCenter } = selectionBox(editor.selection)
|
|
69
|
-
|
|
70
|
-
state.current = {
|
|
71
|
-
initialDeg: angle(selectionCenter, point(), cursorPosition(event, page)),
|
|
72
|
-
page,
|
|
73
|
-
selectionCenter,
|
|
74
|
-
nodes: nodes.map((node) => ({ node, base: box(node) })),
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const deg = nodes.length === 1 ? nodes[0].rotation : 0
|
|
78
|
-
editor.action = { action: "rotate", payload: { deg } }
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
onMove(event) {
|
|
82
|
-
const { initialDeg, page, selectionCenter } = state.current
|
|
83
|
-
if (!page) return
|
|
84
|
-
|
|
85
|
-
const deg =
|
|
86
|
-
angle(selectionCenter, point(), cursorPosition(event, page)) - initialDeg
|
|
87
|
-
const display = applyRotation(deg, event.shiftKey)
|
|
88
|
-
|
|
89
|
-
editor.action = {
|
|
90
|
-
action: "rotate",
|
|
91
|
-
payload: {
|
|
92
|
-
deg: display,
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
onCancel() {
|
|
98
|
-
editor.action = {}
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
onEnd(event) {
|
|
102
|
-
const { initialDeg, nodes, page, selectionCenter } = state.current
|
|
103
|
-
if (!page) return
|
|
104
|
-
|
|
105
|
-
const deg =
|
|
106
|
-
angle(selectionCenter, point(), cursorPosition(event, page)) - initialDeg
|
|
107
|
-
|
|
108
|
-
applyRotation(deg, event.shiftKey)
|
|
109
|
-
|
|
110
|
-
if (nodes.length === 1) {
|
|
111
|
-
const [n] = nodes
|
|
112
|
-
editor.history.push({
|
|
113
|
-
redo: ["set-node-props", [n.node.id, { rotation: n.node.rotation }]],
|
|
114
|
-
undo: ["set-node-props", [n.node.id, { rotation: n.base.rotation }]],
|
|
115
|
-
})
|
|
116
|
-
} else {
|
|
117
|
-
const redo: HistoryAction[] = []
|
|
118
|
-
const undo: HistoryAction[] = []
|
|
119
|
-
|
|
120
|
-
for (const { node, base } of nodes) {
|
|
121
|
-
const baseRect = boxRect(base)
|
|
122
|
-
redo.push(["set-node-props", [node.id, pick(node, ["rotation", "x", "y"])]])
|
|
123
|
-
undo.push([
|
|
124
|
-
"set-node-props",
|
|
125
|
-
[node.id, { rotation: base.rotation, x: baseRect.x, y: baseRect.y }],
|
|
126
|
-
])
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
editor.history.push({
|
|
130
|
-
redo: ["batch", redo],
|
|
131
|
-
undo: ["batch", undo],
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
editor.action = {}
|
|
136
|
-
},
|
|
137
|
-
})
|
|
138
|
-
}
|