@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.
Files changed (38) hide show
  1. package/lib/hooks/actions.ts +89 -67
  2. package/lib/hooks/batch.ts +9 -5
  3. package/lib/hooks/index.ts +7 -7
  4. package/lib/hooks/page.ts +2 -4
  5. package/lib/hooks/pointer/useMovePoint.ts +100 -0
  6. package/lib/hooks/pointer/{moveable.ts → useMoveable.ts} +30 -36
  7. package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +2 -2
  8. package/lib/hooks/pointer/useResize/index.ts +31 -0
  9. package/lib/hooks/pointer/useResize/multi.ts +161 -0
  10. package/lib/hooks/pointer/useResize/multiLineNode.ts +99 -0
  11. package/lib/hooks/pointer/useResize/multiRegularNode.ts +109 -0
  12. package/lib/hooks/pointer/useResize/multiTextNode.ts +108 -0
  13. package/lib/hooks/pointer/useResize/singleRegularNode.ts +91 -0
  14. package/lib/hooks/pointer/useResize/singleTextNode.ts +115 -0
  15. package/lib/hooks/pointer/useRotation.ts +102 -0
  16. package/lib/hooks/pointer/{selector.ts → useSelector.ts} +2 -1
  17. package/lib/hooks/pointer/{snap.ts → useSnap.ts} +3 -2
  18. package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
  19. package/lib/hooks/textMarks.ts +30 -19
  20. package/lib/model/editor.ts +18 -4
  21. package/lib/model/geometry/math.ts +8 -20
  22. package/lib/model/index.ts +9 -1
  23. package/lib/model/node/imageNode.ts +1 -1
  24. package/lib/model/node/lineNode.ts +41 -20
  25. package/lib/model/node/textNode.ts +5 -14
  26. package/lib/model/node.ts +18 -9
  27. package/lib/model/page.ts +3 -2
  28. package/lib/ui/node/EditableContent/index.tsx +6 -5
  29. package/lib/ui/node/ImageContent.tsx +0 -1
  30. package/lib/ui/node/LineContent.tsx +1 -3
  31. package/lib/ui/node/NodeView.tsx +2 -0
  32. package/lib/ui/node/ShapeContent/EllipseContent.tsx +0 -1
  33. package/lib/ui/node/ShapeContent/RectangleContent.tsx +0 -1
  34. package/lib/ui/selection.ts +6 -5
  35. package/package.json +1 -1
  36. package/lib/hooks/pointer/movePoint.ts +0 -75
  37. package/lib/hooks/pointer/resize.ts +0 -247
  38. 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
+ }