@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
|
@@ -20,6 +20,11 @@ export interface Rect extends Point, Size {
|
|
|
20
20
|
readonly right: number
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface Line {
|
|
24
|
+
readonly vertices: Point[]
|
|
25
|
+
readonly strokeWidth: number
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* A rotated rectangle around a pivot point.
|
|
25
30
|
* Pivot point is relative to the center point.
|
|
@@ -349,6 +354,128 @@ export function boxContainsPoint(target: Box, point: Point): boolean {
|
|
|
349
354
|
)
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
function pointSegmentDistance(target: Point, a: Point, b: Point): number {
|
|
358
|
+
const segment = pointSubtract(b, a)
|
|
359
|
+
const lengthSq = vectorDotProd(segment, segment)
|
|
360
|
+
|
|
361
|
+
if (lengthSq === 0) {
|
|
362
|
+
return pointDist(target, a)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const fromStart = pointSubtract(target, a)
|
|
366
|
+
const projection = clamp(vectorDotProd(fromStart, segment) / lengthSq, 0, 1)
|
|
367
|
+
const closest = pointAdd(a, pointMultiply(segment, projection))
|
|
368
|
+
|
|
369
|
+
return pointDist(target, closest)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function segmentCross(a: Point, b: Point, c: Point): number {
|
|
373
|
+
return floatNorm((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function pointOnSegment(target: Point, a: Point, b: Point): boolean {
|
|
377
|
+
const x = floatNorm(target.x)
|
|
378
|
+
const y = floatNorm(target.y)
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
segmentCross(a, b, target) === 0 &&
|
|
382
|
+
x >= floatNorm(Math.min(a.x, b.x)) &&
|
|
383
|
+
x <= floatNorm(Math.max(a.x, b.x)) &&
|
|
384
|
+
y >= floatNorm(Math.min(a.y, b.y)) &&
|
|
385
|
+
y <= floatNorm(Math.max(a.y, b.y))
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
390
|
+
const abc = segmentCross(a, b, c)
|
|
391
|
+
const abd = segmentCross(a, b, d)
|
|
392
|
+
const cda = segmentCross(c, d, a)
|
|
393
|
+
const cdb = segmentCross(c, d, b)
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
(abc === 0 && pointOnSegment(c, a, b)) ||
|
|
397
|
+
(abd === 0 && pointOnSegment(d, a, b)) ||
|
|
398
|
+
(cda === 0 && pointOnSegment(a, c, d)) ||
|
|
399
|
+
(cdb === 0 && pointOnSegment(b, c, d)) ||
|
|
400
|
+
(abc * abd < 0 && cda * cdb < 0)
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function segmentDistance(a: Point, b: Point, c: Point, d: Point): number {
|
|
405
|
+
if (segmentsIntersect(a, b, c, d)) return 0
|
|
406
|
+
|
|
407
|
+
return Math.min(
|
|
408
|
+
pointSegmentDistance(a, c, d),
|
|
409
|
+
pointSegmentDistance(b, c, d),
|
|
410
|
+
pointSegmentDistance(c, a, b),
|
|
411
|
+
pointSegmentDistance(d, a, b),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @returns Whether point is contained in the stroked polyline.
|
|
417
|
+
*/
|
|
418
|
+
export function lineContainsPoint(line: Line, point: Point): boolean {
|
|
419
|
+
const points = line.vertices
|
|
420
|
+
const radius = line.strokeWidth / 2
|
|
421
|
+
|
|
422
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
423
|
+
const a = points[i]
|
|
424
|
+
const b = points[i + 1]
|
|
425
|
+
|
|
426
|
+
if (pointSegmentDistance(point, a, b) <= radius) return true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @returns Whether the stroked polyline intersects the box.
|
|
434
|
+
*/
|
|
435
|
+
export function lineIntersectsBox(line: Line, box: Box): boolean {
|
|
436
|
+
const points = line.vertices
|
|
437
|
+
|
|
438
|
+
for (const point of points) {
|
|
439
|
+
if (boxContainsPoint(box, point)) return true
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const radius = line.strokeWidth / 2
|
|
443
|
+
const corners = boxCorners(box)
|
|
444
|
+
const edges = corners.map(
|
|
445
|
+
(corner, i) => [corner, corners[(i + 1) % corners.length]] as const,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
for (const corner of corners) {
|
|
449
|
+
if (lineContainsPoint(line, corner)) return true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (points.length === 1) {
|
|
453
|
+
return edges.some(([a, b]) => pointSegmentDistance(points[0], a, b) <= radius)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
457
|
+
const a = points[i]
|
|
458
|
+
const b = points[i + 1]
|
|
459
|
+
|
|
460
|
+
for (const [c, d] of edges) {
|
|
461
|
+
if (segmentDistance(a, b, c, d) <= radius) return true
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* @returns Line with increased strokeWidth for accessibility reasons.
|
|
470
|
+
*/
|
|
471
|
+
export function accessibleLine(line: Line): Line {
|
|
472
|
+
const { strokeWidth, vertices } = line
|
|
473
|
+
return {
|
|
474
|
+
vertices,
|
|
475
|
+
strokeWidth: Math.max(24, strokeWidth),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
352
479
|
/**
|
|
353
480
|
* @returns Four corners of a box.
|
|
354
481
|
* ⠀⠀ ●
|
|
@@ -447,7 +574,7 @@ export function resizeBox(box: Box, edge: Edge, by: number): Box {
|
|
|
447
574
|
* Scale box by one of its edges keeping its aspect ratio.
|
|
448
575
|
* The center and pivot is shifted accordingly so that visually only the edge moves.
|
|
449
576
|
*/
|
|
450
|
-
export function scaleBox(box: Box, direction: Edge | Corner, by: number) {
|
|
577
|
+
export function scaleBox(box: Box, direction: Edge | Corner, by: number): Box {
|
|
451
578
|
const vertical = direction.includes("n") || direction.includes("s")
|
|
452
579
|
const scale =
|
|
453
580
|
vertical && box.height !== 0
|
package/lib/model/history.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
2
|
import type { Editor } from "./editor"
|
|
3
|
-
import type { Node,
|
|
3
|
+
import type { Node, SerializedNode } from "./node"
|
|
4
4
|
import type { PageProps } from "./page"
|
|
5
5
|
|
|
6
|
-
export type HistoryAction<
|
|
7
|
-
| [action: "batch", payload: Array<HistoryAction
|
|
8
|
-
| [
|
|
9
|
-
action: "add-node",
|
|
10
|
-
payload: [pageId: string, nodes: Array<SerializedNode<string, Props>>],
|
|
11
|
-
]
|
|
6
|
+
export type HistoryAction<N extends Node = Node> =
|
|
7
|
+
| [action: "batch", payload: Array<HistoryAction<N>>]
|
|
8
|
+
| [action: "add-node", payload: [pageId: string, nodes: Array<SerializedNode<N>>]]
|
|
12
9
|
| [action: "delete-node", payload: [pageId: string, nodes: Array<string>]]
|
|
13
|
-
| [action: "set-node-props", payload: [nodeId: string, props: Partial<
|
|
10
|
+
| [action: "set-node-props", payload: [nodeId: string, props: Partial<N["props"]>]]
|
|
14
11
|
| [action: "set-page-props", payload: [pageId: string, props: Partial<PageProps>]]
|
|
15
12
|
| [action: "stack-order", payload: [pageId: string, order: Array<string>]]
|
|
16
13
|
|
|
@@ -19,9 +16,9 @@ function assert<T>(instance?: T): T {
|
|
|
19
16
|
return instance!
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
export type HistoryEntry<
|
|
23
|
-
undo: HistoryAction<
|
|
24
|
-
redo: HistoryAction<
|
|
19
|
+
export type HistoryEntry<N extends Node = Node> = {
|
|
20
|
+
undo: HistoryAction<N>
|
|
21
|
+
redo: HistoryAction<N>
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export class HistoryStore {
|
|
@@ -39,7 +36,7 @@ export class HistoryStore {
|
|
|
39
36
|
return this._redoHistory
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
readonly push = <
|
|
39
|
+
readonly push = <N extends Node>(entry: HistoryEntry<N>) => {
|
|
43
40
|
if (this._redoHistory.length > 0) {
|
|
44
41
|
this._redoHistory = []
|
|
45
42
|
}
|
|
@@ -73,7 +70,7 @@ export class HistoryStore {
|
|
|
73
70
|
this.emitChange()
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
private execute([action, payload]: HistoryAction) {
|
|
73
|
+
private execute<N extends Node>([action, payload]: HistoryAction<N>) {
|
|
77
74
|
switch (action) {
|
|
78
75
|
case "batch": {
|
|
79
76
|
payload.map((action) => this.execute(action))
|
package/lib/model/index.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
export { Editor, type NodeConstructor, type SerializedEditor } from "./editor"
|
|
2
2
|
export { type HistoryAction, type HistoryEntry } from "./history"
|
|
3
3
|
export { Node, type NodeProps, type SerializedNode } from "./node"
|
|
4
|
-
export { EditableNode, type EditableNodeProps } from "./node/
|
|
5
|
-
export { FormattableNode, type FormattableNodeProps } from "./node/
|
|
6
|
-
export { GroupNode, type GroupNodeProps } from "./node/
|
|
7
|
-
export { ImageNode, type ImageNodeProps } from "./node/
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
export { EditableNode, type EditableNodeProps } from "./node/editableNode"
|
|
5
|
+
export { FormattableNode, type FormattableNodeProps } from "./node/formattableNode"
|
|
6
|
+
export { GroupNode, type GroupNodeProps } from "./node/groupNode"
|
|
7
|
+
export { ImageNode, type ImageNodeProps } from "./node/imageNode"
|
|
8
|
+
export { LineNode, type LineNodeProps } from "./node/lineNode"
|
|
9
|
+
export {
|
|
10
|
+
ShapeNode,
|
|
11
|
+
type ShapeNodeProps,
|
|
12
|
+
newRectangleShape,
|
|
13
|
+
newPolygonShape,
|
|
14
|
+
newEllipseShape,
|
|
15
|
+
newArrowShape,
|
|
16
|
+
newStarShape,
|
|
17
|
+
} from "./node/shapeNode"
|
|
18
|
+
export { TextNode, type TextNodeProps } from "./node/textNode"
|
|
14
19
|
export { Page, type PageProps, type SerializedPage } from "./page"
|
|
15
20
|
export { flattenNodes } from "./traversal"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Editor as Tiptap, type JSONContent } from "@tiptap/react"
|
|
2
2
|
import { computed, state } from "react-bolt"
|
|
3
|
-
import type
|
|
4
|
-
import { Node, type NodeProps, type SerializedNode } from "../../node"
|
|
3
|
+
import { Node, type NodeProps } from "../../node"
|
|
5
4
|
import type { Page } from "../../page"
|
|
6
5
|
import { TiptapExtensions } from "./tiptapExtensions"
|
|
7
6
|
|
|
@@ -69,35 +68,15 @@ export abstract class EditableNode extends Node {
|
|
|
69
68
|
EditableNode.lastFocused = null
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
constructor(
|
|
73
|
-
|
|
74
|
-
page: Page,
|
|
75
|
-
{ content, lineHeight, ...props }: EditableNodeProps,
|
|
76
|
-
) {
|
|
77
|
-
super(editor, page, props)
|
|
71
|
+
constructor(page: Page, { content, lineHeight, ...props }: EditableNodeProps) {
|
|
72
|
+
super(page, props)
|
|
78
73
|
this.lineHeight = lineHeight ?? 1.2
|
|
79
74
|
this.content = content ?? { type: "doc", content: [] }
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
const rect = this.tiptap.view.dom.getBoundingClientRect()
|
|
84
|
-
return (
|
|
85
|
-
super.blockMove(event) ||
|
|
86
|
-
(this.tiptap.isFocused &&
|
|
87
|
-
event.clientX >= rect.left &&
|
|
88
|
-
event.clientX <= rect.right &&
|
|
89
|
-
event.clientY >= rect.top &&
|
|
90
|
-
event.clientY <= rect.bottom)
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
props(): EditableNodeProps {
|
|
77
|
+
get props(): EditableNodeProps {
|
|
95
78
|
const { content, lineHeight } = this
|
|
96
|
-
return { ...super.props
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
serialize(): SerializedNode<string, EditableNodeProps> {
|
|
100
|
-
return super.serialize()
|
|
79
|
+
return { ...super.props, content, lineHeight }
|
|
101
80
|
}
|
|
102
81
|
|
|
103
82
|
/**
|
|
@@ -107,14 +86,19 @@ export abstract class EditableNode extends Node {
|
|
|
107
86
|
*/
|
|
108
87
|
private static lastFocused: EditableNode | null = null
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Get the current focused node from {@link Editor.selection}.
|
|
91
|
+
*/
|
|
92
|
+
static getFocused(selection: Iterable<EditableNode>) {
|
|
93
|
+
const nodes = Array.from(selection)
|
|
94
|
+
|
|
95
|
+
const focused = nodes.find((e) => e.tiptap.isFocused)
|
|
112
96
|
|
|
113
97
|
if (focused) {
|
|
114
98
|
return (this.lastFocused = focused)
|
|
115
99
|
}
|
|
116
100
|
|
|
117
|
-
if (this.lastFocused &&
|
|
101
|
+
if (this.lastFocused && nodes.includes(this.lastFocused)) {
|
|
118
102
|
return this.lastFocused
|
|
119
103
|
}
|
|
120
104
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
|
-
import type
|
|
3
|
-
import
|
|
4
|
-
import type { Page } from "../page"
|
|
2
|
+
import { Node, type NodeProps } from "../../node"
|
|
3
|
+
import type { Page } from "../../page"
|
|
5
4
|
|
|
6
5
|
export type FormattableNodeProps = NodeProps &
|
|
7
6
|
Partial<
|
|
@@ -37,7 +36,6 @@ export abstract class FormattableNode extends Node {
|
|
|
37
36
|
@state accessor casing: "capitalize" | "uppercase" | "lowercase" | "normal"
|
|
38
37
|
|
|
39
38
|
constructor(
|
|
40
|
-
editor: Editor,
|
|
41
39
|
page: Page,
|
|
42
40
|
{
|
|
43
41
|
bold,
|
|
@@ -55,7 +53,7 @@ export abstract class FormattableNode extends Node {
|
|
|
55
53
|
...props
|
|
56
54
|
}: FormattableNodeProps,
|
|
57
55
|
) {
|
|
58
|
-
super(
|
|
56
|
+
super(page, props)
|
|
59
57
|
this.bold = bold ?? false
|
|
60
58
|
this.italic = italic ?? false
|
|
61
59
|
this.underline = underline ?? false
|
|
@@ -70,7 +68,7 @@ export abstract class FormattableNode extends Node {
|
|
|
70
68
|
this.casing = casing ?? "normal"
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
props(): FormattableNodeProps {
|
|
71
|
+
get props(): FormattableNodeProps {
|
|
74
72
|
const {
|
|
75
73
|
bold,
|
|
76
74
|
italic,
|
|
@@ -86,7 +84,7 @@ export abstract class FormattableNode extends Node {
|
|
|
86
84
|
casing,
|
|
87
85
|
} = this
|
|
88
86
|
return {
|
|
89
|
-
...super.props
|
|
87
|
+
...super.props,
|
|
90
88
|
size,
|
|
91
89
|
spacing,
|
|
92
90
|
lineHeight,
|
|
@@ -101,8 +99,4 @@ export abstract class FormattableNode extends Node {
|
|
|
101
99
|
...(family && { family }),
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
|
-
|
|
105
|
-
serialize(): SerializedNode<string, FormattableNodeProps> {
|
|
106
|
-
return super.serialize()
|
|
107
|
-
}
|
|
108
102
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../editor"
|
|
3
2
|
import { Node, type NodeProps, type SerializedNode } from "../node"
|
|
4
3
|
import type { Page } from "../page"
|
|
5
4
|
|
|
@@ -8,15 +7,15 @@ export interface GroupNodeProps extends NodeProps {
|
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export class GroupNode extends Node {
|
|
11
|
-
get name() {
|
|
10
|
+
get name(): "group" {
|
|
12
11
|
return "group"
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
@state accessor nodes = new Set<Node>()
|
|
16
15
|
|
|
17
|
-
constructor(
|
|
18
|
-
super(
|
|
19
|
-
this.nodes = new Set(nodes.map((node) => editor.deserializeNode(page, node)))
|
|
16
|
+
constructor(page: Page, { nodes = [], ...props }: GroupNodeProps) {
|
|
17
|
+
super(page, props)
|
|
18
|
+
this.nodes = new Set(nodes.map((node) => page.editor.deserializeNode(page, node)))
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
get width() {
|
|
@@ -63,17 +62,14 @@ export class GroupNode extends Node {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
props(): GroupNodeProps {
|
|
65
|
+
get props(): GroupNodeProps {
|
|
66
|
+
const { nodes, page } = this
|
|
67
67
|
return {
|
|
68
|
-
...super.props
|
|
69
|
-
nodes:
|
|
68
|
+
...super.props,
|
|
69
|
+
nodes: nodes
|
|
70
70
|
.values()
|
|
71
|
-
.map((node) =>
|
|
71
|
+
.map((node) => page.editor.serializeNode(node))
|
|
72
72
|
.toArray(),
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
serialize(): SerializedNode<this["name"], GroupNodeProps> {
|
|
77
|
-
return super.serialize()
|
|
78
|
-
}
|
|
79
75
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
|
-
import type
|
|
3
|
-
import { Node, type NodeProps, type SerializedNode } from "../node"
|
|
2
|
+
import { Node, type NodeProps } from "../node"
|
|
4
3
|
import type { Page } from "../page"
|
|
5
4
|
|
|
6
5
|
export type ImageNodeProps = NodeProps &
|
|
7
6
|
Partial<Pick<ImageNode, "url" | "fit" | "roundness" | "borderWidth" | "borderColor">>
|
|
8
7
|
|
|
9
8
|
export class ImageNode extends Node {
|
|
10
|
-
get name() {
|
|
9
|
+
get name(): "image" {
|
|
11
10
|
return "image"
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -18,24 +17,19 @@ export class ImageNode extends Node {
|
|
|
18
17
|
@state accessor borderColor: string
|
|
19
18
|
|
|
20
19
|
constructor(
|
|
21
|
-
editor: Editor,
|
|
22
20
|
page: Page,
|
|
23
21
|
{ url, fit, roundness, borderWidth, borderColor, ...props }: ImageNodeProps,
|
|
24
22
|
) {
|
|
25
|
-
super(
|
|
23
|
+
super(page, props)
|
|
26
24
|
this.url = url ?? null
|
|
27
|
-
this.fit = "cover"
|
|
25
|
+
this.fit = fit ?? "cover"
|
|
28
26
|
this.roundness = roundness ?? 0
|
|
29
27
|
this.borderColor = borderColor ?? "#000000"
|
|
30
28
|
this.borderWidth = borderWidth ?? 0
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
props(): ImageNodeProps {
|
|
31
|
+
get props(): ImageNodeProps {
|
|
34
32
|
const { url, fit, roundness, borderWidth, borderColor } = this
|
|
35
|
-
return { ...super.props
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
serialize(): SerializedNode<this["name"], ImageNodeProps> {
|
|
39
|
-
return super.serialize()
|
|
33
|
+
return { ...super.props, url, fit, roundness, borderWidth, borderColor }
|
|
40
34
|
}
|
|
41
35
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { computed, state } from "react-bolt"
|
|
2
|
+
import {
|
|
3
|
+
floatNorm,
|
|
4
|
+
pointAdd,
|
|
5
|
+
rect,
|
|
6
|
+
rectCenter,
|
|
7
|
+
rotatePoint,
|
|
8
|
+
type Line,
|
|
9
|
+
type Point,
|
|
10
|
+
} from "../geometry/math"
|
|
11
|
+
import { Node, type NodeProps } from "../node"
|
|
12
|
+
import type { Page } from "../page"
|
|
13
|
+
|
|
14
|
+
export type LineNodeProps = NodeProps &
|
|
15
|
+
Partial<Pick<LineNode, "points" | "strokeWidth" | "strokeColor">>
|
|
16
|
+
|
|
17
|
+
export class LineNode extends Node {
|
|
18
|
+
get name(): "line" {
|
|
19
|
+
return "line"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Relative to {@link x},{@link y} */
|
|
23
|
+
@state accessor points: Point[] = []
|
|
24
|
+
@state accessor strokeWidth: number
|
|
25
|
+
@state accessor strokeColor: string
|
|
26
|
+
|
|
27
|
+
@computed get height(): number {
|
|
28
|
+
return this.rect.height
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set height(height: number) {
|
|
32
|
+
this.scalePoints("y", height)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@computed get width(): number {
|
|
36
|
+
return this.rect.width
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set width(width: number) {
|
|
40
|
+
this.scalePoints("x", width)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(page: Page, { points, strokeWidth, strokeColor, ...props }: LineNodeProps) {
|
|
44
|
+
super(page, props)
|
|
45
|
+
this.strokeWidth = strokeWidth ?? 1
|
|
46
|
+
this.strokeColor = strokeColor ?? "#000000"
|
|
47
|
+
this.points = points ?? [
|
|
48
|
+
{ x: 0, y: 0 },
|
|
49
|
+
{ x: 100, y: 0 },
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get props(): LineNodeProps {
|
|
54
|
+
const { strokeWidth, strokeColor, points } = this
|
|
55
|
+
return { ...super.props, points, strokeWidth, strokeColor }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@computed private get rect() {
|
|
59
|
+
return rect(...this.points)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@computed get vertices(): Line["vertices"] {
|
|
63
|
+
const center = rectCenter(this)
|
|
64
|
+
return this.points.map((p) => rotatePoint(pointAdd(p, this), center, this.rotation))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private scalePoints(axis: keyof Point, size: number) {
|
|
68
|
+
const { x, y, width, height } = this.rect
|
|
69
|
+
|
|
70
|
+
const origin = axis === "x" ? x : y
|
|
71
|
+
const scale = size / (axis === "x" ? width : height)
|
|
72
|
+
|
|
73
|
+
this.points = this.points.map((p) => {
|
|
74
|
+
return {
|
|
75
|
+
...p,
|
|
76
|
+
[axis]: floatNorm(origin + (p[axis] - origin) * scale),
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../../editor"
|
|
3
2
|
import type { Page } from "../../page"
|
|
4
|
-
import { EditableNode, type EditableNodeProps } from "../
|
|
5
|
-
import type
|
|
3
|
+
import { EditableNode, type EditableNodeProps } from "../editableNode"
|
|
4
|
+
import { newRectangleShape, type Shape } from "./shape"
|
|
5
|
+
|
|
6
|
+
export * from "./shape"
|
|
6
7
|
|
|
7
8
|
export type ShapeNodeProps = EditableNodeProps &
|
|
8
9
|
Partial<
|
|
9
|
-
Pick<
|
|
10
|
+
Pick<
|
|
11
|
+
ShapeNode,
|
|
12
|
+
"valign" | "halign" | "background" | "borderWidth" | "borderColor" | "shape"
|
|
13
|
+
>
|
|
10
14
|
>
|
|
11
15
|
|
|
12
|
-
export
|
|
16
|
+
export class ShapeNode extends EditableNode {
|
|
17
|
+
get name(): "shape" {
|
|
18
|
+
return "shape"
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
@state accessor background: string
|
|
14
22
|
@state accessor borderWidth: number
|
|
15
23
|
@state accessor borderColor: string
|
|
@@ -17,32 +25,39 @@ export abstract class ShapeNode extends EditableNode {
|
|
|
17
25
|
@state accessor halign: "left" | "center" | "right" | "justify"
|
|
18
26
|
@state accessor valign: "top" | "center" | "bottom"
|
|
19
27
|
|
|
28
|
+
@state accessor shape: Shape
|
|
29
|
+
|
|
20
30
|
constructor(
|
|
21
|
-
editor: Editor,
|
|
22
31
|
page: Page,
|
|
23
|
-
{
|
|
32
|
+
{
|
|
33
|
+
valign,
|
|
34
|
+
halign,
|
|
35
|
+
background,
|
|
36
|
+
borderColor,
|
|
37
|
+
borderWidth,
|
|
38
|
+
shape,
|
|
39
|
+
...props
|
|
40
|
+
}: ShapeNodeProps,
|
|
24
41
|
) {
|
|
25
|
-
super(
|
|
42
|
+
super(page, props)
|
|
26
43
|
this.valign = valign ?? "center"
|
|
27
44
|
this.halign = halign ?? "center"
|
|
28
45
|
this.background = background ?? "#7f7f7f"
|
|
29
46
|
this.borderColor = borderColor ?? "#000000"
|
|
30
47
|
this.borderWidth = borderWidth ?? 0
|
|
48
|
+
this.shape = shape ?? newRectangleShape()
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
props(): ShapeNodeProps {
|
|
34
|
-
const { valign, halign, background, borderColor, borderWidth } = this
|
|
51
|
+
get props(): ShapeNodeProps {
|
|
52
|
+
const { valign, halign, background, borderColor, borderWidth, shape } = this
|
|
35
53
|
return {
|
|
36
|
-
...super.props
|
|
54
|
+
...super.props,
|
|
37
55
|
valign,
|
|
38
56
|
halign,
|
|
39
57
|
background,
|
|
58
|
+
shape,
|
|
40
59
|
...(borderWidth && { borderColor }),
|
|
41
60
|
...(borderWidth && { borderWidth }),
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
|
-
|
|
45
|
-
serialize(): SerializedNode<this["name"], EditableNodeProps> {
|
|
46
|
-
return super.serialize()
|
|
47
|
-
}
|
|
48
63
|
}
|