@lazlon-platform/html-editor 0.4.0 → 0.5.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.
@@ -5,9 +5,10 @@ import type { Node, NodeProps } from "../model/node"
5
5
  import { GroupNode } from "../model/node/group"
6
6
  import type { Page } from "../model/page"
7
7
  import { flattenNodes } from "../model/traversal"
8
- import { getTargetRect } from "../ui/selection"
8
+ import { selectionBox } from "../ui/selection"
9
9
  import { useBatchSet } from "./batch"
10
10
  import { useEditor } from "./editor"
11
+ import { boxBounds } from "../model/geometry/math"
11
12
 
12
13
  export function clone(
13
14
  editor: Editor,
@@ -95,7 +96,7 @@ export function useGroupAction() {
95
96
 
96
97
  function group() {
97
98
  const { selection, selectionPage: page } = editor
98
- const { width, height, x, y } = getTargetRect(selection)
99
+ const { width, height, x, y } = boxBounds(selectionBox(selection))
99
100
  if (selection.size === 0 || !page) return
100
101
 
101
102
  const group = new GroupNode(editor, page, {
@@ -296,7 +297,7 @@ export function useDistributeAction() {
296
297
  const editor = useEditor()
297
298
 
298
299
  function distribute(pos: "x" | "y", size: "width" | "height") {
299
- const rect = getTargetRect(editor.selection)
300
+ const rect = boxBounds(selectionBox(editor.selection))
300
301
  const array = editor.selection.values().toArray()
301
302
 
302
303
  const undo: HistoryAction[] = array.map((node) => [
package/lib/hooks/node.ts CHANGED
@@ -1,31 +1,39 @@
1
1
  import type { Node } from "../model"
2
- import { rotatedAABB } from "../model/geometry"
2
+ import { box, boxBounds, deg } from "../model/geometry/math"
3
3
  import { useNodeField, useNodeFieldBatch } from "./batch"
4
4
 
5
5
  export function useVisualPositionBatch(nodes: Node[]) {
6
- const rotation = useNodeField(nodes, "rotation", 0)
6
+ const rotation = useNodeField(nodes, "rotation", deg(0))
7
7
  const width = useNodeField(nodes, "width", NaN)
8
8
  const height = useNodeField(nodes, "height", NaN)
9
9
  const x = useNodeFieldBatch(nodes, "x", NaN)
10
10
  const y = useNodeFieldBatch(nodes, "y", NaN)
11
11
 
12
- const box = rotatedAABB({ x: x.value, y: y.value, width, height }, rotation)
12
+ const bounds = boxBounds(
13
+ box({
14
+ x: x.value,
15
+ y: y.value,
16
+ width,
17
+ height,
18
+ rotation,
19
+ }),
20
+ )
13
21
 
14
22
  function withBoundingBox(axis: "x" | "y", fn: (value: number) => void) {
15
23
  return function (value: number): void {
16
- const delta = value - box[axis]
24
+ const delta = value - bounds[axis]
17
25
  return fn(axis === "x" ? x.value + delta : y.value + delta)
18
26
  }
19
27
  }
20
28
 
21
29
  return {
22
30
  x: {
23
- value: box.x,
31
+ value: bounds.x,
24
32
  onChange: withBoundingBox("x", x.onChange),
25
33
  onChangeEnd: withBoundingBox("x", x.onChangeEnd),
26
34
  },
27
35
  y: {
28
- value: box.y,
36
+ value: bounds.y,
29
37
  onChange: withBoundingBox("y", y.onChange),
30
38
  onChangeEnd: withBoundingBox("y", y.onChangeEnd),
31
39
  },
@@ -1,73 +1,94 @@
1
1
  import { useRef } from "react"
2
- import type { Point } from "../../model/geometry"
2
+ import {
3
+ box,
4
+ boxBounds,
5
+ boxContainsPoint,
6
+ type Point,
7
+ point,
8
+ pointSubtract,
9
+ rect,
10
+ } from "../../model/geometry/math"
3
11
  import type { HistoryEntry } from "../../model/history"
4
12
  import type { Node } from "../../model/node"
5
- import { getTargetRect, isPointerInSelectionRect } from "../../ui/selection"
13
+ import { selectionBox } from "../../ui/selection"
6
14
  import { useEditor, usePage } from "../editor"
7
- import { usePointer } from "./pointer"
15
+ import { cursorPosition, usePointer } from "./pointer"
8
16
  import { useSnap } from "./snap"
9
17
 
10
- type StartPoints = {
11
- node: Node
12
- start: Point
13
- offset: Point
14
- }
15
-
16
- const moveInit = {
17
- start: { x: 0, y: 0, width: 0, height: 0 },
18
- grab: { x: 0, y: 0 },
19
- }
20
-
21
18
  export function useMoveable() {
22
19
  const editor = useEditor()
23
20
  const page = usePage()
24
- const startpoints = useRef(Array<StartPoints>())
25
- const move = useRef(moveInit)
26
21
  const snap = useSnap()
27
22
 
23
+ const state = useRef({
24
+ initialSelectionRect: rect(),
25
+ cursorOffset: point(),
26
+ nodes: Array<{
27
+ node: Node
28
+ startingPoint: Point
29
+ selectionOffset: Point
30
+ }>(),
31
+ })
32
+
28
33
  return usePointer({
29
34
  onDown(event) {
30
- const { selection } = editor
31
- const rect = getTargetRect(selection)
32
-
33
- startpoints.current = selection
34
- .values()
35
- .map((n) => ({
36
- node: n,
37
- start: { x: n.x, y: n.y },
38
- offset: { x: n.x - rect.x, y: n.y - rect.y },
39
- }))
40
- .toArray()
41
-
42
- move.current = {
43
- start: rect,
44
- grab: {
45
- x: Math.round(event.clientX / editor.zoom - rect.x),
46
- y: Math.round(event.clientY / editor.zoom - rect.y),
47
- },
35
+ const cursor = cursorPosition(event, page)
36
+
37
+ // clicked outside of selection, try grabbing nodes under the cursor
38
+ if (!boxContainsPoint(selectionBox(editor.selection), cursor)) {
39
+ const stackOrderedNodes = editor.nodes.values().toArray().toReversed()
40
+ for (const node of stackOrderedNodes) {
41
+ if (boxContainsPoint(box(node), cursor)) {
42
+ editor.selection = new Set([node])
43
+ break
44
+ }
45
+ }
48
46
  }
49
47
 
50
- for (const node of selection) {
48
+ if (editor.selection.size === 0) return false
49
+
50
+ const initialSelectionBox = selectionBox(editor.selection)
51
+ const initialSelectionRect = boxBounds(initialSelectionBox)
52
+
53
+ for (const node of editor.selection) {
51
54
  if (node.blockMove(event)) return false
52
55
  }
53
56
 
54
- return isPointerInSelectionRect(selection, event)
57
+ if (boxContainsPoint(initialSelectionBox, cursor)) {
58
+ state.current = {
59
+ initialSelectionRect,
60
+ cursorOffset: pointSubtract(cursor, initialSelectionRect),
61
+ nodes: editor.selection
62
+ .values()
63
+ .map((node) => ({
64
+ node,
65
+ startingPoint: point(node),
66
+ selectionOffset: pointSubtract(point(node), initialSelectionRect),
67
+ }))
68
+ .toArray(),
69
+ }
70
+ } else {
71
+ return false
72
+ }
55
73
  },
56
74
 
57
- onMove({ event }) {
58
- const px = Math.round(event.clientX / editor.zoom - move.current.grab.x)
59
- const py = Math.round(event.clientY / editor.zoom - move.current.grab.y)
60
-
61
- const { x, y } = snap(!event.shiftKey, {
62
- x: px,
63
- y: py,
64
- width: move.current.start.width,
65
- height: move.current.start.height,
66
- })
67
-
68
- for (const { node, offset } of startpoints.current) {
69
- node.x = x + offset.x
70
- node.y = y + offset.y
75
+ onMove(event) {
76
+ const cursor = cursorPosition(event, page)
77
+ const target = pointSubtract(cursor, state.current.cursorOffset)
78
+
79
+ const { x, y } = snap(
80
+ !event.shiftKey,
81
+ rect({
82
+ x: target.x,
83
+ y: target.y,
84
+ width: state.current.initialSelectionRect.width,
85
+ height: state.current.initialSelectionRect.height,
86
+ }),
87
+ )
88
+
89
+ for (const { node, selectionOffset } of state.current.nodes) {
90
+ node.x = x + selectionOffset.x
91
+ node.y = y + selectionOffset.y
71
92
  }
72
93
 
73
94
  editor.action = { action: "move", payload: { x, y } }
@@ -82,12 +103,12 @@ export function useMoveable() {
82
103
  editor.action = {}
83
104
  page.snapLines = []
84
105
 
85
- const entries = startpoints.current.map(({ node, start }): HistoryEntry => {
86
- return {
106
+ const entries: HistoryEntry[] = state.current.nodes.map(
107
+ ({ node, startingPoint: start }) => ({
87
108
  redo: ["set-node-props", [node.id, { x: node.x, y: node.y }]],
88
109
  undo: ["set-node-props", [node.id, { x: start.x, y: start.y }]],
89
- }
90
- })
110
+ }),
111
+ )
91
112
 
92
113
  editor.history.push({
93
114
  redo: ["batch", entries.map((e) => e.redo)],
@@ -1,15 +1,26 @@
1
+ import { Page, type Node } from "@lazlon/html-editor/model"
1
2
  import { useCallback, useRef } from "react"
2
-
3
- type MoveEvent = {
4
- start: React.PointerEvent
5
- event: globalThis.PointerEvent
6
- }
3
+ import { floatNorm, type Point } from "../../model/geometry/math"
7
4
 
8
5
  export type UsePointerProps = {
9
6
  onDown?(event: React.PointerEvent): boolean | void
10
- onMove?(event: MoveEvent): void
11
- onEnd?(event: MoveEvent): void
12
- onCancel?(event: MoveEvent): void
7
+ onMove?(event: globalThis.PointerEvent): void
8
+ onEnd?(event: globalThis.PointerEvent): void
9
+ onCancel?(event: globalThis.PointerEvent): void
10
+ }
11
+
12
+ export function cursorPosition(
13
+ event: { clientX: number; clientY: number },
14
+ relativeTo: Page | Node,
15
+ ): Point {
16
+ const page = relativeTo instanceof Page ? relativeTo : relativeTo.page
17
+ const { zoom } = page.editor
18
+ const { x, y } = page.ref!.getBoundingClientRect()
19
+
20
+ return {
21
+ x: floatNorm(event.clientX / zoom - x),
22
+ y: floatNorm(event.clientY / zoom - y),
23
+ }
13
24
  }
14
25
 
15
26
  export function usePointer(props: UsePointerProps) {
@@ -23,7 +34,7 @@ export function usePointer(props: UsePointerProps) {
23
34
 
24
35
  function onPointerMove(event: globalThis.PointerEvent) {
25
36
  isMovingRef.current = true
26
- onMove?.({ start, event })
37
+ onMove?.(event)
27
38
  }
28
39
 
29
40
  function onPointerUp(event: globalThis.PointerEvent) {
@@ -32,9 +43,9 @@ export function usePointer(props: UsePointerProps) {
32
43
  removeEventListener("pointercancel", onPointerUp)
33
44
 
34
45
  if (isMovingRef.current) {
35
- onEnd?.({ start, event })
46
+ onEnd?.(event)
36
47
  } else {
37
- onCancel?.({ start, event })
48
+ onCancel?.(event)
38
49
  }
39
50
 
40
51
  isMovingRef.current = false