@lazlon-platform/html-editor 0.4.0 → 0.6.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 +54 -26
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/node.ts +14 -6
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +92 -57
- package/lib/hooks/pointer/pointer.ts +21 -11
- package/lib/hooks/pointer/resize.ts +176 -210
- package/lib/hooks/pointer/rotation.ts +89 -68
- package/lib/hooks/pointer/selectionFrame.ts +8 -11
- package/lib/hooks/pointer/selector.ts +62 -40
- package/lib/hooks/pointer/snap.ts +23 -23
- package/lib/hooks/textMarks.ts +1 -3
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +13 -9
- package/lib/model/geometry/math.ts +623 -0
- package/lib/model/geometry/svg.ts +55 -0
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +7 -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} +5 -11
- package/lib/model/node/lineNode.ts +59 -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} +19 -21
- package/lib/model/node.ts +11 -29
- package/lib/model/page.ts +4 -3
- 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} +4 -3
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +32 -0
- package/lib/ui/node/NodeView.tsx +1 -13
- 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 +9 -26
- package/package.json +34 -34
- package/lib/model/geometry.ts +0 -247
- 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 -108
- package/lib/model/node/shape/star.ts +0 -63
- 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
package/lib/model/geometry.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
export interface Point {
|
|
2
|
-
x: number
|
|
3
|
-
y: number
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface Rect extends Point {
|
|
7
|
-
width: number
|
|
8
|
-
height: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function rectCorners({ x, y, width, height }: Rect): Point[] {
|
|
12
|
-
return [
|
|
13
|
-
{ x, y },
|
|
14
|
-
{ x: x + width, y },
|
|
15
|
-
{ x, y: y + height },
|
|
16
|
-
{ x: x + width, y: y + height },
|
|
17
|
-
]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* const center = { x: 0, y: 0 }
|
|
24
|
-
* const P = { x: 2, y: 0 }
|
|
25
|
-
* rotate(P, center, 90) // P' = { x: 0, y: 2 }
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* P
|
|
29
|
-
* ●
|
|
30
|
-
* │
|
|
31
|
-
* │ 90°
|
|
32
|
-
* ●─────● P'
|
|
33
|
-
*/
|
|
34
|
-
export function rotatePoint(p: Point, center: Point, deg: number): Point {
|
|
35
|
-
const rad = (deg * Math.PI) / 180
|
|
36
|
-
const cos = Math.cos(rad)
|
|
37
|
-
const sin = Math.sin(rad)
|
|
38
|
-
const dx = p.x - center.x
|
|
39
|
-
const dy = p.y - center.y
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
x: center.x + dx * cos - dy * sin,
|
|
43
|
-
y: center.y + dx * sin + dy * cos,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function boundingBox(args: Point[] | Rect[]): Rect {
|
|
48
|
-
const points: Point[] = args.flatMap((arg) =>
|
|
49
|
-
"width" in arg && "height" in arg ? rectCorners(arg) : arg,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
let minX = Infinity
|
|
53
|
-
let minY = Infinity
|
|
54
|
-
let maxX = -Infinity
|
|
55
|
-
let maxY = -Infinity
|
|
56
|
-
|
|
57
|
-
for (const p of points) {
|
|
58
|
-
if (p.x < minX) minX = p.x
|
|
59
|
-
if (p.y < minY) minY = p.y
|
|
60
|
-
if (p.x > maxX) maxX = p.x
|
|
61
|
-
if (p.y > maxY) maxY = p.y
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
x: minX,
|
|
66
|
-
y: minY,
|
|
67
|
-
width: maxX - minX,
|
|
68
|
-
height: maxY - minY,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function rectCenter({ x, y, width, height }: Rect): Point {
|
|
73
|
-
return {
|
|
74
|
-
x: x + width / 2,
|
|
75
|
-
y: y + height / 2,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* axis-aligned bounding box (AABB) of the rotated rectangle
|
|
81
|
-
*/
|
|
82
|
-
export function rotatedAABB(rect: Rect, deg: number): Rect {
|
|
83
|
-
return boundingBox(rectCorners(rect).map((c) => rotatePoint(c, rectCenter(rect), deg)))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function dist(a: Point, b: Point) {
|
|
87
|
-
const dx = a.x - b.x
|
|
88
|
-
const dy = a.y - b.y
|
|
89
|
-
return Math.hypot(dx, dy)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function sub(a: Point, b: Point) {
|
|
93
|
-
return {
|
|
94
|
-
x: a.x - b.x,
|
|
95
|
-
y: a.y - b.y,
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function add(a: Point, b: Point) {
|
|
100
|
-
return {
|
|
101
|
-
x: a.x + b.x,
|
|
102
|
-
y: a.y + b.y,
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function mul(p: Point, n: number) {
|
|
107
|
-
return {
|
|
108
|
-
x: p.x * n,
|
|
109
|
-
y: p.y * n,
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function norm(p: Point) {
|
|
114
|
-
const d = Math.hypot(p.x, p.y)
|
|
115
|
-
return {
|
|
116
|
-
x: d === 0 ? 0 : p.x / d,
|
|
117
|
-
y: d === 0 ? 0 : p.y / d,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function dot(a: Point, b: Point) {
|
|
122
|
-
return a.x * b.x + a.y * b.y
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function clamp(n: number, lo: number, hi: number) {
|
|
126
|
-
return Math.max(lo, Math.min(hi, n))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function tidyFloat(v: number) {
|
|
130
|
-
return Math.abs(v) < 1e-10 ? 0 : +v.toFixed(3)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function radToDeg(rad: number): number {
|
|
134
|
-
return (rad * 180) / Math.PI
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* @example
|
|
139
|
-
* ```ts
|
|
140
|
-
* const center = { x: 0, y: 0 }
|
|
141
|
-
* const a = { x: 0, y: 2 }
|
|
142
|
-
* const b = { x: 2, y: 0 }
|
|
143
|
-
* angle(center, a, b) // 90
|
|
144
|
-
* ```
|
|
145
|
-
* a
|
|
146
|
-
* ●
|
|
147
|
-
* │
|
|
148
|
-
* │ 90°
|
|
149
|
-
* ●─────● b
|
|
150
|
-
*/
|
|
151
|
-
export function angle(center: Point, a: Point, b: Point): number {
|
|
152
|
-
const angA = radToDeg(Math.atan2(a.y - center.y, a.x - center.x))
|
|
153
|
-
const angB = radToDeg(Math.atan2(b.y - center.y, b.x - center.x))
|
|
154
|
-
return Math.round(((((angA + angB + 180) % 360) + 360) % 360) - 180)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function roundedPathData(points: Point[], roundness: number): string {
|
|
158
|
-
const count = points.length
|
|
159
|
-
const winding = Math.sign(
|
|
160
|
-
points.reduce((area, curr, i) => {
|
|
161
|
-
const next = points[(i + 1) % count]
|
|
162
|
-
return area + curr.x * next.y - next.x * curr.y
|
|
163
|
-
}, 0),
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
const cmds = Array.from({ length: count }, (_, i) => {
|
|
167
|
-
const prev = points[(i - 1 + count) % count]
|
|
168
|
-
const curr = points[i]
|
|
169
|
-
const next = points[(i + 1) % count]
|
|
170
|
-
const cmd = i === 0 ? "M" : "L"
|
|
171
|
-
|
|
172
|
-
const v1 = norm(sub(prev, curr))
|
|
173
|
-
const v2 = norm(sub(next, curr))
|
|
174
|
-
|
|
175
|
-
const cosTheta = clamp(dot(v1, v2), -1, 1)
|
|
176
|
-
const theta = Math.acos(cosTheta)
|
|
177
|
-
const cornerCross =
|
|
178
|
-
(curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x)
|
|
179
|
-
|
|
180
|
-
if (!isFinite(theta) || theta < 1e-6 || roundness <= 0) {
|
|
181
|
-
return `${cmd} ${tidyFloat(curr.x)} ${tidyFloat(curr.y)}`
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const dPrev = dist(curr, prev)
|
|
185
|
-
const dNext = dist(curr, next)
|
|
186
|
-
const tIdeal = roundness / Math.tan(theta / 2)
|
|
187
|
-
const t = Math.min(tIdeal, dPrev / 2, dNext / 2)
|
|
188
|
-
const rEff = t * Math.tan(theta / 2)
|
|
189
|
-
const p1 = add(curr, mul(v1, t))
|
|
190
|
-
const p2 = add(curr, mul(v2, t))
|
|
191
|
-
const isConcave = winding !== 0 && Math.sign(cornerCross) !== winding
|
|
192
|
-
const sweep = isConcave ? 0 : 1
|
|
193
|
-
const arc = `A ${tidyFloat(rEff)} ${tidyFloat(rEff)} 0 0 ${sweep} ${tidyFloat(p2.x)} ${tidyFloat(p2.y)}`
|
|
194
|
-
|
|
195
|
-
return `${cmd} ${tidyFloat(p1.x)} ${tidyFloat(p1.y)} ${arc}`
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
return cmds.concat("Z").join(" ")
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function regularPolygonPoints(props: {
|
|
202
|
-
width: number
|
|
203
|
-
height: number
|
|
204
|
-
sides: number
|
|
205
|
-
}): Point[] {
|
|
206
|
-
const { width, height, sides } = props
|
|
207
|
-
const rotation = sides % 2 === 0 ? Math.PI / sides : 0
|
|
208
|
-
const cx = width / 2
|
|
209
|
-
const cy = height / 2
|
|
210
|
-
const rx = width / 2
|
|
211
|
-
const ry = height / 2
|
|
212
|
-
|
|
213
|
-
return Array.from({ length: sides }, (_, i) => {
|
|
214
|
-
const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
|
|
215
|
-
return {
|
|
216
|
-
x: cx + rx * Math.cos(angle),
|
|
217
|
-
y: cy + ry * Math.sin(angle),
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function starPoints(props: {
|
|
223
|
-
width: number
|
|
224
|
-
height: number
|
|
225
|
-
corners: number
|
|
226
|
-
depth: number
|
|
227
|
-
}): Point[] {
|
|
228
|
-
const { width, height, corners, depth } = props
|
|
229
|
-
const cx = width / 2
|
|
230
|
-
const cy = height / 2
|
|
231
|
-
const outerRx = width / 2
|
|
232
|
-
const outerRy = height / 2
|
|
233
|
-
const innerRx = outerRx * depth
|
|
234
|
-
const innerRy = outerRy * depth
|
|
235
|
-
|
|
236
|
-
return Array.from({ length: corners * 2 }, (_, i) => {
|
|
237
|
-
const angle = (i * Math.PI) / corners - Math.PI / 2
|
|
238
|
-
const isOuter = i % 2 === 0
|
|
239
|
-
const rx = isOuter ? outerRx : innerRx
|
|
240
|
-
const ry = isOuter ? outerRy : innerRy
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
x: cx + rx * Math.cos(angle),
|
|
244
|
-
y: cy + ry * Math.sin(angle),
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../../editor"
|
|
3
|
-
import type { SerializedNode } from "../../node"
|
|
4
|
-
import type { Page } from "../../page"
|
|
5
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
6
|
-
import { roundedPathData } from "../../geometry"
|
|
7
|
-
|
|
8
|
-
export type ArrowNodeProps = ShapeNodeProps & Partial<Pick<ArrowNode, "roundness">>
|
|
9
|
-
|
|
10
|
-
export class ArrowNode extends ShapeNode {
|
|
11
|
-
get name() {
|
|
12
|
-
return "arrow"
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@state accessor roundness: number
|
|
16
|
-
|
|
17
|
-
constructor(editor: Editor, page: Page, { roundness = 0, ...props }: ArrowNodeProps) {
|
|
18
|
-
super(editor, page, props)
|
|
19
|
-
this.roundness = roundness
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
props(): ArrowNodeProps {
|
|
23
|
-
return {
|
|
24
|
-
...super.props(),
|
|
25
|
-
roundness: this.roundness,
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
serialize(): SerializedNode<this["name"], ArrowNodeProps> {
|
|
30
|
-
return super.serialize()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@computed get svgPathData() {
|
|
34
|
-
const { width, height } = this
|
|
35
|
-
const bodyRight = width * 0.68
|
|
36
|
-
const inset = height * 0.28
|
|
37
|
-
|
|
38
|
-
const arrowPoints = [
|
|
39
|
-
{ x: 0, y: inset },
|
|
40
|
-
{ x: bodyRight, y: inset },
|
|
41
|
-
{ x: bodyRight, y: 0 },
|
|
42
|
-
{ x: width, y: height / 2 },
|
|
43
|
-
{ x: bodyRight, y: height },
|
|
44
|
-
{ x: bodyRight, y: height - inset },
|
|
45
|
-
{ x: 0, y: height - inset },
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
return roundedPathData(arrowPoints, this.roundness)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Editor } from "../../editor"
|
|
2
|
-
import type { SerializedNode } from "../../node"
|
|
3
|
-
import type { Page } from "../../page"
|
|
4
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
5
|
-
|
|
6
|
-
export type EllipseNodeProps = ShapeNodeProps
|
|
7
|
-
|
|
8
|
-
export class EllipseNode extends ShapeNode {
|
|
9
|
-
get name() {
|
|
10
|
-
return "ellipse"
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
constructor(editor: Editor, page: Page, props: EllipseNodeProps) {
|
|
14
|
-
super(editor, page, props)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
props(): EllipseNodeProps {
|
|
18
|
-
return {
|
|
19
|
-
...super.props(),
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
serialize(): SerializedNode<this["name"], EllipseNodeProps> {
|
|
24
|
-
return super.serialize()
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../../editor"
|
|
3
|
-
import type { SerializedNode } from "../../node"
|
|
4
|
-
import type { Page } from "../../page"
|
|
5
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
6
|
-
import { regularPolygonPoints, roundedPathData } from "../../geometry"
|
|
7
|
-
|
|
8
|
-
export type PolygonNodeProps = ShapeNodeProps &
|
|
9
|
-
Partial<
|
|
10
|
-
Pick<
|
|
11
|
-
PolygonNode,
|
|
12
|
-
| "roundness"
|
|
13
|
-
| "sides"
|
|
14
|
-
| "cornerTopLeft"
|
|
15
|
-
| "cornerTopRight"
|
|
16
|
-
| "cornerBottomLeft"
|
|
17
|
-
| "cornerBottomRight"
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
export class PolygonNode extends ShapeNode {
|
|
22
|
-
get name() {
|
|
23
|
-
return "polygon"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@state private accessor _roundness: number
|
|
27
|
-
@state accessor sides: number
|
|
28
|
-
|
|
29
|
-
// rectangles are special cased where they support by corner rounding
|
|
30
|
-
@state accessor cornerTopLeft: number
|
|
31
|
-
@state accessor cornerTopRight: number
|
|
32
|
-
@state accessor cornerBottomLeft: number
|
|
33
|
-
@state accessor cornerBottomRight: number
|
|
34
|
-
|
|
35
|
-
set roundness(r: number) {
|
|
36
|
-
if (this.sides === 4) {
|
|
37
|
-
this.cornerTopLeft = r
|
|
38
|
-
this.cornerTopRight = r
|
|
39
|
-
this.cornerBottomLeft = r
|
|
40
|
-
this.cornerBottomRight = r
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this._roundness = r
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@computed get roundness() {
|
|
47
|
-
if (this.sides === 4) {
|
|
48
|
-
const [first, ...radii] = [
|
|
49
|
-
this.cornerTopLeft,
|
|
50
|
-
this.cornerTopRight,
|
|
51
|
-
this.cornerBottomLeft,
|
|
52
|
-
this.cornerBottomRight,
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
return radii.reduce((acc, it) => (it === acc ? acc : 0), first)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return this._roundness
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
constructor(
|
|
62
|
-
editor: Editor,
|
|
63
|
-
page: Page,
|
|
64
|
-
{
|
|
65
|
-
sides,
|
|
66
|
-
cornerTopLeft,
|
|
67
|
-
cornerTopRight,
|
|
68
|
-
cornerBottomLeft,
|
|
69
|
-
cornerBottomRight,
|
|
70
|
-
roundness = 0,
|
|
71
|
-
...props
|
|
72
|
-
}: PolygonNodeProps,
|
|
73
|
-
) {
|
|
74
|
-
super(editor, page, props)
|
|
75
|
-
this._roundness = roundness
|
|
76
|
-
this.sides = sides ?? 4
|
|
77
|
-
this.cornerTopLeft = cornerTopLeft ?? roundness
|
|
78
|
-
this.cornerTopRight = cornerTopRight ?? roundness
|
|
79
|
-
this.cornerBottomLeft = cornerBottomLeft ?? roundness
|
|
80
|
-
this.cornerBottomRight = cornerBottomRight ?? roundness
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
props(): PolygonNodeProps {
|
|
84
|
-
return {
|
|
85
|
-
...super.props(),
|
|
86
|
-
sides: this.sides,
|
|
87
|
-
cornerTopLeft: this.cornerTopLeft,
|
|
88
|
-
cornerTopRight: this.cornerTopRight,
|
|
89
|
-
cornerBottomLeft: this.cornerBottomLeft,
|
|
90
|
-
cornerBottomRight: this.cornerBottomRight,
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
serialize(): SerializedNode<this["name"], PolygonNodeProps> {
|
|
95
|
-
return super.serialize()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@computed get svgPathData() {
|
|
99
|
-
return roundedPathData(
|
|
100
|
-
regularPolygonPoints({
|
|
101
|
-
width: this.width,
|
|
102
|
-
height: this.height,
|
|
103
|
-
sides: this.sides,
|
|
104
|
-
}),
|
|
105
|
-
this._roundness,
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { computed, state } from "react-bolt"
|
|
2
|
-
import { clamp, roundedPathData, starPoints } from "../../geometry"
|
|
3
|
-
import type { Editor } from "../../editor"
|
|
4
|
-
import type { SerializedNode } from "../../node"
|
|
5
|
-
import type { Page } from "../../page"
|
|
6
|
-
import { ShapeNode, type ShapeNodeProps } from "./shape"
|
|
7
|
-
|
|
8
|
-
export type StarNodeProps = ShapeNodeProps &
|
|
9
|
-
Partial<Pick<StarNode, "corners" | "roundness" | "depth">>
|
|
10
|
-
|
|
11
|
-
export class StarNode extends ShapeNode {
|
|
12
|
-
get name() {
|
|
13
|
-
return "star"
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
@state accessor corners: number
|
|
17
|
-
@state accessor roundness: number
|
|
18
|
-
@state private accessor _depth: number
|
|
19
|
-
|
|
20
|
-
@computed get depth() {
|
|
21
|
-
return this._depth
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
set depth(value: number) {
|
|
25
|
-
this._depth = clamp(value, 0, 1)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
constructor(
|
|
29
|
-
editor: Editor,
|
|
30
|
-
page: Page,
|
|
31
|
-
{ corners = 5, roundness = 0, depth = 0.4, ...props }: StarNodeProps,
|
|
32
|
-
) {
|
|
33
|
-
super(editor, page, props)
|
|
34
|
-
this.corners = corners
|
|
35
|
-
this.roundness = roundness
|
|
36
|
-
this._depth = clamp(depth, 0, 1)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
props(): StarNodeProps {
|
|
40
|
-
return {
|
|
41
|
-
...super.props(),
|
|
42
|
-
corners: this.corners,
|
|
43
|
-
roundness: this.roundness,
|
|
44
|
-
depth: this.depth,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
serialize(): SerializedNode<this["name"], StarNodeProps> {
|
|
49
|
-
return super.serialize()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@computed get svgPathData() {
|
|
53
|
-
return roundedPathData(
|
|
54
|
-
starPoints({
|
|
55
|
-
width: this.width,
|
|
56
|
-
height: this.height,
|
|
57
|
-
corners: this.corners,
|
|
58
|
-
depth: this.depth,
|
|
59
|
-
}),
|
|
60
|
-
this.roundness,
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import clsx from "clsx"
|
|
2
|
-
import { useId } from "react"
|
|
3
|
-
import { useStore } from "react-bolt"
|
|
4
|
-
import { type ArrowNode } from "../../model/node/shape/arrow"
|
|
5
|
-
import { EditableContent } from "./EditableContent"
|
|
6
|
-
|
|
7
|
-
export function ArrowContent(props: { node: ArrowNode; isStatic?: boolean }) {
|
|
8
|
-
const maskId = useId()
|
|
9
|
-
const { node, isStatic } = props
|
|
10
|
-
|
|
11
|
-
const [valign, halign, background, borderWidth, borderColor] = useStore(
|
|
12
|
-
node,
|
|
13
|
-
"valign",
|
|
14
|
-
"halign",
|
|
15
|
-
"background",
|
|
16
|
-
"borderWidth",
|
|
17
|
-
"borderColor",
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
const [w, h, d] = useStore(node, "width", "height", "svgPathData")
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<div className="relative size-full">
|
|
24
|
-
<svg width={w} height={h} className="absolute inset-0">
|
|
25
|
-
<defs>
|
|
26
|
-
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
27
|
-
<path d={d} fill="white" />
|
|
28
|
-
</mask>
|
|
29
|
-
</defs>
|
|
30
|
-
|
|
31
|
-
<path d={d} fill={background} />
|
|
32
|
-
<path
|
|
33
|
-
d={d}
|
|
34
|
-
fill="none"
|
|
35
|
-
stroke={borderColor}
|
|
36
|
-
strokeWidth={borderWidth}
|
|
37
|
-
mask={`url(#${maskId})`}
|
|
38
|
-
/>
|
|
39
|
-
</svg>
|
|
40
|
-
<div
|
|
41
|
-
className={clsx(
|
|
42
|
-
"flex size-full",
|
|
43
|
-
valign === "top" && "items-start",
|
|
44
|
-
valign === "center" && "items-center",
|
|
45
|
-
valign === "bottom" && "items-end",
|
|
46
|
-
halign === "left" && "justify-start text-left",
|
|
47
|
-
halign === "center" && "justify-center text-center",
|
|
48
|
-
halign === "right" && "justify-end text-right",
|
|
49
|
-
halign === "justify" && "w-full text-justify",
|
|
50
|
-
)}
|
|
51
|
-
>
|
|
52
|
-
<EditableContent
|
|
53
|
-
isStatic={isStatic}
|
|
54
|
-
node={node}
|
|
55
|
-
className={clsx(halign === "justify" && "w-full")}
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import clsx from "clsx"
|
|
2
|
-
import { useStore } from "react-bolt"
|
|
3
|
-
import { type EllipseNode } from "../../model/node/shape/ellipse"
|
|
4
|
-
import { EditableContent } from "./EditableContent"
|
|
5
|
-
|
|
6
|
-
export function EllipseContent(props: { node: EllipseNode; isStatic?: boolean }) {
|
|
7
|
-
const { node, isStatic } = props
|
|
8
|
-
|
|
9
|
-
const [valign, halign, background, borderWidth, borderColor] = useStore(
|
|
10
|
-
node,
|
|
11
|
-
"valign",
|
|
12
|
-
"halign",
|
|
13
|
-
"background",
|
|
14
|
-
"borderWidth",
|
|
15
|
-
"borderColor",
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="relative size-full">
|
|
20
|
-
<div
|
|
21
|
-
className="absolute inset-0 size-full"
|
|
22
|
-
onDragStart={(e) => e.preventDefault()}
|
|
23
|
-
style={{
|
|
24
|
-
borderRadius: "50%",
|
|
25
|
-
background,
|
|
26
|
-
border: `${borderWidth}px solid ${borderColor}`,
|
|
27
|
-
}}
|
|
28
|
-
/>
|
|
29
|
-
<div
|
|
30
|
-
className={clsx(
|
|
31
|
-
"flex size-full",
|
|
32
|
-
valign === "top" && "items-start",
|
|
33
|
-
valign === "center" && "items-center",
|
|
34
|
-
valign === "bottom" && "items-end",
|
|
35
|
-
halign === "left" && "justify-start text-left",
|
|
36
|
-
halign === "center" && "justify-center text-center",
|
|
37
|
-
halign === "right" && "justify-end text-right",
|
|
38
|
-
halign === "justify" && "w-full text-justify",
|
|
39
|
-
)}
|
|
40
|
-
>
|
|
41
|
-
<EditableContent
|
|
42
|
-
isStatic={isStatic}
|
|
43
|
-
node={node}
|
|
44
|
-
className={clsx(halign === "justify" && "w-full")}
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import clsx from "clsx"
|
|
2
|
-
import { useId } from "react"
|
|
3
|
-
import { useStore } from "react-bolt"
|
|
4
|
-
import { type PolygonNode } from "../../model/node/shape/polygon"
|
|
5
|
-
import { EditableContent } from "./EditableContent"
|
|
6
|
-
|
|
7
|
-
export function PolygonContent(props: { node: PolygonNode; isStatic?: boolean }) {
|
|
8
|
-
const maskId = useId()
|
|
9
|
-
const { node, isStatic } = props
|
|
10
|
-
|
|
11
|
-
const [valign, halign, background, borderWidth, borderColor, sides] = useStore(
|
|
12
|
-
node,
|
|
13
|
-
"valign",
|
|
14
|
-
"halign",
|
|
15
|
-
"background",
|
|
16
|
-
"borderWidth",
|
|
17
|
-
"borderColor",
|
|
18
|
-
"sides",
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
const [w, h, d] = useStore(node, "width", "height", "svgPathData")
|
|
22
|
-
|
|
23
|
-
const radii = useStore(
|
|
24
|
-
node,
|
|
25
|
-
"cornerTopLeft",
|
|
26
|
-
"cornerTopRight",
|
|
27
|
-
"cornerBottomRight",
|
|
28
|
-
"cornerBottomLeft",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div className="relative size-full">
|
|
33
|
-
{sides !== 4 ? (
|
|
34
|
-
<svg width={w} height={h} className="absolute inset-0">
|
|
35
|
-
<defs>
|
|
36
|
-
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
37
|
-
<path d={d} fill="white" />
|
|
38
|
-
</mask>
|
|
39
|
-
</defs>
|
|
40
|
-
|
|
41
|
-
<path d={d} fill={background} />
|
|
42
|
-
<path
|
|
43
|
-
d={d}
|
|
44
|
-
fill="none"
|
|
45
|
-
stroke={borderColor}
|
|
46
|
-
strokeWidth={borderWidth}
|
|
47
|
-
mask={`url(#${maskId})`}
|
|
48
|
-
/>
|
|
49
|
-
</svg>
|
|
50
|
-
) : (
|
|
51
|
-
<div
|
|
52
|
-
className="absolute inset-0 size-full"
|
|
53
|
-
onDragStart={(e) => e.preventDefault()}
|
|
54
|
-
style={{
|
|
55
|
-
borderRadius: radii.map((r) => `${r}px`).join(" "),
|
|
56
|
-
background: background,
|
|
57
|
-
border: `${borderWidth}px solid ${borderColor}`,
|
|
58
|
-
}}
|
|
59
|
-
/>
|
|
60
|
-
)}
|
|
61
|
-
<div
|
|
62
|
-
className={clsx(
|
|
63
|
-
"flex size-full",
|
|
64
|
-
valign === "top" && "items-start",
|
|
65
|
-
valign === "center" && "items-center",
|
|
66
|
-
valign === "bottom" && "items-end",
|
|
67
|
-
halign === "left" && "justify-start text-left",
|
|
68
|
-
halign === "center" && "justify-center text-center",
|
|
69
|
-
halign === "right" && "justify-end text-right",
|
|
70
|
-
halign === "justify" && "w-full text-justify",
|
|
71
|
-
)}
|
|
72
|
-
>
|
|
73
|
-
<EditableContent
|
|
74
|
-
isStatic={isStatic}
|
|
75
|
-
node={node}
|
|
76
|
-
className={clsx(halign === "justify" && "w-full")}
|
|
77
|
-
/>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|