@lazlon-platform/html-editor 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/hooks/actions.ts +89 -67
- package/lib/hooks/batch.ts +9 -5
- package/lib/hooks/index.ts +7 -7
- package/lib/hooks/page.ts +2 -4
- package/lib/hooks/pointer/useMovePoint.ts +100 -0
- package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +30 -36
- package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +2 -2
- package/lib/hooks/pointer/useResize/index.ts +31 -0
- package/lib/hooks/pointer/useResize/multi.ts +161 -0
- package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
- package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
- package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
- package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
- package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
- package/lib/hooks/pointer/useRotation.ts +102 -0
- package/lib/hooks/pointer/{selector.ts → useSelector.ts} +2 -1
- package/lib/hooks/pointer/{snap.ts → useSnap.ts} +3 -2
- package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
- package/lib/hooks/textMarks.ts +30 -19
- package/lib/model/editor.ts +18 -4
- package/lib/model/geometry/math.ts +8 -20
- package/lib/model/index.ts +9 -1
- package/lib/model/node/imageNode.ts +1 -1
- package/lib/model/node/lineNode.ts +41 -20
- package/lib/model/node/textNode.ts +5 -14
- package/lib/model/node.ts +18 -9
- package/lib/model/page.ts +3 -2
- package/lib/ui/node/EditableContent/index.tsx +6 -5
- package/lib/ui/node/ImageContent.tsx +0 -1
- package/lib/ui/node/LineContent.tsx +1 -3
- package/lib/ui/node/NodeView.tsx +2 -0
- package/lib/ui/node/ShapeContent/EllipseContent.tsx +0 -1
- package/lib/ui/node/ShapeContent/RectangleContent.tsx +0 -1
- package/lib/ui/selection.ts +6 -5
- package/package.json +1 -1
- package/lib/hooks/pointer/movePoint.ts +0 -75
- package/lib/hooks/pointer/resize.ts +0 -247
- package/lib/hooks/pointer/rotation.ts +0 -138
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import { Node } from "../../../model"
|
|
4
|
+
import {
|
|
5
|
+
box,
|
|
6
|
+
boxRect,
|
|
7
|
+
deg,
|
|
8
|
+
floatNorm,
|
|
9
|
+
perpDistance,
|
|
10
|
+
point,
|
|
11
|
+
resizeBox,
|
|
12
|
+
type Box,
|
|
13
|
+
type Corner,
|
|
14
|
+
type Edge,
|
|
15
|
+
} from "../../../model/geometry/math"
|
|
16
|
+
import { useEditor } from "../../editor"
|
|
17
|
+
import { cursorPosition, usePointer } from "../usePointer"
|
|
18
|
+
|
|
19
|
+
export function setRegularNodeSize(node: Node, size: Box) {
|
|
20
|
+
const { x, y, width, height } = boxRect(size)
|
|
21
|
+
node.x = floatNorm(x)
|
|
22
|
+
node.y = floatNorm(y)
|
|
23
|
+
node.width = floatNorm(width)
|
|
24
|
+
node.height = floatNorm(height)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useSingleRegularNodeResize(direction: Edge | Corner) {
|
|
28
|
+
const editor = useEditor()
|
|
29
|
+
const edges = direction.split("") as Edge[]
|
|
30
|
+
const [node] = useStore(editor, "selection")
|
|
31
|
+
|
|
32
|
+
const state = useRef({
|
|
33
|
+
base: box(),
|
|
34
|
+
anchorPoint: point(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function redoProps() {
|
|
38
|
+
const { x, y, width, height } = node
|
|
39
|
+
return { x, y, width, height }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function undoProps() {
|
|
43
|
+
const { x, y, width, height } = boxRect(state.current.base)
|
|
44
|
+
return { x, y, width, height }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return usePointer({
|
|
48
|
+
onDown(event) {
|
|
49
|
+
if (!node || node.locked) return false
|
|
50
|
+
|
|
51
|
+
state.current = {
|
|
52
|
+
base: box(node),
|
|
53
|
+
anchorPoint: cursorPosition(event, node.page),
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
onMove(event) {
|
|
58
|
+
const { base, anchorPoint } = state.current
|
|
59
|
+
const cursor = cursorPosition(event, node.page)
|
|
60
|
+
|
|
61
|
+
setRegularNodeSize(
|
|
62
|
+
node,
|
|
63
|
+
edges.reduce((size, edge) => {
|
|
64
|
+
const r = edge === "w" ? 90 : edge === "e" ? -90 : edge === "n" ? 180 : 0
|
|
65
|
+
return resizeBox(
|
|
66
|
+
size,
|
|
67
|
+
edge,
|
|
68
|
+
perpDistance(anchorPoint, cursor, deg(node.rotation + r)),
|
|
69
|
+
)
|
|
70
|
+
}, base),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
editor.action = {
|
|
74
|
+
action: "resize",
|
|
75
|
+
payload: { width: node.width, height: node.height },
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
onCancel() {
|
|
80
|
+
editor.action = {}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
onEnd() {
|
|
84
|
+
editor.action = {}
|
|
85
|
+
editor.pushHistory({
|
|
86
|
+
redo: ["set-node-props", [node.id, redoProps()]],
|
|
87
|
+
undo: ["set-node-props", [node.id, undoProps()]],
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import { useComputed, useStore } from "react-bolt"
|
|
3
|
+
import { Node, TextNode } from "../../../model"
|
|
4
|
+
import {
|
|
5
|
+
box,
|
|
6
|
+
boxRect,
|
|
7
|
+
floatNorm,
|
|
8
|
+
point,
|
|
9
|
+
pointNorm,
|
|
10
|
+
pointSubtract,
|
|
11
|
+
rotatePoint,
|
|
12
|
+
scaleBox,
|
|
13
|
+
vectorDotProd,
|
|
14
|
+
type Box,
|
|
15
|
+
type Corner,
|
|
16
|
+
type Edge,
|
|
17
|
+
type Point,
|
|
18
|
+
} from "../../../model/geometry/math"
|
|
19
|
+
import { useEditor } from "../../editor"
|
|
20
|
+
import { cursorPosition, usePointer } from "../usePointer"
|
|
21
|
+
|
|
22
|
+
export function setTextNodeSize(
|
|
23
|
+
node: TextNode,
|
|
24
|
+
baseFontSize: number,
|
|
25
|
+
baseBoxSize: Box,
|
|
26
|
+
size: Box,
|
|
27
|
+
) {
|
|
28
|
+
const { height, width, x, y } = boxRect(size)
|
|
29
|
+
const targetHeight = Math.max(8, height)
|
|
30
|
+
node.size = floatNorm((baseFontSize * targetHeight) / baseBoxSize.height)
|
|
31
|
+
node.x = floatNorm(x)
|
|
32
|
+
node.y = floatNorm(y)
|
|
33
|
+
node.height = floatNorm(targetHeight)
|
|
34
|
+
node.width = floatNorm(width)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useSingleTextNodeResize(direction: Edge | Corner) {
|
|
38
|
+
const editor = useEditor()
|
|
39
|
+
const [node] = useStore(editor, "selection")
|
|
40
|
+
const isLocked = useComputed(() => node instanceof Node && node.locked)
|
|
41
|
+
const enable =
|
|
42
|
+
node instanceof TextNode && !isLocked && direction !== "w" && direction !== "e"
|
|
43
|
+
|
|
44
|
+
const state = useRef({
|
|
45
|
+
anchorPoint: point(),
|
|
46
|
+
baseFontSize: 0,
|
|
47
|
+
baseBoxSize: box(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function scaleDistance(cursor: Point) {
|
|
51
|
+
const { width, height, rotation } = state.current.baseBoxSize
|
|
52
|
+
const localDirection = pointNorm({
|
|
53
|
+
x: direction.includes("w") ? -width : direction.includes("e") ? width : 0,
|
|
54
|
+
y: direction.includes("n") ? -height : direction.includes("s") ? height : 0,
|
|
55
|
+
})
|
|
56
|
+
const distance = vectorDotProd(
|
|
57
|
+
pointSubtract(cursor, state.current.anchorPoint),
|
|
58
|
+
rotatePoint(localDirection, point(), rotation),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (direction.length === 1) return distance
|
|
62
|
+
const diagonal = Math.hypot(width, height)
|
|
63
|
+
return diagonal === 0 ? 0 : (distance * height) / diagonal
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function redoProps() {
|
|
67
|
+
const { x, y, width, height, size } = node as TextNode
|
|
68
|
+
return { x, y, width, height, size }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function undoProps() {
|
|
72
|
+
const { x, y, width, height } = boxRect(state.current.baseBoxSize)
|
|
73
|
+
const size = state.current.baseFontSize
|
|
74
|
+
return { x, y, width, height, size }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return usePointer({
|
|
78
|
+
onDown(event) {
|
|
79
|
+
if (enable) {
|
|
80
|
+
state.current = {
|
|
81
|
+
anchorPoint: cursorPosition(event, node.page),
|
|
82
|
+
baseFontSize: node.size,
|
|
83
|
+
baseBoxSize: box(node),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return enable
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
onMove(event) {
|
|
90
|
+
if (!enable) return
|
|
91
|
+
|
|
92
|
+
const { baseFontSize, baseBoxSize } = state.current
|
|
93
|
+
const cursor = cursorPosition(event, node.page)
|
|
94
|
+
const size = scaleBox(baseBoxSize, direction, scaleDistance(cursor))
|
|
95
|
+
setTextNodeSize(node, baseFontSize, baseBoxSize, size)
|
|
96
|
+
|
|
97
|
+
editor.action = {
|
|
98
|
+
action: "resize",
|
|
99
|
+
payload: { width: node.width, height: node.height },
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
onCancel() {
|
|
104
|
+
editor.action = {}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
onEnd() {
|
|
108
|
+
editor.action = {}
|
|
109
|
+
editor.pushHistory<TextNode>({
|
|
110
|
+
redo: ["set-node-props", [node.id, redoProps()]],
|
|
111
|
+
undo: ["set-node-props", [node.id, undoProps()]],
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
}
|