@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
|
@@ -1,44 +1,61 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
|
+
import { LineNode } from "../../model"
|
|
2
3
|
import {
|
|
4
|
+
accessibleLine,
|
|
3
5
|
box,
|
|
4
6
|
boxBounds,
|
|
5
7
|
boxContainsPoint,
|
|
8
|
+
lineContainsPoint,
|
|
6
9
|
type Point,
|
|
7
10
|
point,
|
|
8
11
|
pointSubtract,
|
|
9
12
|
rect,
|
|
10
13
|
} from "../../model/geometry/math"
|
|
11
|
-
import type { HistoryEntry } from "../../model/history"
|
|
12
14
|
import type { Node } from "../../model/node"
|
|
13
15
|
import { selectionBox } from "../../ui/selection"
|
|
14
16
|
import { useEditor, usePage } from "../editor"
|
|
15
|
-
import { cursorPosition, usePointer } from "./
|
|
16
|
-
import { useSnap } from "./
|
|
17
|
+
import { cursorPosition, usePointer } from "./usePointer"
|
|
18
|
+
import { useSnap } from "./useSnap"
|
|
17
19
|
|
|
18
20
|
export function useMoveable() {
|
|
19
21
|
const editor = useEditor()
|
|
20
22
|
const page = usePage()
|
|
21
|
-
const snap = useSnap()
|
|
23
|
+
const snap = useSnap(page)
|
|
22
24
|
|
|
23
25
|
const state = useRef({
|
|
24
|
-
|
|
26
|
+
baseRect: rect(),
|
|
25
27
|
cursorOffset: point(),
|
|
26
28
|
nodes: Array<{
|
|
27
29
|
node: Node
|
|
28
|
-
|
|
30
|
+
basePosition: Point
|
|
29
31
|
selectionOffset: Point
|
|
30
32
|
}>(),
|
|
31
33
|
})
|
|
32
34
|
|
|
35
|
+
function isCursorInSelection(cursor: Point) {
|
|
36
|
+
const [firstNode, ...rest] = editor.selection
|
|
37
|
+
if (rest.length === 0 && firstNode && firstNode instanceof LineNode) {
|
|
38
|
+
return lineContainsPoint(accessibleLine(firstNode), cursor)
|
|
39
|
+
}
|
|
40
|
+
return boxContainsPoint(selectionBox(editor.selection), cursor)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isNodeHit(node: Node, cursor: Point) {
|
|
44
|
+
if (node instanceof LineNode) {
|
|
45
|
+
return lineContainsPoint(accessibleLine(node), cursor)
|
|
46
|
+
}
|
|
47
|
+
return boxContainsPoint(box(node), cursor)
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
return usePointer({
|
|
34
51
|
onDown(event) {
|
|
35
52
|
const cursor = cursorPosition(event, page)
|
|
36
53
|
|
|
37
54
|
// clicked outside of selection, try grabbing nodes under the cursor
|
|
38
|
-
if (!
|
|
55
|
+
if (!isCursorInSelection(cursor)) {
|
|
39
56
|
const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
|
|
40
57
|
for (const node of stackOrderedNodes) {
|
|
41
|
-
if (
|
|
58
|
+
if (isNodeHit(node, cursor)) {
|
|
42
59
|
editor.selection = new Set([node])
|
|
43
60
|
break
|
|
44
61
|
}
|
|
@@ -46,29 +63,25 @@ export function useMoveable() {
|
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
if (editor.selection.size === 0) return false
|
|
49
|
-
|
|
50
|
-
const initialSelectionBox = selectionBox(editor.selection)
|
|
51
|
-
const initialSelectionRect = boxBounds(initialSelectionBox)
|
|
66
|
+
if (!isCursorInSelection(cursor)) return false
|
|
52
67
|
|
|
53
68
|
for (const node of editor.selection) {
|
|
54
|
-
if (node.
|
|
69
|
+
if (node.locked) return false
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} else {
|
|
71
|
-
return false
|
|
72
|
+
const baseRect = boxBounds(selectionBox(editor.selection))
|
|
73
|
+
|
|
74
|
+
state.current = {
|
|
75
|
+
baseRect,
|
|
76
|
+
cursorOffset: pointSubtract(cursor, baseRect),
|
|
77
|
+
nodes: editor.selection
|
|
78
|
+
.values()
|
|
79
|
+
.map((node) => ({
|
|
80
|
+
node,
|
|
81
|
+
basePosition: point(node),
|
|
82
|
+
selectionOffset: pointSubtract(point(node), baseRect),
|
|
83
|
+
}))
|
|
84
|
+
.toArray(),
|
|
72
85
|
}
|
|
73
86
|
},
|
|
74
87
|
|
|
@@ -81,8 +94,8 @@ export function useMoveable() {
|
|
|
81
94
|
rect({
|
|
82
95
|
x: target.x,
|
|
83
96
|
y: target.y,
|
|
84
|
-
width: state.current.
|
|
85
|
-
height: state.current.
|
|
97
|
+
width: state.current.baseRect.width,
|
|
98
|
+
height: state.current.baseRect.height,
|
|
86
99
|
}),
|
|
87
100
|
)
|
|
88
101
|
|
|
@@ -103,17 +116,12 @@ export function useMoveable() {
|
|
|
103
116
|
editor.action = {}
|
|
104
117
|
page.snapLines = []
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
({ node,
|
|
108
|
-
redo: ["set-node-props", [node.id,
|
|
109
|
-
undo: ["set-node-props", [node.id,
|
|
110
|
-
}),
|
|
119
|
+
editor.pushHistory(
|
|
120
|
+
state.current.nodes.map(({ node, basePosition }) => ({
|
|
121
|
+
redo: ["set-node-props", [node.id, point(node)]],
|
|
122
|
+
undo: ["set-node-props", [node.id, basePosition]],
|
|
123
|
+
})),
|
|
111
124
|
)
|
|
112
|
-
|
|
113
|
-
editor.history.push({
|
|
114
|
-
redo: ["batch", entries.map((e) => e.redo)],
|
|
115
|
-
undo: ["batch", entries.map((e) => e.undo)],
|
|
116
|
-
})
|
|
117
125
|
},
|
|
118
126
|
})
|
|
119
127
|
}
|
|
@@ -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,15 +11,14 @@ 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
|
|
|
20
19
|
return {
|
|
21
|
-
x: floatNorm(event.clientX
|
|
22
|
-
y: floatNorm(event.clientY
|
|
20
|
+
x: floatNorm((event.clientX - x) / zoom),
|
|
21
|
+
y: floatNorm((event.clientY - y) / zoom),
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TextNode } from "@lazlon/html-editor/model"
|
|
2
|
+
import { useComputed, useStore } from "react-bolt"
|
|
3
|
+
import type { Corner, Edge } from "../../../model/geometry/math"
|
|
4
|
+
import { useEditor } from "../../editor"
|
|
5
|
+
import { useMultiResize } from "./multi"
|
|
6
|
+
import { useSingleRegularNodeResize } from "./singleRegularNode"
|
|
7
|
+
import { useSingleTextNodeResize } from "./singleTextNode"
|
|
8
|
+
import type { usePointer } from "../usePointer"
|
|
9
|
+
|
|
10
|
+
export function useResize(direction: Edge | Corner): ReturnType<typeof usePointer> {
|
|
11
|
+
const editor = useEditor()
|
|
12
|
+
const selection = useStore(editor, "selection")
|
|
13
|
+
const [node, ...nodes] = selection
|
|
14
|
+
const singleText = useSingleTextNodeResize(direction)
|
|
15
|
+
const single = useSingleRegularNodeResize(direction)
|
|
16
|
+
const multi = useMultiResize(direction)
|
|
17
|
+
const isLocked = useComputed(() => selection.values().some((node) => node.locked))
|
|
18
|
+
|
|
19
|
+
if (isLocked) {
|
|
20
|
+
return {
|
|
21
|
+
onPointerDown() {},
|
|
22
|
+
isMoving: () => false,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return nodes.length === 0
|
|
27
|
+
? node instanceof TextNode && direction !== "w" && direction !== "e"
|
|
28
|
+
? singleText
|
|
29
|
+
: single
|
|
30
|
+
: multi
|
|
31
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import { LineNode, TextNode, type HistoryEntry, type Node } from "../../../model"
|
|
4
|
+
import {
|
|
5
|
+
boxRect,
|
|
6
|
+
point,
|
|
7
|
+
pointSubtract,
|
|
8
|
+
rect,
|
|
9
|
+
type Corner,
|
|
10
|
+
type Edge,
|
|
11
|
+
type Point,
|
|
12
|
+
type Rect,
|
|
13
|
+
} from "../../../model/geometry/math"
|
|
14
|
+
import { selectionBox } from "../../../ui/selection"
|
|
15
|
+
import { useEditor } from "../../editor"
|
|
16
|
+
import { cursorPosition, usePointer } from "../usePointer"
|
|
17
|
+
import { useMultiLineNodeResize } from "./multiLineNode"
|
|
18
|
+
import { useMultiRegularNodeResize } from "./multiRegularNode"
|
|
19
|
+
import { useMultiTextNodeResize } from "./multiTextNode"
|
|
20
|
+
|
|
21
|
+
interface MultiResizeHookDownContext {
|
|
22
|
+
event: React.PointerEvent
|
|
23
|
+
baseSelectionRect: Rect
|
|
24
|
+
baseCursorPosition: Point
|
|
25
|
+
relativeSize(node: Node): Rect
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MultiResizeHookMoveContext {
|
|
29
|
+
event: globalThis.PointerEvent
|
|
30
|
+
baseSelectionRect: Rect
|
|
31
|
+
baseCursorPosition: Point
|
|
32
|
+
selectionRect: Rect
|
|
33
|
+
cursorPosition: Point
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MultiResizeHookEndContext {
|
|
37
|
+
event: globalThis.PointerEvent
|
|
38
|
+
baseSelectionRect: Rect
|
|
39
|
+
baseCursorPosition: Point
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MultiResizeHook<N extends Node> {
|
|
43
|
+
onDown(ctx: MultiResizeHookDownContext): void
|
|
44
|
+
onMove(ctx: MultiResizeHookMoveContext): void
|
|
45
|
+
onEnd(ctx: MultiResizeHookEndContext): Array<HistoryEntry<N>>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useMultiResize(direction: Edge | Corner) {
|
|
49
|
+
const editor = useEditor()
|
|
50
|
+
const selection = useStore(editor, "selection").values().toArray()
|
|
51
|
+
|
|
52
|
+
const text = useMultiTextNodeResize(
|
|
53
|
+
direction,
|
|
54
|
+
selection.filter((node) => node instanceof TextNode),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const line = useMultiLineNodeResize(
|
|
58
|
+
direction,
|
|
59
|
+
selection.filter((node) => node instanceof LineNode),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const regular = useMultiRegularNodeResize(
|
|
63
|
+
direction,
|
|
64
|
+
selection.filter((node) => !(node instanceof TextNode || node instanceof LineNode)),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const state = useRef({
|
|
68
|
+
baseSelectionRect: rect(),
|
|
69
|
+
baseCursorPosition: point(),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return usePointer({
|
|
73
|
+
onDown(event) {
|
|
74
|
+
if (selection.length === 0 || !editor.selectionPage) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const baseSelectionRect = boxRect(selectionBox(editor.selection))
|
|
79
|
+
const baseCursorPosition = cursorPosition(event, editor.selectionPage!)
|
|
80
|
+
const { x, y, width, height } = baseSelectionRect
|
|
81
|
+
|
|
82
|
+
const ctx: MultiResizeHookDownContext = {
|
|
83
|
+
event,
|
|
84
|
+
baseSelectionRect,
|
|
85
|
+
baseCursorPosition,
|
|
86
|
+
relativeSize: (node) => {
|
|
87
|
+
return rect({
|
|
88
|
+
x: (node.x - x) / width,
|
|
89
|
+
y: (node.y - y) / height,
|
|
90
|
+
width: node.width / width,
|
|
91
|
+
height: node.height / height,
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
state.current = {
|
|
97
|
+
baseSelectionRect,
|
|
98
|
+
baseCursorPosition,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
text.onDown(ctx)
|
|
102
|
+
line.onDown(ctx)
|
|
103
|
+
regular.onDown(ctx)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
onMove(event) {
|
|
107
|
+
const r = { ...state.current.baseSelectionRect }
|
|
108
|
+
const p = pointSubtract(
|
|
109
|
+
cursorPosition(event, editor.selectionPage!),
|
|
110
|
+
state.current.baseCursorPosition,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
/* mutate rect */ {
|
|
114
|
+
if (direction.includes("n") && r.height - p.y > 0) {
|
|
115
|
+
r.height -= p.y
|
|
116
|
+
r.y += p.y
|
|
117
|
+
}
|
|
118
|
+
if (direction.includes("s")) {
|
|
119
|
+
r.height += p.y
|
|
120
|
+
}
|
|
121
|
+
if (direction.includes("w") && r.width - p.x > 0) {
|
|
122
|
+
r.width -= p.x
|
|
123
|
+
r.x += p.x
|
|
124
|
+
}
|
|
125
|
+
if (direction.includes("e")) {
|
|
126
|
+
r.width += p.x
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ctx: MultiResizeHookMoveContext = {
|
|
131
|
+
event,
|
|
132
|
+
baseSelectionRect: state.current.baseSelectionRect,
|
|
133
|
+
baseCursorPosition: state.current.baseCursorPosition,
|
|
134
|
+
selectionRect: r,
|
|
135
|
+
cursorPosition: p,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
text.onMove(ctx)
|
|
139
|
+
line.onMove(ctx)
|
|
140
|
+
regular.onMove(ctx)
|
|
141
|
+
|
|
142
|
+
editor.action = { action: "resize", payload: r }
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onCancel() {
|
|
146
|
+
editor.action = {}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
onEnd(event) {
|
|
150
|
+
editor.action = {}
|
|
151
|
+
|
|
152
|
+
const ctx: MultiResizeHookEndContext = {
|
|
153
|
+
event,
|
|
154
|
+
baseSelectionRect: state.current.baseSelectionRect,
|
|
155
|
+
baseCursorPosition: state.current.baseCursorPosition,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
editor.pushHistory([...text.onEnd(ctx), ...line.onEnd(ctx), ...regular.onEnd(ctx)])
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import type { LineNode } from "../../../model"
|
|
3
|
+
import {
|
|
4
|
+
deg,
|
|
5
|
+
floatNorm,
|
|
6
|
+
point,
|
|
7
|
+
pointSubtract,
|
|
8
|
+
rect,
|
|
9
|
+
rectCenter,
|
|
10
|
+
rotatePoint,
|
|
11
|
+
type Corner,
|
|
12
|
+
type Edge,
|
|
13
|
+
type Point,
|
|
14
|
+
type Rect,
|
|
15
|
+
} from "../../../model/geometry/math"
|
|
16
|
+
import type { MultiResizeHook } from "./multi"
|
|
17
|
+
|
|
18
|
+
type NodeState = {
|
|
19
|
+
node: LineNode
|
|
20
|
+
basePoints: Point[]
|
|
21
|
+
baseX: number
|
|
22
|
+
baseY: number
|
|
23
|
+
relativeVertices: Point[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function relativePoint(p: Point, selectionRect: Rect): Point {
|
|
27
|
+
return {
|
|
28
|
+
x: selectionRect.width === 0 ? 0 : (p.x - selectionRect.x) / selectionRect.width,
|
|
29
|
+
y: selectionRect.height === 0 ? 0 : (p.y - selectionRect.y) / selectionRect.height,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pagePoint(relative: Point, selectionRect: Rect): Point {
|
|
34
|
+
return {
|
|
35
|
+
x: selectionRect.x + relative.x * selectionRect.width,
|
|
36
|
+
y: selectionRect.y + relative.y * selectionRect.height,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unrotatedPagePoints(vertices: Point[], rotation: number): Point[] {
|
|
41
|
+
const origin = point()
|
|
42
|
+
const unrotatedBounds = rect(
|
|
43
|
+
...vertices.map((p) => rotatePoint(p, origin, deg(-rotation))),
|
|
44
|
+
)
|
|
45
|
+
const center = rotatePoint(rectCenter(unrotatedBounds), origin, deg(rotation))
|
|
46
|
+
return vertices.map((p) => rotatePoint(p, center, deg(-rotation)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setLinePoints(node: LineNode, pagePoints: Point[]) {
|
|
50
|
+
const bounds = point(rect(...pagePoints))
|
|
51
|
+
node.x = floatNorm(bounds.x)
|
|
52
|
+
node.y = floatNorm(bounds.y)
|
|
53
|
+
node.points = pagePoints.map((p) => pointSubtract(p, bounds))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useMultiLineNodeResize(
|
|
57
|
+
_direction: Edge | Corner,
|
|
58
|
+
nodes: LineNode[],
|
|
59
|
+
): MultiResizeHook<LineNode> {
|
|
60
|
+
const state = useRef(Array<NodeState>())
|
|
61
|
+
|
|
62
|
+
function redoProps(state: NodeState) {
|
|
63
|
+
const { x, y, points } = state.node
|
|
64
|
+
return { x, y, points }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function undoProps(state: NodeState) {
|
|
68
|
+
const { baseX: x, baseY: y, basePoints: points } = state
|
|
69
|
+
return { x, y, points }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
onDown({ baseSelectionRect }) {
|
|
74
|
+
state.current = nodes.map((node) => {
|
|
75
|
+
return {
|
|
76
|
+
node,
|
|
77
|
+
basePoints: node.points,
|
|
78
|
+
baseX: node.x,
|
|
79
|
+
baseY: node.y,
|
|
80
|
+
relativeVertices: node.vertices.map((p) => relativePoint(p, baseSelectionRect)),
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
onMove({ selectionRect }) {
|
|
86
|
+
for (const item of state.current) {
|
|
87
|
+
const vertices = item.relativeVertices.map((p) => pagePoint(p, selectionRect))
|
|
88
|
+
setLinePoints(item.node, unrotatedPagePoints(vertices, item.node.rotation))
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
onEnd() {
|
|
93
|
+
return state.current.map((state) => ({
|
|
94
|
+
redo: ["set-node-props", [state.node.id, redoProps(state)]],
|
|
95
|
+
undo: ["set-node-props", [state.node.id, undoProps(state)]],
|
|
96
|
+
}))
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import type { Node } from "../../../model"
|
|
3
|
+
import {
|
|
4
|
+
box,
|
|
5
|
+
boxRect,
|
|
6
|
+
rect,
|
|
7
|
+
rectCenter,
|
|
8
|
+
type Box,
|
|
9
|
+
type Corner,
|
|
10
|
+
type Edge,
|
|
11
|
+
type Point,
|
|
12
|
+
type Rect,
|
|
13
|
+
} from "../../../model/geometry/math"
|
|
14
|
+
import type { MultiResizeHook } from "./multi"
|
|
15
|
+
import { setRegularNodeSize } from "./singleRegularNode"
|
|
16
|
+
|
|
17
|
+
type NodeState = {
|
|
18
|
+
node: Node
|
|
19
|
+
baseSize: Box
|
|
20
|
+
relative: Rect
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resizeScale(enabled: boolean, base: number, target: number) {
|
|
24
|
+
return enabled && base !== 0 ? target / base : 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function selectionScale(
|
|
28
|
+
direction: Edge | Corner,
|
|
29
|
+
baseSelectionRect: Rect,
|
|
30
|
+
selectionRect: Rect,
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
x: resizeScale(
|
|
34
|
+
direction.includes("w") || direction.includes("e"),
|
|
35
|
+
baseSelectionRect.width,
|
|
36
|
+
selectionRect.width,
|
|
37
|
+
),
|
|
38
|
+
y: resizeScale(
|
|
39
|
+
direction.includes("n") || direction.includes("s"),
|
|
40
|
+
baseSelectionRect.height,
|
|
41
|
+
selectionRect.height,
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scaledRegularBox(baseSize: Box, target: Rect, scale: Point): Box {
|
|
47
|
+
const r = (baseSize.rotation * Math.PI) / 180
|
|
48
|
+
const cos = Math.cos(r)
|
|
49
|
+
const sin = Math.sin(r)
|
|
50
|
+
const center = rectCenter(target)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
center,
|
|
54
|
+
pivot: center,
|
|
55
|
+
rotation: baseSize.rotation,
|
|
56
|
+
width: baseSize.width * Math.hypot(scale.x * cos, scale.y * sin),
|
|
57
|
+
height: baseSize.height * Math.hypot(scale.x * sin, scale.y * cos),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useMultiRegularNodeResize(
|
|
62
|
+
direction: Edge | Corner,
|
|
63
|
+
nodes: Node[],
|
|
64
|
+
): MultiResizeHook<Node> {
|
|
65
|
+
const state = useRef(Array<NodeState>())
|
|
66
|
+
|
|
67
|
+
function redoProps(state: NodeState) {
|
|
68
|
+
const { x, y, width, height } = state.node
|
|
69
|
+
return { x, y, width, height }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function undoProps(state: NodeState) {
|
|
73
|
+
const { x, y, width, height } = boxRect(state.baseSize)
|
|
74
|
+
return { x, y, width, height }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
onDown({ relativeSize }) {
|
|
79
|
+
state.current = nodes.map((node) => ({
|
|
80
|
+
node,
|
|
81
|
+
baseSize: box(node),
|
|
82
|
+
relative: relativeSize(node),
|
|
83
|
+
}))
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
onMove({ baseSelectionRect, selectionRect }) {
|
|
87
|
+
const { x, y, width, height } = selectionRect
|
|
88
|
+
const scale = selectionScale(direction, baseSelectionRect, selectionRect)
|
|
89
|
+
|
|
90
|
+
for (const { node, relative, baseSize } of state.current) {
|
|
91
|
+
const size = rect({
|
|
92
|
+
x: relative.x * width + x,
|
|
93
|
+
y: relative.y * height + y,
|
|
94
|
+
width: width * relative.width,
|
|
95
|
+
height: height * relative.height,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
setRegularNodeSize(node, scaledRegularBox(baseSize, size, scale))
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
onEnd() {
|
|
103
|
+
return state.current.map((state) => ({
|
|
104
|
+
undo: ["set-node-props", [state.node.id, undoProps(state)]],
|
|
105
|
+
redo: ["set-node-props", [state.node.id, redoProps(state)]],
|
|
106
|
+
}))
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import { TextNode } from "../../../model"
|
|
3
|
+
import {
|
|
4
|
+
box,
|
|
5
|
+
boxRect,
|
|
6
|
+
rect,
|
|
7
|
+
scaleBox,
|
|
8
|
+
type Box,
|
|
9
|
+
type Corner,
|
|
10
|
+
type Edge,
|
|
11
|
+
type Rect,
|
|
12
|
+
} from "../../../model/geometry/math"
|
|
13
|
+
import type { MultiResizeHook } from "./multi"
|
|
14
|
+
import { setTextNodeSize } from "./singleTextNode"
|
|
15
|
+
import { setRegularNodeSize } from "./singleRegularNode"
|
|
16
|
+
|
|
17
|
+
type NodeState = {
|
|
18
|
+
node: TextNode
|
|
19
|
+
baseFontSize: number
|
|
20
|
+
baseBoxSize: Box
|
|
21
|
+
relative: Rect
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function textScale(base: Box, target: Rect, direction: Edge | Corner) {
|
|
25
|
+
const horizontal = direction.includes("w") || direction.includes("e")
|
|
26
|
+
const vertical = direction.includes("n") || direction.includes("s")
|
|
27
|
+
const widthScale = base.width === 0 ? 1 : target.width / base.width
|
|
28
|
+
const heightScale = base.height === 0 ? 1 : target.height / base.height
|
|
29
|
+
|
|
30
|
+
if (horizontal && vertical) return Math.min(widthScale, heightScale)
|
|
31
|
+
if (horizontal) return widthScale
|
|
32
|
+
return heightScale
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scaledTextBox(base: Box, target: Rect, direction: Edge | Corner) {
|
|
36
|
+
const scale = textScale(base, target, direction)
|
|
37
|
+
const by =
|
|
38
|
+
direction.includes("n") || direction.includes("s")
|
|
39
|
+
? base.height * scale - base.height
|
|
40
|
+
: base.width * scale - base.width
|
|
41
|
+
|
|
42
|
+
return scaleBox(base, direction, by)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useMultiTextNodeResize(
|
|
46
|
+
direction: Edge | Corner,
|
|
47
|
+
nodes: TextNode[],
|
|
48
|
+
): MultiResizeHook<TextNode> {
|
|
49
|
+
const state = useRef(Array<NodeState>())
|
|
50
|
+
|
|
51
|
+
function redoProps(state: NodeState) {
|
|
52
|
+
const { x, y, width, height, size } = state.node
|
|
53
|
+
return { x, y, width, height, size }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function undoProps(state: NodeState) {
|
|
57
|
+
const { baseBoxSize, baseFontSize: size } = state
|
|
58
|
+
const { x, y, width, height } = boxRect(baseBoxSize)
|
|
59
|
+
return { x, y, width, height, size }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
onDown({ relativeSize }) {
|
|
64
|
+
state.current = nodes.map((node) => ({
|
|
65
|
+
node,
|
|
66
|
+
baseFontSize: node.size,
|
|
67
|
+
baseBoxSize: box(node),
|
|
68
|
+
relative: relativeSize(node),
|
|
69
|
+
}))
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
onMove({ selectionRect }) {
|
|
73
|
+
const { width, height, x, y } = selectionRect
|
|
74
|
+
for (const { node, relative, baseBoxSize, baseFontSize } of state.current) {
|
|
75
|
+
const size = rect({
|
|
76
|
+
x: relative.x * width + x,
|
|
77
|
+
y: relative.y * height + y,
|
|
78
|
+
width: width * relative.width,
|
|
79
|
+
height: height * relative.height,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (direction === "w" || direction === "e") {
|
|
83
|
+
setRegularNodeSize(node, box(size))
|
|
84
|
+
} else {
|
|
85
|
+
const scaled = scaledTextBox(baseBoxSize, size, direction)
|
|
86
|
+
setTextNodeSize(
|
|
87
|
+
node,
|
|
88
|
+
baseFontSize,
|
|
89
|
+
baseBoxSize,
|
|
90
|
+
box({
|
|
91
|
+
x: size.x + size.width / 2 - scaled.width / 2,
|
|
92
|
+
y: size.y + size.height / 2 - scaled.height / 2,
|
|
93
|
+
width: scaled.width,
|
|
94
|
+
height: scaled.height,
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
onEnd() {
|
|
102
|
+
return state.current.map((state) => ({
|
|
103
|
+
redo: ["set-node-props", [state.node.id, redoProps(state)]],
|
|
104
|
+
undo: ["set-node-props", [state.node.id, undoProps(state)]],
|
|
105
|
+
}))
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|