@lazlon-platform/html-editor 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/hooks/actions.ts +136 -87
- package/lib/hooks/batch.ts +24 -10
- package/lib/hooks/index.ts +7 -6
- package/lib/hooks/page.ts +2 -4
- package/lib/hooks/pointer/useMovePoint.ts +100 -0
- package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +47 -39
- package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
- 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} +18 -3
- package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
- package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
- package/lib/hooks/textMarks.ts +30 -21
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +31 -13
- package/lib/model/geometry/math.ts +128 -1
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +15 -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} +6 -12
- package/lib/model/node/lineNode.ts +80 -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} +9 -24
- package/lib/model/node.ts +27 -32
- package/lib/model/page.ts +4 -4
- 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} +10 -7
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +30 -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/lib/ui/selection.ts +6 -5
- package/package.json +1 -1
- package/lib/hooks/pointer/resize.ts +0 -247
- package/lib/hooks/pointer/rotation.ts +0 -103
- 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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { Point } from "../../../model/geometry/math"
|
|
4
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
5
|
+
import type { ShapeNode, StarShape } from "../../../model/node/shapeNode"
|
|
6
|
+
|
|
7
|
+
function starPoints(props: {
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
corners: number
|
|
11
|
+
depth: number
|
|
12
|
+
}): Point[] {
|
|
13
|
+
const { width, height, corners, depth } = props
|
|
14
|
+
const cx = width / 2
|
|
15
|
+
const cy = height / 2
|
|
16
|
+
const outerRx = width / 2
|
|
17
|
+
const outerRy = height / 2
|
|
18
|
+
const innerRx = outerRx * depth
|
|
19
|
+
const innerRy = outerRy * depth
|
|
20
|
+
|
|
21
|
+
return Array.from({ length: corners * 2 }, (_, i) => {
|
|
22
|
+
const angle = (i * Math.PI) / corners - Math.PI / 2
|
|
23
|
+
const isOuter = i % 2 === 0
|
|
24
|
+
const rx = isOuter ? outerRx : innerRx
|
|
25
|
+
const ry = isOuter ? outerRy : innerRy
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
x: cx + rx * Math.cos(angle),
|
|
29
|
+
y: cy + ry * Math.sin(angle),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function StarContent(props: { node: ShapeNode; shape: StarShape["props"] }) {
|
|
35
|
+
const maskId = useId()
|
|
36
|
+
const { node, shape } = props
|
|
37
|
+
|
|
38
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
39
|
+
node,
|
|
40
|
+
"background",
|
|
41
|
+
"borderWidth",
|
|
42
|
+
"borderColor",
|
|
43
|
+
)
|
|
44
|
+
const [width, height] = useStore(node, "width", "height")
|
|
45
|
+
const { corners, depth, roundness } = shape
|
|
46
|
+
|
|
47
|
+
const d = roundedPathData(
|
|
48
|
+
starPoints({
|
|
49
|
+
width,
|
|
50
|
+
height,
|
|
51
|
+
corners,
|
|
52
|
+
depth,
|
|
53
|
+
}),
|
|
54
|
+
roundness,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
59
|
+
<defs>
|
|
60
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
61
|
+
<path d={d} fill="white" />
|
|
62
|
+
</mask>
|
|
63
|
+
</defs>
|
|
64
|
+
|
|
65
|
+
<path d={d} fill={background} />
|
|
66
|
+
<path
|
|
67
|
+
d={d}
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke={borderColor}
|
|
70
|
+
strokeWidth={borderWidth}
|
|
71
|
+
mask={`url(#${maskId})`}
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import clsx from "clsx"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { ShapeNode } from "../../../model"
|
|
4
|
+
import { EditableContent } from "../EditableContent"
|
|
5
|
+
import { ArrowContent } from "./ArrowContent"
|
|
6
|
+
import { EllipseContent } from "./EllipseContent"
|
|
7
|
+
import { PolygonContent } from "./PolygonContent"
|
|
8
|
+
import { RectangleShape } from "./RectangleContent"
|
|
9
|
+
import { StarContent } from "./StarContent"
|
|
10
|
+
|
|
11
|
+
export function ShapeContent(props: { node: ShapeNode; isStatic?: boolean }) {
|
|
12
|
+
const { node, isStatic } = props
|
|
13
|
+
const [valign, halign, shape] = useStore(node, "valign", "halign", "shape")
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="relative size-full">
|
|
17
|
+
{shape.name === "rectangle" && <RectangleShape node={node} shape={shape.props} />}
|
|
18
|
+
{shape.name === "polygon" && <PolygonContent node={node} shape={shape.props} />}
|
|
19
|
+
{shape.name === "star" && <StarContent node={node} shape={shape.props} />}
|
|
20
|
+
{shape.name === "ellipse" && <EllipseContent node={node} />}
|
|
21
|
+
{shape.name === "arrow" && <ArrowContent node={node} shape={shape.props} />}
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
className={clsx(
|
|
25
|
+
"flex size-full",
|
|
26
|
+
valign === "top" && "items-start",
|
|
27
|
+
valign === "center" && "items-center",
|
|
28
|
+
valign === "bottom" && "items-end",
|
|
29
|
+
halign === "left" && "justify-start text-left",
|
|
30
|
+
halign === "center" && "justify-center text-center",
|
|
31
|
+
halign === "right" && "justify-end text-right",
|
|
32
|
+
halign === "justify" && "w-full text-justify",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<EditableContent
|
|
36
|
+
isStatic={isStatic}
|
|
37
|
+
node={node}
|
|
38
|
+
className={clsx(halign === "justify" && "w-full")}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -2,7 +2,7 @@ import clsx from "clsx"
|
|
|
2
2
|
import { useEffect } from "react"
|
|
3
3
|
import { useStore } from "react-bolt"
|
|
4
4
|
import { useEditor } from "../../hooks/editor"
|
|
5
|
-
import type { TextNode } from "../../model
|
|
5
|
+
import type { TextNode } from "../../model"
|
|
6
6
|
import { EditableContent } from "./EditableContent"
|
|
7
7
|
|
|
8
8
|
export function TextContent(props: {
|
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,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),
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
onMove(event) {
|
|
84
|
-
const cursor = cursorPosition(event, node)
|
|
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,103 +0,0 @@
|
|
|
1
|
-
import { useRef } from "react"
|
|
2
|
-
import { angle, deg, point, rectCenter, type Deg } from "../../model/geometry/math"
|
|
3
|
-
import type { HistoryAction } from "../../model/history"
|
|
4
|
-
import type { Node } from "../../model/node"
|
|
5
|
-
import { selectionDOMRect } from "../../ui/selection"
|
|
6
|
-
import { useEditor } from "../editor"
|
|
7
|
-
import { usePointer } from "./pointer"
|
|
8
|
-
|
|
9
|
-
function snappedDeg(startDeg: number, delta: number, shiftKey: boolean) {
|
|
10
|
-
const rotation = startDeg + delta
|
|
11
|
-
return deg(shiftKey ? Math.round(rotation / 45) * 45 : rotation)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function useRotation() {
|
|
15
|
-
const editor = useEditor()
|
|
16
|
-
|
|
17
|
-
const state = useRef({
|
|
18
|
-
centerPoint: point(),
|
|
19
|
-
inititalDeg: deg(0),
|
|
20
|
-
nodes: Array<{ node: Node; deg: Deg }>(),
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
return usePointer({
|
|
24
|
-
onDown(event) {
|
|
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 })),
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const deg = nodes.length === 1 ? nodes[0].rotation : 0
|
|
39
|
-
editor.action = { action: "rotate", payload: { deg } }
|
|
40
|
-
},
|
|
41
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
for (const n of nodes) {
|
|
49
|
-
n.node.rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const display =
|
|
53
|
-
nodes.length === 1
|
|
54
|
-
? nodes[0].node.rotation
|
|
55
|
-
: snappedDeg(inititalDeg, deg, event.shiftKey)
|
|
56
|
-
|
|
57
|
-
editor.action = {
|
|
58
|
-
action: "rotate",
|
|
59
|
-
payload: {
|
|
60
|
-
deg: display,
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
onCancel() {
|
|
66
|
-
editor.action = {}
|
|
67
|
-
},
|
|
68
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
if (nodes.length === 0) {
|
|
76
|
-
const [n] = nodes
|
|
77
|
-
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
78
|
-
editor.history.push({
|
|
79
|
-
redo: ["set-node-props", [n.node.id, { rotation }]],
|
|
80
|
-
undo: ["set-node-props", [n.node.id, { rotation: n.deg }]],
|
|
81
|
-
})
|
|
82
|
-
n.node.rotation = rotation
|
|
83
|
-
} else {
|
|
84
|
-
const redo: HistoryAction[] = []
|
|
85
|
-
const undo: HistoryAction[] = []
|
|
86
|
-
|
|
87
|
-
for (const n of nodes) {
|
|
88
|
-
const rotation = snappedDeg(n.deg, deg, event.shiftKey)
|
|
89
|
-
redo.push(["set-node-props", [n.node.id, { rotation }]])
|
|
90
|
-
undo.push(["set-node-props", [n.node.id, { rotation: n.deg }]])
|
|
91
|
-
n.node.rotation = rotation
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
editor.history.push({
|
|
95
|
-
redo: ["batch", redo],
|
|
96
|
-
undo: ["batch", undo],
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
editor.action = {}
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../../editor"
|
|
3
|
-
import type { SerializedNode } from "../../node"
|
|
4
|
-
import type { Page } from "../../page"
|
|
5
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
6
|
-
import { roundedPathData } from "../../geometry/svg"
|
|
7
|
-
|
|
8
|
-
export type ArrowNodeProps = ShapeNodeProps & Partial<Pick<ArrowNode, "roundness">>
|
|
9
|
-
|
|
10
|
-
export class ArrowNode extends ShapeNode {
|
|
11
|
-
get name() {
|
|
12
|
-
return "arrow"
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@state accessor roundness: number
|
|
16
|
-
|
|
17
|
-
constructor(editor: Editor, page: Page, { roundness = 0, ...props }: ArrowNodeProps) {
|
|
18
|
-
super(editor, page, props)
|
|
19
|
-
this.roundness = roundness
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
props(): ArrowNodeProps {
|
|
23
|
-
return {
|
|
24
|
-
...super.props(),
|
|
25
|
-
roundness: this.roundness,
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
serialize(): SerializedNode<this["name"], ArrowNodeProps> {
|
|
30
|
-
return super.serialize()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@computed get svgPathData() {
|
|
34
|
-
const { width, height } = this
|
|
35
|
-
const bodyRight = width * 0.68
|
|
36
|
-
const inset = height * 0.28
|
|
37
|
-
|
|
38
|
-
const arrowPoints = [
|
|
39
|
-
{ x: 0, y: inset },
|
|
40
|
-
{ x: bodyRight, y: inset },
|
|
41
|
-
{ x: bodyRight, y: 0 },
|
|
42
|
-
{ x: width, y: height / 2 },
|
|
43
|
-
{ x: bodyRight, y: height },
|
|
44
|
-
{ x: bodyRight, y: height - inset },
|
|
45
|
-
{ x: 0, y: height - inset },
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
return roundedPathData(arrowPoints, this.roundness)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Editor } from "../../editor"
|
|
2
|
-
import type { SerializedNode } from "../../node"
|
|
3
|
-
import type { Page } from "../../page"
|
|
4
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
5
|
-
|
|
6
|
-
export type EllipseNodeProps = ShapeNodeProps
|
|
7
|
-
|
|
8
|
-
export class EllipseNode extends ShapeNode {
|
|
9
|
-
get name() {
|
|
10
|
-
return "ellipse"
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
constructor(editor: Editor, page: Page, props: EllipseNodeProps) {
|
|
14
|
-
super(editor, page, props)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
props(): EllipseNodeProps {
|
|
18
|
-
return {
|
|
19
|
-
...super.props(),
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
serialize(): SerializedNode<this["name"], EllipseNodeProps> {
|
|
24
|
-
return super.serialize()
|
|
25
|
-
}
|
|
26
|
-
}
|