@lazlon-platform/html-editor 0.4.0 → 0.5.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 +4 -3
- package/lib/hooks/node.ts +14 -6
- package/lib/hooks/pointer/moveable.ts +75 -54
- package/lib/hooks/pointer/pointer.ts +22 -11
- package/lib/hooks/pointer/resize.ts +176 -210
- package/lib/hooks/pointer/rotation.ts +41 -55
- package/lib/hooks/pointer/selectionFrame.ts +8 -11
- package/lib/hooks/pointer/selector.ts +48 -40
- package/lib/hooks/pointer/snap.ts +20 -20
- package/lib/model/geometry/math.ts +484 -0
- package/lib/model/geometry/svg.ts +55 -0
- package/lib/model/node/shape/arrow.ts +1 -1
- package/lib/model/node/shape/polygon.ts +23 -1
- package/lib/model/node/shape/star.ts +29 -1
- package/lib/model/node/text.ts +14 -10
- package/lib/model/node.ts +3 -7
- package/lib/model/page.ts +3 -1
- package/lib/ui/node/NodeView.tsx +1 -13
- package/lib/ui/selection.ts +9 -26
- package/package.json +34 -34
- package/lib/model/geometry.ts +0 -247
|
@@ -1,60 +1,115 @@
|
|
|
1
|
+
import { TextNode, type HistoryEntry, type Node } from "@lazlon/html-editor/model"
|
|
2
|
+
import { pick } from "es-toolkit"
|
|
1
3
|
import { useRef } from "react"
|
|
2
4
|
import { useStore } from "react-bolt"
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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"
|
|
7
27
|
import { useEditor } from "../editor"
|
|
8
|
-
import { usePointer } from "./pointer"
|
|
28
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
9
29
|
|
|
10
|
-
|
|
11
|
-
type Corner = "ne" | "nw" | "se" | "sw"
|
|
12
|
-
type Direction = Edge | Corner
|
|
30
|
+
const rectProps = ["x", "y", "width", "height"] as const
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
17
37
|
}
|
|
18
38
|
|
|
19
|
-
function
|
|
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) {
|
|
20
67
|
const editor = useEditor()
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
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
|
+
})
|
|
24
75
|
|
|
25
76
|
return usePointer({
|
|
26
|
-
onDown() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
77
|
+
onDown(event) {
|
|
78
|
+
state.current = {
|
|
79
|
+
baseSize: box(node),
|
|
80
|
+
anchorPoint: cursorPosition(event, node),
|
|
81
|
+
}
|
|
30
82
|
},
|
|
31
|
-
onMove(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let size = init.current
|
|
83
|
+
onMove(event) {
|
|
84
|
+
const cursor = cursorPosition(event, node)
|
|
35
85
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
size = resize(
|
|
41
|
-
size,
|
|
42
|
-
node.rotation,
|
|
86
|
+
if (node instanceof TextNode && direction !== "w" && direction !== "e") {
|
|
87
|
+
const size = scaleBox(
|
|
88
|
+
state.current.baseSize,
|
|
43
89
|
direction,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
scaleDistance(
|
|
91
|
+
state.current.baseSize,
|
|
92
|
+
direction,
|
|
93
|
+
state.current.anchorPoint,
|
|
94
|
+
cursor,
|
|
48
95
|
),
|
|
49
96
|
)
|
|
50
|
-
}
|
|
51
97
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
56
110
|
|
|
57
|
-
|
|
111
|
+
setNodeSize(node, boxRect(size))
|
|
112
|
+
}
|
|
58
113
|
},
|
|
59
114
|
onCancel() {
|
|
60
115
|
editor.action = {}
|
|
@@ -62,111 +117,118 @@ function useSingleResize(direction: Direction) {
|
|
|
62
117
|
onEnd() {
|
|
63
118
|
editor.action = {}
|
|
64
119
|
|
|
65
|
-
const prev =
|
|
66
|
-
const next =
|
|
67
|
-
x: node.x,
|
|
68
|
-
y: node.y,
|
|
69
|
-
width: node.width,
|
|
70
|
-
height: node.height,
|
|
71
|
-
}
|
|
120
|
+
const prev = boxRect(state.current.baseSize)
|
|
121
|
+
const next = rect(node)
|
|
72
122
|
|
|
73
123
|
editor.history.push({
|
|
74
|
-
redo: ["set-node-props", [node.id,
|
|
75
|
-
undo: ["set-node-props", [node.id,
|
|
124
|
+
redo: ["set-node-props", [node.id, pick(prev, rectProps)]],
|
|
125
|
+
undo: ["set-node-props", [node.id, pick(next, rectProps)]],
|
|
76
126
|
})
|
|
77
127
|
},
|
|
78
128
|
})
|
|
79
129
|
}
|
|
80
130
|
|
|
81
|
-
function useMultiResize(direction:
|
|
131
|
+
function useMultiResize(direction: Edge | Corner) {
|
|
82
132
|
const editor = useEditor()
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
nodes:
|
|
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
|
+
}>(),
|
|
89
143
|
})
|
|
90
144
|
|
|
91
145
|
return usePointer({
|
|
92
|
-
onDown() {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
width: node.width / width,
|
|
112
|
-
height: node.height / height,
|
|
113
|
-
})),
|
|
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
|
+
})),
|
|
114
165
|
}
|
|
115
166
|
},
|
|
116
|
-
onMove({ start, event }) {
|
|
117
|
-
const px = Math.round((event.clientX - start.clientX) / editor.zoom)
|
|
118
|
-
const py = Math.round((event.clientY - start.clientY) / editor.zoom)
|
|
119
167
|
|
|
120
|
-
|
|
168
|
+
onMove(event) {
|
|
169
|
+
const r = { ...state.current.initialTargetRect }
|
|
170
|
+
const p = pointSubtract(
|
|
171
|
+
cursorPosition(event, editor.selectionPage!),
|
|
172
|
+
state.current.initialCursorPos,
|
|
173
|
+
)
|
|
121
174
|
|
|
122
175
|
/* mutate rect */ {
|
|
123
|
-
const { x, y, height, width } =
|
|
176
|
+
const { x, y, height, width } = state.current.initialTargetRect
|
|
124
177
|
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
178
|
+
if (edges.includes("n") && height - p.y > 0) {
|
|
179
|
+
r.height = height - p.y
|
|
180
|
+
r.y = y + p.y
|
|
128
181
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
182
|
+
if (edges.includes("s")) {
|
|
183
|
+
r.height = height + p.y
|
|
131
184
|
}
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
185
|
+
if (edges.includes("w") && width - p.x > 0) {
|
|
186
|
+
r.width = width - p.x
|
|
187
|
+
r.x = x + p.x
|
|
135
188
|
}
|
|
136
|
-
if (
|
|
137
|
-
|
|
189
|
+
if (edges.includes("e")) {
|
|
190
|
+
r.width = width + p.x
|
|
138
191
|
}
|
|
139
192
|
}
|
|
140
193
|
|
|
141
194
|
/* mutate nodes according to rect */ {
|
|
142
|
-
for (const { node,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
215
|
}
|
|
148
216
|
}
|
|
149
217
|
|
|
150
|
-
editor.action = { action: "resize", payload:
|
|
218
|
+
editor.action = { action: "resize", payload: r }
|
|
151
219
|
},
|
|
220
|
+
|
|
152
221
|
onCancel() {
|
|
153
222
|
editor.action = {}
|
|
154
223
|
},
|
|
224
|
+
|
|
155
225
|
onEnd() {
|
|
156
226
|
editor.action = {}
|
|
157
227
|
|
|
158
|
-
const entries =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
[node.id, { x: node.x, y: node.y, width: node.width, height: node.height }],
|
|
163
|
-
],
|
|
164
|
-
undo: [
|
|
165
|
-
"set-node-props",
|
|
166
|
-
[node.id, { width: undo.width, height: undo.height, x: undo.x, y: undo.y }],
|
|
167
|
-
],
|
|
168
|
-
}),
|
|
169
|
-
)
|
|
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
|
+
}))
|
|
170
232
|
|
|
171
233
|
editor.history.push({
|
|
172
234
|
redo: ["batch", entries.map((e) => e.redo)],
|
|
@@ -176,106 +238,10 @@ function useMultiResize(direction: Direction) {
|
|
|
176
238
|
})
|
|
177
239
|
}
|
|
178
240
|
|
|
179
|
-
|
|
180
|
-
* Assumptions (matches typical HTML/CSS editors):
|
|
181
|
-
* - node.x, node.y are the unrotated rectangle's TOP-LEFT (CSS left/top).
|
|
182
|
-
* - rotation is around the rectangle CENTER (transform-origin: center).
|
|
183
|
-
* - resizing keeps the OPPOSITE side fixed in world space (so it won't "drift" when rotated).
|
|
184
|
-
* - n is the delta to width/height (positive grows, negative shrinks).
|
|
185
|
-
*/
|
|
186
|
-
function resize(rect: Rect, deg: number, side: "n" | "s" | "e" | "w", n: number): Rect {
|
|
187
|
-
const minSize = 1
|
|
188
|
-
|
|
189
|
-
const w0 = rect.width
|
|
190
|
-
const h0 = rect.height
|
|
191
|
-
|
|
192
|
-
const w1 = side === "e" || side === "w" ? Math.max(minSize, w0 + n) : w0
|
|
193
|
-
const h1 = side === "n" || side === "s" ? Math.max(minSize, h0 + n) : h0
|
|
194
|
-
|
|
195
|
-
const hx0 = w0 / 2
|
|
196
|
-
const hy0 = h0 / 2
|
|
197
|
-
const hx1 = w1 / 2
|
|
198
|
-
const hy1 = h1 / 2
|
|
199
|
-
|
|
200
|
-
const theta = (deg * Math.PI) / 180
|
|
201
|
-
const cos = Math.cos(theta)
|
|
202
|
-
const sin = Math.sin(theta)
|
|
203
|
-
|
|
204
|
-
const rot = (p: { x: number; y: number }) => ({
|
|
205
|
-
x: p.x * cos - p.y * sin,
|
|
206
|
-
y: p.x * sin + p.y * cos,
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
// Center of the rect in world coords (since x,y is top-left pre-rotation)
|
|
210
|
-
const c0 = { x: rect.x + hx0, y: rect.y + hy0 }
|
|
211
|
-
|
|
212
|
-
// Opposite side midpoint is the anchor (fixed in world space)
|
|
213
|
-
const anchorLocal0 =
|
|
214
|
-
side === "e"
|
|
215
|
-
? { x: -hx0, y: 0 }
|
|
216
|
-
: side === "w"
|
|
217
|
-
? { x: hx0, y: 0 }
|
|
218
|
-
: side === "s"
|
|
219
|
-
? { x: 0, y: -hy0 }
|
|
220
|
-
: { x: 0, y: hy0 } // side === "n"
|
|
221
|
-
|
|
222
|
-
const a = rot(anchorLocal0)
|
|
223
|
-
const anchorWorld = { x: c0.x + a.x, y: c0.y + a.y }
|
|
224
|
-
|
|
225
|
-
// After resize, the anchor local point changes because half-extents changed
|
|
226
|
-
const anchorLocal1 =
|
|
227
|
-
side === "e"
|
|
228
|
-
? { x: -hx1, y: 0 }
|
|
229
|
-
: side === "w"
|
|
230
|
-
? { x: hx1, y: 0 }
|
|
231
|
-
: side === "s"
|
|
232
|
-
? { x: 0, y: -hy1 }
|
|
233
|
-
: { x: 0, y: hy1 } // side === "n"
|
|
234
|
-
|
|
235
|
-
// Solve for new center so the anchor stays in the same world position:
|
|
236
|
-
// anchorWorld = c1 + R * anchorLocal1 => c1 = anchorWorld - R * anchorLocal1
|
|
237
|
-
const a1 = rot(anchorLocal1)
|
|
238
|
-
const c1 = { x: anchorWorld.x - a1.x, y: anchorWorld.y - a1.y }
|
|
239
|
-
|
|
240
|
-
// Convert center back to unrotated top-left
|
|
241
|
-
const x1 = c1.x - hx1
|
|
242
|
-
const y1 = c1.y - hy1
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
x: x1,
|
|
246
|
-
y: y1,
|
|
247
|
-
width: w1,
|
|
248
|
-
height: h1,
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Perpendicular (signed) distance from point b to the infinite line
|
|
254
|
-
* that passes through point a with direction `deg` (degrees).
|
|
255
|
-
*/
|
|
256
|
-
function distance(a: Point, b: Point, deg: number): number {
|
|
257
|
-
const t = (deg * Math.PI) / 180
|
|
258
|
-
|
|
259
|
-
// unit direction along the line
|
|
260
|
-
const dx = Math.cos(t)
|
|
261
|
-
const dy = Math.sin(t)
|
|
262
|
-
|
|
263
|
-
// unit normal (perpendicular) to the line
|
|
264
|
-
const nx = -dy
|
|
265
|
-
const ny = dx
|
|
266
|
-
|
|
267
|
-
// vector from a to b
|
|
268
|
-
const vx = b.x - a.x
|
|
269
|
-
const vy = b.y - a.y
|
|
270
|
-
|
|
271
|
-
// projection onto normal => signed perpendicular distance
|
|
272
|
-
return vx * nx + vy * ny
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function useResize(direction: Direction) {
|
|
241
|
+
export function useResize(direction: Edge | Corner) {
|
|
276
242
|
const editor = useEditor()
|
|
277
|
-
const
|
|
243
|
+
const nodes = useStore(editor, "selection")
|
|
278
244
|
const single = useSingleResize(direction)
|
|
279
245
|
const multi = useMultiResize(direction)
|
|
280
|
-
return
|
|
246
|
+
return nodes.size === 1 ? single : multi
|
|
281
247
|
}
|
|
@@ -1,71 +1,58 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
-
import {
|
|
3
|
-
import { angle, type Point } from "../../model/geometry"
|
|
2
|
+
import { angle, deg, point, rectCenter, type Deg } from "../../model/geometry/math"
|
|
4
3
|
import type { HistoryAction } from "../../model/history"
|
|
5
4
|
import type { Node } from "../../model/node"
|
|
6
|
-
import {
|
|
5
|
+
import { selectionDOMRect } from "../../ui/selection"
|
|
7
6
|
import { useEditor } from "../editor"
|
|
8
7
|
import { usePointer } from "./pointer"
|
|
9
8
|
|
|
10
|
-
type RotationState = {
|
|
11
|
-
anchor: Point
|
|
12
|
-
center: Point
|
|
13
|
-
deg: number
|
|
14
|
-
nodes: Array<{ node: Node; deg: number }>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
9
|
function snappedDeg(startDeg: number, delta: number, shiftKey: boolean) {
|
|
18
10
|
const rotation = startDeg + delta
|
|
19
|
-
return shiftKey ? Math.round(rotation / 45) * 45 : rotation
|
|
11
|
+
return deg(shiftKey ? Math.round(rotation / 45) * 45 : rotation)
|
|
20
12
|
}
|
|
21
13
|
|
|
22
14
|
export function useRotation() {
|
|
23
|
-
const start = useRef<RotationState | null>(null)
|
|
24
15
|
const editor = useEditor()
|
|
25
|
-
|
|
26
|
-
const
|
|
16
|
+
|
|
17
|
+
const state = useRef({
|
|
18
|
+
centerPoint: point(),
|
|
19
|
+
inititalDeg: deg(0),
|
|
20
|
+
nodes: Array<{ node: Node; deg: Deg }>(),
|
|
21
|
+
})
|
|
27
22
|
|
|
28
23
|
return usePointer({
|
|
29
24
|
onDown(event) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
y: event.clientY,
|
|
42
|
-
})
|
|
43
|
-
start.current = {
|
|
44
|
-
anchor,
|
|
45
|
-
center,
|
|
46
|
-
deg,
|
|
47
|
-
nodes: selectionArray.map((node) => ({
|
|
48
|
-
node,
|
|
49
|
-
deg: node.rotation,
|
|
50
|
-
})),
|
|
25
|
+
const target = selectionDOMRect(editor.selection)
|
|
26
|
+
const nodes = editor.selection.values().toArray()
|
|
27
|
+
const center = rectCenter(target)
|
|
28
|
+
|
|
29
|
+
state.current = {
|
|
30
|
+
centerPoint: center,
|
|
31
|
+
inititalDeg: angle(center, point(), {
|
|
32
|
+
x: event.clientX,
|
|
33
|
+
y: event.clientY,
|
|
34
|
+
}),
|
|
35
|
+
nodes: nodes.map((node) => ({ node, deg: node.rotation })),
|
|
51
36
|
}
|
|
52
|
-
|
|
37
|
+
|
|
38
|
+
const deg = nodes.length === 1 ? nodes[0].rotation : 0
|
|
39
|
+
editor.action = { action: "rotate", payload: { deg } }
|
|
53
40
|
},
|
|
54
|
-
onMove({ event }) {
|
|
55
|
-
const s = start.current
|
|
56
|
-
if (!s) return
|
|
57
41
|
|
|
58
|
-
|
|
59
|
-
const
|
|
42
|
+
onMove(event) {
|
|
43
|
+
const { centerPoint, inititalDeg, nodes } = state.current
|
|
44
|
+
|
|
45
|
+
const clientPoint = { x: event.clientX, y: event.clientY }
|
|
46
|
+
const deg = angle(centerPoint, point(), clientPoint) - inititalDeg
|
|
60
47
|
|
|
61
|
-
for (const n of
|
|
48
|
+
for (const n of nodes) {
|
|
62
49
|
n.node.rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
const display =
|
|
66
|
-
|
|
67
|
-
?
|
|
68
|
-
: snappedDeg(
|
|
53
|
+
nodes.length === 1
|
|
54
|
+
? nodes[0].node.rotation
|
|
55
|
+
: snappedDeg(inititalDeg, deg, event.shiftKey)
|
|
69
56
|
|
|
70
57
|
editor.action = {
|
|
71
58
|
action: "rotate",
|
|
@@ -74,19 +61,19 @@ export function useRotation() {
|
|
|
74
61
|
},
|
|
75
62
|
}
|
|
76
63
|
},
|
|
64
|
+
|
|
77
65
|
onCancel() {
|
|
78
66
|
editor.action = {}
|
|
79
|
-
start.current = null
|
|
80
67
|
},
|
|
81
|
-
onEnd({ event }) {
|
|
82
|
-
const s = start.current
|
|
83
|
-
if (!s) return
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
const
|
|
69
|
+
onEnd(event) {
|
|
70
|
+
const { centerPoint, inititalDeg, nodes } = state.current
|
|
71
|
+
|
|
72
|
+
const clientPoint = { x: event.clientX, y: event.clientY }
|
|
73
|
+
const deg = angle(centerPoint, point(), clientPoint) - inititalDeg
|
|
87
74
|
|
|
88
|
-
if (
|
|
89
|
-
const [n] =
|
|
75
|
+
if (nodes.length === 0) {
|
|
76
|
+
const [n] = nodes
|
|
90
77
|
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
91
78
|
editor.history.push({
|
|
92
79
|
redo: ["set-node-props", [n.node.id, { rotation }]],
|
|
@@ -97,7 +84,7 @@ export function useRotation() {
|
|
|
97
84
|
const redo: HistoryAction[] = []
|
|
98
85
|
const undo: HistoryAction[] = []
|
|
99
86
|
|
|
100
|
-
for (const n of
|
|
87
|
+
for (const n of nodes) {
|
|
101
88
|
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
102
89
|
redo.push(["set-node-props", [n.node.id, { rotation }]])
|
|
103
90
|
undo.push(["set-node-props", [n.node.id, { rotation: n.deg }]])
|
|
@@ -111,7 +98,6 @@ export function useRotation() {
|
|
|
111
98
|
}
|
|
112
99
|
|
|
113
100
|
editor.action = {}
|
|
114
|
-
start.current = null
|
|
115
101
|
},
|
|
116
102
|
})
|
|
117
103
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react"
|
|
2
2
|
import { useComputed, useStore } from "react-bolt"
|
|
3
|
-
import {
|
|
3
|
+
import { pointSubtract } from "../../model/geometry/math"
|
|
4
|
+
import { selectionDOMRect } from "../../ui/selection"
|
|
4
5
|
import { useEditor } from "../editor"
|
|
5
6
|
|
|
6
7
|
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
|
@@ -73,23 +74,19 @@ export function useSelectionFrame<E extends HTMLElement>(props?: {
|
|
|
73
74
|
|
|
74
75
|
if (props?.accountForSingleSelection && selection.size === 1 && frame) {
|
|
75
76
|
const [firstNode] = selection
|
|
76
|
-
const { page } = firstNode
|
|
77
|
-
|
|
78
77
|
const { x, y, width, height, rotation } = firstNode
|
|
79
|
-
const relative = page.ref!.getBoundingClientRect()
|
|
78
|
+
const relative = firstNode.page.ref!.getBoundingClientRect()
|
|
80
79
|
const tx = relative.x - stage.x + x * zoom
|
|
81
80
|
const ty = relative.y - stage.y + y * zoom
|
|
82
|
-
|
|
83
81
|
frame.style.height = `${height * zoom}px`
|
|
84
82
|
frame.style.width = `${width * zoom}px`
|
|
85
83
|
frame.style.transform = `translate(${tx}px, ${ty}px) rotate(${rotation}deg)`
|
|
86
84
|
} else if (frame) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
frame.style.
|
|
91
|
-
frame.style.
|
|
92
|
-
frame.style.transform = `translate(${tx}px, ${ty}px)`
|
|
85
|
+
const dom = selectionDOMRect(selection)
|
|
86
|
+
const t = pointSubtract(dom, stage)
|
|
87
|
+
frame.style.height = `${dom.height}px`
|
|
88
|
+
frame.style.width = `${dom.width}px`
|
|
89
|
+
frame.style.transform = `translate(${t.x}px, ${t.y}px)`
|
|
93
90
|
}
|
|
94
91
|
})
|
|
95
92
|
|