@lazlon-platform/html-editor 0.5.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.
- package/lib/hooks/actions.ts +51 -24
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +22 -8
- package/lib/hooks/pointer/pointer.ts +2 -3
- package/lib/hooks/pointer/resize.ts +2 -2
- package/lib/hooks/pointer/rotation.ts +74 -39
- package/lib/hooks/pointer/selector.ts +16 -2
- package/lib/hooks/pointer/snap.ts +3 -3
- package/lib/hooks/textMarks.ts +1 -3
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +13 -9
- package/lib/model/geometry/math.ts +139 -0
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +7 -10
- package/lib/model/node/{editable → editableNode}/index.ts +13 -29
- package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
- package/lib/model/node/{group.ts → groupNode.ts} +9 -13
- package/lib/model/node/{image.ts → imageNode.ts} +5 -11
- package/lib/model/node/lineNode.ts +59 -0
- package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
- package/lib/model/node/shapeNode/shape.ts +96 -0
- package/lib/model/node/{text.ts → textNode.ts} +6 -12
- package/lib/model/node.ts +9 -23
- package/lib/model/page.ts +1 -2
- package/lib/model/traversal.ts +1 -1
- package/lib/ui/extractor.ts +3 -3
- package/lib/ui/index.ts +2 -4
- package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +32 -0
- package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
- package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
- package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
- package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
- package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
- package/lib/ui/node/ShapeContent/index.tsx +43 -0
- package/lib/ui/node/TextContent.tsx +1 -1
- package/package.json +1 -1
- package/lib/model/node/shape/arrow.ts +0 -50
- package/lib/model/node/shape/ellipse.ts +0 -26
- package/lib/model/node/shape/polygon.ts +0 -130
- package/lib/model/node/shape/star.ts +0 -91
- package/lib/ui/node/ArrowContent.tsx +0 -60
- package/lib/ui/node/EllipseContent.tsx +0 -49
- package/lib/ui/node/PolygonContent.tsx +0 -81
- package/lib/ui/node/StarContent.tsx +0 -60
- /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
- /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
package/lib/hooks/actions.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
import {
|
|
12
|
+
boxBounds,
|
|
13
|
+
deg,
|
|
14
|
+
floatNorm,
|
|
15
|
+
rectCenter,
|
|
16
|
+
rotatePoint,
|
|
17
|
+
} from "../model/geometry/math"
|
|
8
18
|
import { selectionBox } from "../ui/selection"
|
|
9
19
|
import { useBatchSet } from "./batch"
|
|
10
20
|
import { useEditor } from "./editor"
|
|
11
|
-
import { boxBounds } from "../model/geometry/math"
|
|
12
21
|
|
|
13
22
|
export function clone(
|
|
14
23
|
editor: Editor,
|
|
15
24
|
node: Node,
|
|
16
25
|
props?: ((props: NodeProps) => Partial<NodeProps>) | Partial<NodeProps>,
|
|
17
26
|
) {
|
|
18
|
-
const copy = node.
|
|
27
|
+
const copy = node.props
|
|
19
28
|
const newProps = typeof props === "function" ? props(copy) : props
|
|
20
29
|
const newNode = editor.deserializeNode(node.page, {
|
|
21
30
|
props: { ...copy, ...newProps },
|
|
@@ -35,14 +44,14 @@ export function useAddNodeAction(page?: Page) {
|
|
|
35
44
|
|
|
36
45
|
return function addNode<N extends NodeConstructor>(
|
|
37
46
|
NodeClass: N,
|
|
38
|
-
props?: Partial<
|
|
47
|
+
props?: Partial<N["prototype"]["props"]>,
|
|
39
48
|
) {
|
|
40
49
|
const [firstPage] = editor.pages.values()
|
|
41
50
|
const targetPage = page ?? firstPage
|
|
42
51
|
if (!targetPage) return
|
|
43
52
|
const { width, height, nodes } = targetPage
|
|
44
53
|
|
|
45
|
-
const node = new NodeClass(
|
|
54
|
+
const node = new NodeClass(targetPage, {
|
|
46
55
|
id: editor.id(),
|
|
47
56
|
x: Math.round(width / 2) - 50,
|
|
48
57
|
y: Math.round(height / 2) - 50,
|
|
@@ -53,7 +62,7 @@ export function useAddNodeAction(page?: Page) {
|
|
|
53
62
|
|
|
54
63
|
targetPage.nodes = new Map([...nodes, [node.id, node]])
|
|
55
64
|
editor.history.push({
|
|
56
|
-
redo: ["add-node", [targetPage.id, [
|
|
65
|
+
redo: ["add-node", [targetPage.id, [editor.serializeNode(node)]]],
|
|
57
66
|
undo: ["delete-node", [targetPage.id, [node.id]]],
|
|
58
67
|
})
|
|
59
68
|
|
|
@@ -83,7 +92,7 @@ export function useTrashAction() {
|
|
|
83
92
|
|
|
84
93
|
editor.history.push({
|
|
85
94
|
redo: ["delete-node", [page.id, array.map((node) => node.id)]],
|
|
86
|
-
undo: ["add-node", [page.id, array.map((n) =>
|
|
95
|
+
undo: ["add-node", [page.id, array.map((n) => editor.serializeNode(n))]],
|
|
87
96
|
})
|
|
88
97
|
|
|
89
98
|
editor.selection = new Set()
|
|
@@ -99,22 +108,25 @@ export function useGroupAction() {
|
|
|
99
108
|
const { width, height, x, y } = boxBounds(selectionBox(selection))
|
|
100
109
|
if (selection.size === 0 || !page) return
|
|
101
110
|
|
|
102
|
-
const group = new GroupNode(
|
|
111
|
+
const group = new GroupNode(page, {
|
|
103
112
|
id: editor.id(),
|
|
104
113
|
width,
|
|
105
114
|
height,
|
|
106
115
|
x,
|
|
107
116
|
y,
|
|
108
|
-
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
group.nodes = new Set(
|
|
120
|
+
selection
|
|
109
121
|
.values()
|
|
110
122
|
.map((node) =>
|
|
111
123
|
clone(editor, node, (n) => ({
|
|
112
124
|
x: n.x - x,
|
|
113
125
|
y: n.y - y,
|
|
114
|
-
}))
|
|
126
|
+
})),
|
|
115
127
|
)
|
|
116
128
|
.toArray(),
|
|
117
|
-
|
|
129
|
+
)
|
|
118
130
|
|
|
119
131
|
editor.selection = new Set()
|
|
120
132
|
page.nodes = new Map([
|
|
@@ -139,7 +151,7 @@ export function useGroupAction() {
|
|
|
139
151
|
.toArray(),
|
|
140
152
|
],
|
|
141
153
|
],
|
|
142
|
-
["add-node", [page.id, [
|
|
154
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
143
155
|
],
|
|
144
156
|
],
|
|
145
157
|
undo: [
|
|
@@ -152,7 +164,7 @@ export function useGroupAction() {
|
|
|
152
164
|
page.id,
|
|
153
165
|
selection
|
|
154
166
|
.values()
|
|
155
|
-
.map((n) =>
|
|
167
|
+
.map((n) => editor.serializeNode(n))
|
|
156
168
|
.toArray(),
|
|
157
169
|
],
|
|
158
170
|
],
|
|
@@ -163,13 +175,28 @@ export function useGroupAction() {
|
|
|
163
175
|
|
|
164
176
|
function ungroup(group: GroupNode) {
|
|
165
177
|
const page = group.page
|
|
178
|
+
const groupCenter = rectCenter(group)
|
|
166
179
|
const nodes = group.nodes
|
|
167
180
|
.values()
|
|
168
181
|
.map((node) =>
|
|
169
|
-
clone(editor, node, (n) =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}),
|
|
173
200
|
)
|
|
174
201
|
.toArray()
|
|
175
202
|
|
|
@@ -184,14 +211,14 @@ export function useGroupAction() {
|
|
|
184
211
|
"batch",
|
|
185
212
|
[
|
|
186
213
|
["delete-node", [page.id, [group.id]]],
|
|
187
|
-
["add-node", [page.id, nodes.map((n) =>
|
|
214
|
+
["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
|
|
188
215
|
],
|
|
189
216
|
],
|
|
190
217
|
undo: [
|
|
191
218
|
"batch",
|
|
192
219
|
[
|
|
193
220
|
["delete-node", [page.id, nodes.map((node) => node.id)]],
|
|
194
|
-
["add-node", [page.id, [
|
|
221
|
+
["add-node", [page.id, [editor.serializeNode(group)]]],
|
|
195
222
|
],
|
|
196
223
|
],
|
|
197
224
|
})
|
|
@@ -226,7 +253,7 @@ export function useDuplicateAction() {
|
|
|
226
253
|
|
|
227
254
|
editor.selection = new Set(nodes)
|
|
228
255
|
editor.history.push({
|
|
229
|
-
redo: ["add-node", [page.id, nodes.map((n) =>
|
|
256
|
+
redo: ["add-node", [page.id, nodes.map((n) => editor.serializeNode(n))]],
|
|
230
257
|
undo: ["delete-node", [page.id, nodes.map((node) => node.id)]],
|
|
231
258
|
})
|
|
232
259
|
}
|
package/lib/hooks/batch.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/lib/hooks/index.ts
CHANGED
|
@@ -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"
|
|
@@ -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,8 +1,10 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
2
|
import {
|
|
3
|
+
accessibleLine,
|
|
3
4
|
box,
|
|
4
5
|
boxBounds,
|
|
5
6
|
boxContainsPoint,
|
|
7
|
+
lineContainsPoint,
|
|
6
8
|
type Point,
|
|
7
9
|
point,
|
|
8
10
|
pointSubtract,
|
|
@@ -14,11 +16,12 @@ import { selectionBox } from "../../ui/selection"
|
|
|
14
16
|
import { useEditor, usePage } from "../editor"
|
|
15
17
|
import { cursorPosition, usePointer } from "./pointer"
|
|
16
18
|
import { useSnap } from "./snap"
|
|
19
|
+
import { LineNode } from "@lazlon/html-editor/model"
|
|
17
20
|
|
|
18
21
|
export function useMoveable() {
|
|
19
22
|
const editor = useEditor()
|
|
20
23
|
const page = usePage()
|
|
21
|
-
const snap = useSnap()
|
|
24
|
+
const snap = useSnap(page)
|
|
22
25
|
|
|
23
26
|
const state = useRef({
|
|
24
27
|
initialSelectionRect: rect(),
|
|
@@ -30,15 +33,30 @@ export function useMoveable() {
|
|
|
30
33
|
}>(),
|
|
31
34
|
})
|
|
32
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
|
+
}
|
|
50
|
+
|
|
33
51
|
return usePointer({
|
|
34
52
|
onDown(event) {
|
|
35
53
|
const cursor = cursorPosition(event, page)
|
|
36
54
|
|
|
37
55
|
// clicked outside of selection, try grabbing nodes under the cursor
|
|
38
|
-
if (!
|
|
56
|
+
if (!isCursorInSelection(cursor)) {
|
|
39
57
|
const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
|
|
40
58
|
for (const node of stackOrderedNodes) {
|
|
41
|
-
if (
|
|
59
|
+
if (isNodeHit(node, cursor)) {
|
|
42
60
|
editor.selection = new Set([node])
|
|
43
61
|
break
|
|
44
62
|
}
|
|
@@ -50,11 +68,7 @@ export function useMoveable() {
|
|
|
50
68
|
const initialSelectionBox = selectionBox(editor.selection)
|
|
51
69
|
const initialSelectionRect = boxBounds(initialSelectionBox)
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
if (node.blockMove(event)) return false
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (boxContainsPoint(initialSelectionBox, cursor)) {
|
|
71
|
+
if (isCursorInSelection(cursor)) {
|
|
58
72
|
state.current = {
|
|
59
73
|
initialSelectionRect,
|
|
60
74
|
cursorOffset: pointSubtract(cursor, initialSelectionRect),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Page
|
|
1
|
+
import { Page } from "@lazlon/html-editor/model"
|
|
2
2
|
import { useCallback, useRef } from "react"
|
|
3
3
|
import { floatNorm, type Point } from "../../model/geometry/math"
|
|
4
4
|
|
|
@@ -11,9 +11,8 @@ export type UsePointerProps = {
|
|
|
11
11
|
|
|
12
12
|
export function cursorPosition(
|
|
13
13
|
event: { clientX: number; clientY: number },
|
|
14
|
-
|
|
14
|
+
page: Page,
|
|
15
15
|
): Point {
|
|
16
|
-
const page = relativeTo instanceof Page ? relativeTo : relativeTo.page
|
|
17
16
|
const { zoom } = page.editor
|
|
18
17
|
const { x, y } = page.ref!.getBoundingClientRect()
|
|
19
18
|
|
|
@@ -77,11 +77,11 @@ export function useSingleResize(direction: Edge | Corner) {
|
|
|
77
77
|
onDown(event) {
|
|
78
78
|
state.current = {
|
|
79
79
|
baseSize: box(node),
|
|
80
|
-
anchorPoint: cursorPosition(event, node),
|
|
80
|
+
anchorPoint: cursorPosition(event, node.page),
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
onMove(event) {
|
|
84
|
-
const cursor = cursorPosition(event, node)
|
|
84
|
+
const cursor = cursorPosition(event, node.page)
|
|
85
85
|
|
|
86
86
|
if (node instanceof TextNode && direction !== "w" && direction !== "e") {
|
|
87
87
|
const size = scaleBox(
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
import { pick } from "es-toolkit"
|
|
1
2
|
import { useRef } from "react"
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
angle,
|
|
5
|
+
box,
|
|
6
|
+
boxRect,
|
|
7
|
+
deg,
|
|
8
|
+
floatNorm,
|
|
9
|
+
point,
|
|
10
|
+
rotatePoint,
|
|
11
|
+
type Box,
|
|
12
|
+
} from "../../model/geometry/math"
|
|
3
13
|
import type { HistoryAction } from "../../model/history"
|
|
4
14
|
import type { Node } from "../../model/node"
|
|
5
|
-
import {
|
|
15
|
+
import type { Page } from "../../model/page"
|
|
16
|
+
import { selectionBox } from "../../ui/selection"
|
|
6
17
|
import { useEditor } from "../editor"
|
|
7
|
-
import { usePointer } from "./pointer"
|
|
18
|
+
import { cursorPosition, usePointer } from "./pointer"
|
|
8
19
|
|
|
9
20
|
function snappedDeg(startDeg: number, delta: number, shiftKey: boolean) {
|
|
10
21
|
const rotation = startDeg + delta
|
|
@@ -15,24 +26,52 @@ export function useRotation() {
|
|
|
15
26
|
const editor = useEditor()
|
|
16
27
|
|
|
17
28
|
const state = useRef({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
initialDeg: deg(0),
|
|
30
|
+
page: null as Page | null,
|
|
31
|
+
selectionCenter: point(),
|
|
32
|
+
nodes: Array<{
|
|
33
|
+
node: Node
|
|
34
|
+
base: Box
|
|
35
|
+
}>(),
|
|
21
36
|
})
|
|
22
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
|
+
|
|
23
62
|
return usePointer({
|
|
24
63
|
onDown(event) {
|
|
25
|
-
const target = selectionDOMRect(editor.selection)
|
|
26
64
|
const nodes = editor.selection.values().toArray()
|
|
27
|
-
const
|
|
65
|
+
const page = editor.selectionPage
|
|
66
|
+
if (nodes.length === 0 || !page) return false
|
|
67
|
+
|
|
68
|
+
const { center: selectionCenter } = selectionBox(editor.selection)
|
|
28
69
|
|
|
29
70
|
state.current = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}),
|
|
35
|
-
nodes: nodes.map((node) => ({ node, deg: node.rotation })),
|
|
71
|
+
initialDeg: angle(selectionCenter, point(), cursorPosition(event, page)),
|
|
72
|
+
page,
|
|
73
|
+
selectionCenter,
|
|
74
|
+
nodes: nodes.map((node) => ({ node, base: box(node) })),
|
|
36
75
|
}
|
|
37
76
|
|
|
38
77
|
const deg = nodes.length === 1 ? nodes[0].rotation : 0
|
|
@@ -40,19 +79,12 @@ export function useRotation() {
|
|
|
40
79
|
},
|
|
41
80
|
|
|
42
81
|
onMove(event) {
|
|
43
|
-
const {
|
|
44
|
-
|
|
45
|
-
const clientPoint = { x: event.clientX, y: event.clientY }
|
|
46
|
-
const deg = angle(centerPoint, point(), clientPoint) - inititalDeg
|
|
82
|
+
const { initialDeg, page, selectionCenter } = state.current
|
|
83
|
+
if (!page) return
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const display =
|
|
53
|
-
nodes.length === 1
|
|
54
|
-
? nodes[0].node.rotation
|
|
55
|
-
: snappedDeg(inititalDeg, deg, event.shiftKey)
|
|
85
|
+
const deg =
|
|
86
|
+
angle(selectionCenter, point(), cursorPosition(event, page)) - initialDeg
|
|
87
|
+
const display = applyRotation(deg, event.shiftKey)
|
|
56
88
|
|
|
57
89
|
editor.action = {
|
|
58
90
|
action: "rotate",
|
|
@@ -67,28 +99,31 @@ export function useRotation() {
|
|
|
67
99
|
},
|
|
68
100
|
|
|
69
101
|
onEnd(event) {
|
|
70
|
-
const {
|
|
102
|
+
const { initialDeg, nodes, page, selectionCenter } = state.current
|
|
103
|
+
if (!page) return
|
|
104
|
+
|
|
105
|
+
const deg =
|
|
106
|
+
angle(selectionCenter, point(), cursorPosition(event, page)) - initialDeg
|
|
71
107
|
|
|
72
|
-
|
|
73
|
-
const deg = angle(centerPoint, point(), clientPoint) - inititalDeg
|
|
108
|
+
applyRotation(deg, event.shiftKey)
|
|
74
109
|
|
|
75
|
-
if (nodes.length ===
|
|
110
|
+
if (nodes.length === 1) {
|
|
76
111
|
const [n] = nodes
|
|
77
|
-
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
78
112
|
editor.history.push({
|
|
79
|
-
redo: ["set-node-props", [n.node.id, { rotation }]],
|
|
80
|
-
undo: ["set-node-props", [n.node.id, { rotation: n.
|
|
113
|
+
redo: ["set-node-props", [n.node.id, { rotation: n.node.rotation }]],
|
|
114
|
+
undo: ["set-node-props", [n.node.id, { rotation: n.base.rotation }]],
|
|
81
115
|
})
|
|
82
|
-
n.node.rotation = rotation
|
|
83
116
|
} else {
|
|
84
117
|
const redo: HistoryAction[] = []
|
|
85
118
|
const undo: HistoryAction[] = []
|
|
86
119
|
|
|
87
|
-
for (const
|
|
88
|
-
const
|
|
89
|
-
redo.push(["set-node-props", [
|
|
90
|
-
undo.push([
|
|
91
|
-
|
|
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
|
+
])
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
editor.history.push({
|
|
@@ -6,9 +6,13 @@ import {
|
|
|
6
6
|
pointSubtract,
|
|
7
7
|
rect,
|
|
8
8
|
boxIntersects,
|
|
9
|
+
lineContainsPoint,
|
|
10
|
+
lineIntersectsBox,
|
|
11
|
+
accessibleLine,
|
|
9
12
|
} from "../../model/geometry/math"
|
|
10
13
|
import { useEditor } from "../editor"
|
|
11
14
|
import { cursorPosition, usePointer } from "./pointer"
|
|
15
|
+
import { LineNode } from "../../model"
|
|
12
16
|
|
|
13
17
|
function clientPoint(event: { clientX: number; clientY: number }) {
|
|
14
18
|
return { x: event.clientX, y: event.clientY }
|
|
@@ -25,7 +29,11 @@ export function useSelector(view: (props: null | Rect) => void) {
|
|
|
25
29
|
return usePointer({
|
|
26
30
|
onDown(event) {
|
|
27
31
|
for (const node of editor.nodes.values()) {
|
|
28
|
-
|
|
32
|
+
const cursor = cursorPosition(event, node.page)
|
|
33
|
+
if (
|
|
34
|
+
(node instanceof LineNode && lineContainsPoint(accessibleLine(node), cursor)) ||
|
|
35
|
+
(!(node instanceof LineNode) && boxContainsPoint(box(node), cursor))
|
|
36
|
+
) {
|
|
29
37
|
return false
|
|
30
38
|
}
|
|
31
39
|
}
|
|
@@ -55,7 +63,13 @@ export function useSelector(view: (props: null | Rect) => void) {
|
|
|
55
63
|
return page.nodes
|
|
56
64
|
.values()
|
|
57
65
|
.toArray()
|
|
58
|
-
.filter((node) =>
|
|
66
|
+
.filter((node) => {
|
|
67
|
+
if (node instanceof LineNode) {
|
|
68
|
+
return lineIntersectsBox(node, selection)
|
|
69
|
+
} else {
|
|
70
|
+
return boxIntersects(selection, box(node))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
59
73
|
})
|
|
60
74
|
|
|
61
75
|
editor.selection = new Set(selection)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { type Rect, box, boxBounds, rect } from "../../model/geometry/math"
|
|
2
2
|
import type { Node } from "../../model/node"
|
|
3
3
|
import type { Page } from "../../model/page"
|
|
4
|
-
import { useEditor
|
|
4
|
+
import { useEditor } from "../editor"
|
|
5
5
|
|
|
6
6
|
type Lines = Page["snapLines"]
|
|
7
7
|
const isN = (n?: number) => typeof n === "number"
|
|
8
8
|
|
|
9
|
-
export function useSnap() {
|
|
10
|
-
const page = usePage()
|
|
9
|
+
export function useSnap(page: Page) {
|
|
11
10
|
const editor = useEditor()
|
|
11
|
+
|
|
12
12
|
const threshold = editor.options.snapThreshold
|
|
13
13
|
|
|
14
14
|
function shouldSnap(point: number) {
|
package/lib/hooks/textMarks.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { isEqual } from "es-toolkit"
|
|
2
2
|
import { useCallback, useRef, useSyncExternalStore } from "react"
|
|
3
3
|
import { effect, useComputed } from "react-bolt"
|
|
4
|
-
import { TextNode } from "../model"
|
|
5
|
-
import { EditableNode } from "../model/node/editable"
|
|
6
|
-
import { FormattableNode } from "../model/node/formattable"
|
|
4
|
+
import { TextNode, EditableNode, FormattableNode } from "../model"
|
|
7
5
|
import { useBatchSet, useNodeFieldBatch } from "./batch"
|
|
8
6
|
|
|
9
7
|
function mergeField<T>(values: T[]): T | null {
|
package/lib/lib/googleFonts.ts
CHANGED
|
@@ -55,8 +55,6 @@ export type GoogleWebfont = {
|
|
|
55
55
|
category: FontCategory
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const variants: Variant[] = ["regular", "italic", "700", "700italic"]
|
|
59
|
-
|
|
60
58
|
// server only
|
|
61
59
|
export async function getGoogleFonts(
|
|
62
60
|
key: string,
|
|
@@ -81,9 +79,7 @@ export async function getGoogleFonts(
|
|
|
81
79
|
const res = await fetch(url)
|
|
82
80
|
const { items = [] } = (await res.json()) as { items?: GoogleWebfont[] }
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
const result = items.filter((font) => variants.some((v) => font.variants.includes(v)))
|
|
86
|
-
return sort ? result : result.sort()
|
|
82
|
+
return items
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
export function getLink(font: GoogleWebfont) {
|