@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.
Files changed (64) hide show
  1. package/lib/hooks/actions.ts +136 -87
  2. package/lib/hooks/batch.ts +24 -10
  3. package/lib/hooks/index.ts +7 -6
  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} +47 -39
  7. package/lib/hooks/pointer/{pointer.ts → usePointer.ts} +4 -5
  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} +18 -3
  17. package/lib/hooks/pointer/{snap.ts → useSnap.ts} +5 -4
  18. package/lib/hooks/{pointer/selectionFrame.ts → selectionFrame.ts} +9 -6
  19. package/lib/hooks/textMarks.ts +30 -21
  20. package/lib/lib/googleFonts.ts +1 -5
  21. package/lib/model/editor.ts +31 -13
  22. package/lib/model/geometry/math.ts +128 -1
  23. package/lib/model/history.ts +10 -13
  24. package/lib/model/index.ts +15 -10
  25. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  26. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  27. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  28. package/lib/model/node/{image.ts → imageNode.ts} +6 -12
  29. package/lib/model/node/lineNode.ts +80 -0
  30. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  31. package/lib/model/node/shapeNode/shape.ts +96 -0
  32. package/lib/model/node/{text.ts → textNode.ts} +9 -24
  33. package/lib/model/node.ts +27 -32
  34. package/lib/model/page.ts +4 -4
  35. package/lib/model/traversal.ts +1 -1
  36. package/lib/ui/extractor.ts +3 -3
  37. package/lib/ui/index.ts +2 -4
  38. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +10 -7
  39. package/lib/ui/node/GroupContent.tsx +1 -1
  40. package/lib/ui/node/ImageContent.tsx +1 -1
  41. package/lib/ui/node/LineContent.tsx +30 -0
  42. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  43. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  44. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  45. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  46. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  47. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  48. package/lib/ui/node/TextContent.tsx +1 -1
  49. package/lib/ui/selection.ts +6 -5
  50. package/package.json +1 -1
  51. package/lib/hooks/pointer/resize.ts +0 -247
  52. package/lib/hooks/pointer/rotation.ts +0 -103
  53. package/lib/model/node/shape/arrow.ts +0 -50
  54. package/lib/model/node/shape/ellipse.ts +0 -26
  55. package/lib/model/node/shape/polygon.ts +0 -130
  56. package/lib/model/node/shape/star.ts +0 -91
  57. package/lib/ui/node/ArrowContent.tsx +0 -60
  58. package/lib/ui/node/EllipseContent.tsx +0 -49
  59. package/lib/ui/node/PolygonContent.tsx +0 -81
  60. package/lib/ui/node/StarContent.tsx +0 -60
  61. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  62. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  63. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  64. /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
@@ -1,16 +1,13 @@
1
1
  import { computed, state } from "react-bolt"
2
2
  import type { Editor } from "./editor"
3
- import type { Node, NodeProps, SerializedNode } from "./node"
3
+ import type { Node, SerializedNode } from "./node"
4
4
  import type { PageProps } from "./page"
5
5
 
6
- export type HistoryAction<Props extends NodeProps = NodeProps> =
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<Props>]]
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<Props extends NodeProps = NodeProps> = {
23
- undo: HistoryAction<Props>
24
- redo: HistoryAction<Props>
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 = <Props extends NodeProps = NodeProps>(entry: HistoryEntry<Props>) => {
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))
@@ -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/editable"
5
- export { FormattableNode, type FormattableNodeProps } from "./node/formattable"
6
- export { GroupNode, type GroupNodeProps } from "./node/group"
7
- export { ImageNode, type ImageNodeProps } from "./node/image"
8
- export { ArrowNode, type ArrowNodeProps } from "./node/shape/arrow"
9
- export { EllipseNode, type EllipseNodeProps } from "./node/shape/ellipse"
10
- export { PolygonNode, type PolygonNodeProps } from "./node/shape/polygon"
11
- export { ShapeNode, type ShapeNodeProps } from "./node/shape/shape"
12
- export { StarNode, type StarNodeProps } from "./node/shape/star"
13
- export { TextNode, type TextNodeProps } from "./node/text"
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 { Editor } from "../../editor"
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
- editor: Editor,
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
- blockMove(event: React.PointerEvent): boolean {
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(), content, lineHeight }
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
- static getFocused(node: EditableNode[]) {
111
- const focused = node.find((e) => e.tiptap.isFocused)
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 && node.includes(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 { Editor } from "../editor"
3
- import { Node, type NodeProps, type SerializedNode } from "../node"
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(editor, page, props)
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(editor: Editor, page: Page, { nodes = [], ...props }: GroupNodeProps) {
18
- super(editor, page, props)
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: this.nodes
68
+ ...super.props,
69
+ nodes: nodes
70
70
  .values()
71
- .map((node) => node.serialize())
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 { Editor } from "../editor"
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(editor, page, props)
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(), url, fit, roundness, borderWidth, borderColor }
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 "../editable"
5
- import type { SerializedNode } from "../../node"
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<ShapeNode, "valign" | "halign" | "background" | "borderWidth" | "borderColor">
10
+ Pick<
11
+ ShapeNode,
12
+ "valign" | "halign" | "background" | "borderWidth" | "borderColor" | "shape"
13
+ >
10
14
  >
11
15
 
12
- export abstract class ShapeNode extends EditableNode {
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
- { valign, halign, background, borderColor, borderWidth, ...props }: ShapeNodeProps,
32
+ {
33
+ valign,
34
+ halign,
35
+ background,
36
+ borderColor,
37
+ borderWidth,
38
+ shape,
39
+ ...props
40
+ }: ShapeNodeProps,
24
41
  ) {
25
- super(editor, page, props)
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
  }