@lazlon-platform/html-editor 0.3.6 → 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.
@@ -0,0 +1,484 @@
1
+ export type Deg = number & { readonly __unit: "degrees" }
2
+ export type Rad = number & { readonly __unit: "radians" }
3
+ export type Edge = "n" | "s" | "w" | "e"
4
+ export type Corner = "ne" | "nw" | "se" | "sw"
5
+
6
+ export interface Point {
7
+ readonly x: number
8
+ readonly y: number
9
+ }
10
+
11
+ export interface Size {
12
+ readonly width: number
13
+ readonly height: number
14
+ }
15
+
16
+ export interface Rect extends Point, Size {
17
+ readonly top: number
18
+ readonly bottom: number
19
+ readonly left: number
20
+ readonly right: number
21
+ }
22
+
23
+ /**
24
+ * A rotated rectangle around a pivot point.
25
+ * Pivot point is relative to the center point.
26
+ */
27
+ export interface Box extends Size {
28
+ readonly center: Point
29
+ readonly rotation: Deg
30
+ readonly pivot: Point
31
+ }
32
+
33
+ /**
34
+ * Cast a number to a degree value.
35
+ */
36
+ export function deg(deg: number): Deg {
37
+ return (deg % 360) as Deg
38
+ }
39
+
40
+ /**
41
+ * Cast a number to a radian value.
42
+ */
43
+ export function rad(rad: number): Rad {
44
+ return rad as Rad
45
+ }
46
+
47
+ /**
48
+ * Create a point.
49
+ */
50
+ export function point(p?: Partial<Point>): Point {
51
+ return { x: p?.x ?? 0, y: p?.y ?? 0 }
52
+ }
53
+
54
+ /**
55
+ * Create a rectangle around the given points and rectangles.
56
+ */
57
+ export function rect(...args: Array<(Point & Size) | Point>): Rect {
58
+ const points: Point[] = args.flatMap((arg) =>
59
+ "width" in arg && "height" in arg ? rectCorners(arg) : arg,
60
+ )
61
+
62
+ let minX = Infinity
63
+ let minY = Infinity
64
+ let maxX = -Infinity
65
+ let maxY = -Infinity
66
+
67
+ for (const p of points) {
68
+ if (p.x < minX) minX = p.x
69
+ if (p.y < minY) minY = p.y
70
+ if (p.x > maxX) maxX = p.x
71
+ if (p.y > maxY) maxY = p.y
72
+ }
73
+
74
+ const rect = {
75
+ x: minX,
76
+ y: minY,
77
+ width: maxX - minX,
78
+ height: maxY - minY,
79
+ }
80
+
81
+ return {
82
+ ...rect,
83
+ top: rect.y,
84
+ bottom: rect.y + rect.height,
85
+ left: rect.x,
86
+ right: rect.x + rect.width,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Create a rotatable box for a rectangle
92
+ */
93
+ export function box(input?: Size & Point & { rotation?: Deg }): Box {
94
+ return {
95
+ center: rectCenter(input ?? rect()),
96
+ rotation: input?.rotation ?? deg(0),
97
+ pivot: rectCenter(input ?? rect()),
98
+ width: input?.width ?? 0,
99
+ height: input?.height ?? 0,
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @returns Four corners of a rectangle.
105
+ * ●───●
106
+ * │ │
107
+ * ●───●
108
+ */
109
+ export function rectCorners(rect: Size & Point): [Point, Point, Point, Point] {
110
+ const top = rect.y
111
+ const bottom = rect.y + rect.height
112
+ const left = rect.x
113
+ const right = rect.x + rect.width
114
+ return [
115
+ { y: top, x: left },
116
+ { y: top, x: right },
117
+ { y: bottom, x: right },
118
+ { y: bottom, x: left },
119
+ ]
120
+ }
121
+
122
+ /**
123
+ * @returns Center point of a rectangle.
124
+ * ┌─────┐
125
+ * │ ●C │
126
+ * └─────┘
127
+ */
128
+ export function rectCenter({ x, y, width, height }: Size & Point): Point {
129
+ return {
130
+ x: x + width / 2,
131
+ y: y + height / 2,
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @returns Rotate a point relative to a pivot point.
137
+ *
138
+ * @example
139
+ * ```
140
+ * P = (0,0)
141
+ * A = (2,0)
142
+ *
143
+ * rotate(A, P, 90)
144
+ * -> A' = (0,2)
145
+ * ```
146
+ *
147
+ * A
148
+ * ●
149
+ * │
150
+ * │ 90°
151
+ * ●─────● A'
152
+ * P
153
+ */
154
+ export function rotatePoint(p: Point, pivot: Point, deg: Deg): Point {
155
+ const rad = (deg * Math.PI) / 180
156
+ const cos = Math.cos(rad)
157
+ const sin = Math.sin(rad)
158
+ const dx = p.x - pivot.x
159
+ const dy = p.y - pivot.y
160
+
161
+ return {
162
+ x: pivot.x + dx * cos - dy * sin,
163
+ y: pivot.y + dx * sin + dy * cos,
164
+ }
165
+ }
166
+
167
+ /**
168
+ * @returns Axis-aligned bounding box (AABB) of the rotated rectangle.
169
+ * ⠀▁▁▁▁
170
+ * 🭵 ╱🯒🯓🭰
171
+ * 🭵╱ ╱🭰
172
+ * 🭵🯒🯓╱ 🭰
173
+ * ⠀▔▔▔▔
174
+ */
175
+ export function boxBounds(...boxes: Box[]): Rect {
176
+ const points = boxes.flatMap((box) => {
177
+ const corners = rectCorners({
178
+ x: box.center.x - box.width / 2,
179
+ y: box.center.y - box.height / 2,
180
+ width: box.width,
181
+ height: box.height,
182
+ })
183
+
184
+ return corners.map((corner) => {
185
+ return rotatePoint(corner, box.pivot, box.rotation)
186
+ })
187
+ })
188
+
189
+ return rect(...points)
190
+ }
191
+
192
+ /**
193
+ * @returns Distance between two points.
194
+ *
195
+ * ●
196
+ * ⠀╲
197
+ * ⠀⠀●
198
+ */
199
+ export function pointDist(a: Point, b: Point): number {
200
+ const dx = a.x - b.x
201
+ const dy = a.y - b.y
202
+ return Math.hypot(dx, dy)
203
+ }
204
+
205
+ /**
206
+ * Subtract point `b` from point `a`.
207
+ * b● ────▶ ●a
208
+ */
209
+ export function pointSubtract(a: Point, b: Point): Point {
210
+ return {
211
+ x: a.x - b.x,
212
+ y: a.y - b.y,
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Add point `b` to point `a`
218
+ *
219
+ * a● ────▶ ●b
220
+ */
221
+ export function pointAdd(a: Point, b: Point): Point {
222
+ return {
223
+ x: a.x + b.x,
224
+ y: a.y + b.y,
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Multiply a point by a scalar.
230
+ *
231
+ * ●──▶ p
232
+ * ●──────▶ p * n
233
+ */
234
+ export function pointMultiply(p: Point, n: number): Point {
235
+ return {
236
+ x: p.x * n,
237
+ y: p.y * n,
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Normalize a point as a vector from the origin.
243
+ *
244
+ * ●────────▶ p
245
+ * ●──▶ norm(p)
246
+ */
247
+ export function pointNorm(p: Point): Point {
248
+ const d = Math.hypot(p.x, p.y)
249
+ return {
250
+ x: d === 0 ? 0 : p.x / d,
251
+ y: d === 0 ? 0 : p.y / d,
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Compute the dot product of two points as vectors.
257
+ *
258
+ * B●
259
+ * ╱
260
+ * ╱
261
+ * ▔▔▔▔▔●A
262
+ */
263
+ export function vectorDotProd(a: Point, b: Point): number {
264
+ return a.x * b.x + a.y * b.y
265
+ }
266
+
267
+ /**
268
+ * Clamp a number between inclusive lower and upper bounds.
269
+ */
270
+ export function clamp(n: number, lo: number, hi: number) {
271
+ return Math.max(lo, Math.min(hi, n))
272
+ }
273
+
274
+ /**
275
+ * Round tiny floating point noise to zero and limit precision.
276
+ * @example
277
+ * ```
278
+ * 0.00000000001 -> 0
279
+ * 1.23456 -> 1.235
280
+ * 10 -> 10
281
+ * ```
282
+ */
283
+ export function floatNorm(v: number): number {
284
+ return Math.abs(v) < 1e-10 ? 0 : +v.toFixed(3)
285
+ }
286
+
287
+ /**
288
+ * Convert radians to degrees.
289
+ *
290
+ * ```text
291
+ * π rad = 180°
292
+ * ```
293
+ */
294
+ export function radToDeg(rad: Rad): Deg {
295
+ return deg((rad * 180) / Math.PI)
296
+ }
297
+
298
+ /**
299
+ * @example
300
+ * ```ts
301
+ * C = (0,0)
302
+ * A = (0,2)
303
+ * B = (2,0)
304
+ * angle(C, A, B) // 90
305
+ * ```
306
+ * A
307
+ * ●
308
+ * │
309
+ * │ 90°
310
+ * ●─────● B
311
+ * C
312
+ */
313
+ export function angle(center: Point, a: Point, b: Point): Deg {
314
+ const angA = radToDeg(rad(Math.atan2(a.y - center.y, a.x - center.x)))
315
+ const angB = radToDeg(rad(Math.atan2(b.y - center.y, b.x - center.x)))
316
+ return deg(Math.round(((((angA + angB + 180) % 360) + 360) % 360) - 180))
317
+ }
318
+
319
+ /**
320
+ * @returns Unrotated rectangle for box
321
+ */
322
+ export function boxRect(box: Box): Rect {
323
+ return rect({
324
+ x: box.center.x - box.width / 2,
325
+ y: box.center.y - box.height / 2,
326
+ width: box.width,
327
+ height: box.height,
328
+ })
329
+ }
330
+
331
+ /**
332
+ * @returns Whether point is contained in box.
333
+ * @example
334
+ * ┌──┐
335
+ * │ ●│
336
+ * └──┘
337
+ */
338
+ export function boxContainsPoint(target: Box, point: Point): boolean {
339
+ const p = rotatePoint(point, target.pivot, deg(-target.rotation))
340
+ const r = boxRect(target)
341
+ const x = floatNorm(p.x)
342
+ const y = floatNorm(p.y)
343
+
344
+ return (
345
+ x >= floatNorm(r.left) &&
346
+ x <= floatNorm(r.right) &&
347
+ y >= floatNorm(r.top) &&
348
+ y <= floatNorm(r.bottom)
349
+ )
350
+ }
351
+
352
+ /**
353
+ * @returns Four corners of a box.
354
+ * ⠀⠀ ●
355
+ * ⠀🯐🯑 🯒🯓
356
+ * ● ●
357
+ * ⠀🯒🯓 🯐🯑
358
+ * ⠀⠀⠀●
359
+ */
360
+ function boxCorners(box: Box): [Point, Point, Point, Point] {
361
+ const [a, b, c, d] = rectCorners(boxRect(box)).map((corner) =>
362
+ rotatePoint(corner, box.pivot, box.rotation),
363
+ )
364
+ return [a, b, c, d]
365
+ }
366
+
367
+ /**
368
+ * @returns Perpendicular normalized axes for each box edge.
369
+ */
370
+ function boxAxes(corners: Point[]): Point[] {
371
+ return corners
372
+ .map((corner, i) => pointSubtract(corners[(i + 1) % corners.length], corner))
373
+ .map((edge) => pointNorm({ x: -edge.y, y: edge.x }))
374
+ .filter((axis) => axis.x !== 0 || axis.y !== 0)
375
+ }
376
+
377
+ /**
378
+ * @returns Smallest and largest scalar projections of points onto an axis.
379
+ */
380
+ function projectPoints(points: Point[], axis: Point) {
381
+ let min = Infinity
382
+ let max = -Infinity
383
+
384
+ for (const point of points) {
385
+ const projection = floatNorm(vectorDotProd(point, axis))
386
+ if (projection < min) min = projection
387
+ if (projection > max) max = projection
388
+ }
389
+
390
+ return { min, max }
391
+ }
392
+
393
+ /**
394
+ * @returns Whether `a` covers any point from `b`.
395
+ */
396
+ export function boxIntersects(a: Box, b: Box): boolean {
397
+ const ac = boxCorners(a)
398
+ const bc = boxCorners(b)
399
+
400
+ return [...boxAxes(ac), ...boxAxes(bc)].every((axis) => {
401
+ const ap = projectPoints(ac, axis)
402
+ const bp = projectPoints(bc, axis)
403
+
404
+ return ap.max >= bp.min && bp.max >= ap.min
405
+ })
406
+ }
407
+
408
+ /**
409
+ * Perpendicular (signed) distance from point b to the infinite line
410
+ * that passes through point a with direction `deg` (degrees).
411
+ */
412
+ export function perpDistance(a: Point, b: Point, deg: Deg): number {
413
+ const normal = rotatePoint(point({ y: 1 }), point(), deg)
414
+ const vector = pointSubtract(b, a)
415
+ return vectorDotProd(vector, normal)
416
+ }
417
+
418
+ /**
419
+ * Resize box by one of its edges.
420
+ * The center and pivot is shifted accordingly so that visually only the edge moves.
421
+ */
422
+ export function resizeBox(box: Box, edge: Edge, by: number): Box {
423
+ const delta = by / 2
424
+ const direction =
425
+ edge === "n"
426
+ ? { x: 0, y: -1 }
427
+ : edge === "s"
428
+ ? { x: 0, y: 1 }
429
+ : edge === "w"
430
+ ? { x: -1, y: 0 }
431
+ : { x: 1, y: 0 }
432
+
433
+ const shift = pointMultiply(rotatePoint(direction, point(), box.rotation), delta)
434
+ const center = pointAdd(box.center, shift)
435
+ const pivot = pointAdd(box.pivot, shift)
436
+
437
+ return {
438
+ ...box,
439
+ center,
440
+ pivot,
441
+ width: edge === "w" || edge === "e" ? box.width + by : box.width,
442
+ height: edge === "n" || edge === "s" ? box.height + by : box.height,
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Scale box by one of its edges keeping its aspect ratio.
448
+ * The center and pivot is shifted accordingly so that visually only the edge moves.
449
+ */
450
+ export function scaleBox(box: Box, direction: Edge | Corner, by: number) {
451
+ const vertical = direction.includes("n") || direction.includes("s")
452
+ const scale =
453
+ vertical && box.height !== 0
454
+ ? (box.height + by) / box.height
455
+ : box.width !== 0
456
+ ? (box.width + by) / box.width
457
+ : 1
458
+
459
+ const width = box.width * scale
460
+ const height = box.height * scale
461
+ const widthDelta = width - box.width
462
+ const heightDelta = height - box.height
463
+
464
+ const isCorner = direction.length === 2
465
+ const north = direction.includes("n") ? heightDelta : 0
466
+ const south = direction.includes("s") ? heightDelta : 0
467
+ const west = direction.includes("w") ? widthDelta : 0
468
+ const east = direction.includes("e") ? widthDelta : 0
469
+ const x = (east - west) / 2
470
+ const y = (south - north) / 2
471
+ const edgeShift = isCorner ? { x, y } : vertical ? { x: 0, y } : { x, y: 0 }
472
+
473
+ const shift = rotatePoint(edgeShift, point(), box.rotation)
474
+ const center = pointAdd(box.center, shift)
475
+ const pivot = pointAdd(box.pivot, shift)
476
+
477
+ return {
478
+ rotation: box.rotation,
479
+ center,
480
+ pivot,
481
+ width,
482
+ height,
483
+ }
484
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ clamp,
3
+ floatNorm,
4
+ type Point,
5
+ pointAdd,
6
+ pointDist,
7
+ pointMultiply,
8
+ pointNorm,
9
+ pointSubtract,
10
+ vectorDotProd,
11
+ } from "./math"
12
+
13
+ export function roundedPathData(points: Point[], roundness: number): string {
14
+ const count = points.length
15
+ const winding = Math.sign(
16
+ points.reduce((area, curr, i) => {
17
+ const next = points[(i + 1) % count]
18
+ return area + curr.x * next.y - next.x * curr.y
19
+ }, 0),
20
+ )
21
+
22
+ const cmds = Array.from({ length: count }, (_, i) => {
23
+ const prev = points[(i - 1 + count) % count]
24
+ const curr = points[i]
25
+ const next = points[(i + 1) % count]
26
+ const cmd = i === 0 ? "M" : "L"
27
+
28
+ const v1 = pointNorm(pointSubtract(prev, curr))
29
+ const v2 = pointNorm(pointSubtract(next, curr))
30
+
31
+ const cosTheta = clamp(vectorDotProd(v1, v2), -1, 1)
32
+ const theta = Math.acos(cosTheta)
33
+ const cornerCross =
34
+ (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x)
35
+
36
+ if (!isFinite(theta) || theta < 1e-6 || roundness <= 0) {
37
+ return `${cmd} ${floatNorm(curr.x)} ${floatNorm(curr.y)}`
38
+ }
39
+
40
+ const dPrev = pointDist(curr, prev)
41
+ const dNext = pointDist(curr, next)
42
+ const tIdeal = roundness / Math.tan(theta / 2)
43
+ const t = Math.min(tIdeal, dPrev / 2, dNext / 2)
44
+ const rEff = t * Math.tan(theta / 2)
45
+ const p1 = pointAdd(curr, pointMultiply(v1, t))
46
+ const p2 = pointAdd(curr, pointMultiply(v2, t))
47
+ const isConcave = winding !== 0 && Math.sign(cornerCross) !== winding
48
+ const sweep = isConcave ? 0 : 1
49
+ const arc = `A ${floatNorm(rEff)} ${floatNorm(rEff)} 0 0 ${sweep} ${floatNorm(p2.x)} ${floatNorm(p2.y)}`
50
+
51
+ return `${cmd} ${floatNorm(p1.x)} ${floatNorm(p1.y)} ${arc}`
52
+ })
53
+
54
+ return cmds.concat("Z").join(" ")
55
+ }
@@ -3,7 +3,7 @@ import type { Editor } from "../../editor"
3
3
  import type { SerializedNode } from "../../node"
4
4
  import type { Page } from "../../page"
5
5
  import { ShapeNode, type ShapeNodeProps } from "./shape"
6
- import { roundedPathData } from "../../geometry"
6
+ import { roundedPathData } from "../../geometry/svg"
7
7
 
8
8
  export type ArrowNodeProps = ShapeNodeProps & Partial<Pick<ArrowNode, "roundness">>
9
9
 
@@ -1,9 +1,10 @@
1
1
  import { computed, state } from "react-bolt"
2
2
  import type { Editor } from "../../editor"
3
+ import type { Point } from "../../geometry/math"
4
+ import { roundedPathData } from "../../geometry/svg"
3
5
  import type { SerializedNode } from "../../node"
4
6
  import type { Page } from "../../page"
5
7
  import { ShapeNode, type ShapeNodeProps } from "./shape"
6
- import { regularPolygonPoints, roundedPathData } from "../../geometry"
7
8
 
8
9
  export type PolygonNodeProps = ShapeNodeProps &
9
10
  Partial<
@@ -106,3 +107,24 @@ export class PolygonNode extends ShapeNode {
106
107
  )
107
108
  }
108
109
  }
110
+
111
+ function regularPolygonPoints(props: {
112
+ width: number
113
+ height: number
114
+ sides: number
115
+ }): Point[] {
116
+ const { width, height, sides } = props
117
+ const rotation = sides % 2 === 0 ? Math.PI / sides : 0
118
+ const cx = width / 2
119
+ const cy = height / 2
120
+ const rx = width / 2
121
+ const ry = height / 2
122
+
123
+ return Array.from({ length: sides }, (_, i) => {
124
+ const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
125
+ return {
126
+ x: cx + rx * Math.cos(angle),
127
+ y: cy + ry * Math.sin(angle),
128
+ }
129
+ })
130
+ }
@@ -1,9 +1,10 @@
1
1
  import { computed, state } from "react-bolt"
2
- import { clamp, roundedPathData, starPoints } from "../../geometry"
3
2
  import type { Editor } from "../../editor"
4
3
  import type { SerializedNode } from "../../node"
5
4
  import type { Page } from "../../page"
6
5
  import { ShapeNode, type ShapeNodeProps } from "./shape"
6
+ import { clamp, type Point } from "../../geometry/math"
7
+ import { roundedPathData } from "../../geometry/svg"
7
8
 
8
9
  export type StarNodeProps = ShapeNodeProps &
9
10
  Partial<Pick<StarNode, "corners" | "roundness" | "depth">>
@@ -61,3 +62,30 @@ export class StarNode extends ShapeNode {
61
62
  )
62
63
  }
63
64
  }
65
+
66
+ function starPoints(props: {
67
+ width: number
68
+ height: number
69
+ corners: number
70
+ depth: number
71
+ }): Point[] {
72
+ const { width, height, corners, depth } = props
73
+ const cx = width / 2
74
+ const cy = height / 2
75
+ const outerRx = width / 2
76
+ const outerRy = height / 2
77
+ const innerRx = outerRx * depth
78
+ const innerRy = outerRy * depth
79
+
80
+ return Array.from({ length: corners * 2 }, (_, i) => {
81
+ const angle = (i * Math.PI) / corners - Math.PI / 2
82
+ const isOuter = i % 2 === 0
83
+ const rx = isOuter ? outerRx : innerRx
84
+ const ry = isOuter ? outerRy : innerRy
85
+
86
+ return {
87
+ x: cx + rx * Math.cos(angle),
88
+ y: cy + ry * Math.sin(angle),
89
+ }
90
+ })
91
+ }
@@ -1,11 +1,10 @@
1
1
  import { computed, state } from "react-bolt"
2
2
  import type { Editor } from "../editor"
3
3
  import type { Page } from "../page"
4
- import { EditableNode, type EditableNodeProps } from "./editable"
5
4
  import type { SerializedNode } from "../node"
5
+ import { EditableNode, type EditableNodeProps } from "./editable"
6
6
 
7
- export type TextNodeProps = EditableNodeProps &
8
- Partial<Pick<TextNode, "halign" | "scale">>
7
+ export type TextNodeProps = EditableNodeProps & Partial<Pick<TextNode, "halign" | "size">>
9
8
 
10
9
  export class TextNode extends EditableNode {
11
10
  get name() {
@@ -13,21 +12,25 @@ export class TextNode extends EditableNode {
13
12
  }
14
13
 
15
14
  @state accessor halign: "left" | "center" | "right" | "justify"
16
- @state accessor scale: number = 1
15
+ @state accessor size: number = 16
17
16
  @state accessor contentHeight: number = 24
18
17
 
18
+ private observer = new ResizeObserver(([entry]) => {
19
+ this.contentHeight = entry.target.clientHeight
20
+ })
21
+
19
22
  @computed get height(): number {
20
- return this.contentHeight * this.scale
23
+ return this.contentHeight
21
24
  }
22
25
 
23
- set height(n: number) {
24
- this.scale = n / this.contentHeight
26
+ set height(_) {
27
+ // no-op: TextNode's height can be set using its font-size
25
28
  }
26
29
 
27
- constructor(editor: Editor, page: Page, { halign, scale, ...props }: TextNodeProps) {
30
+ constructor(editor: Editor, page: Page, { halign, size, ...props }: TextNodeProps) {
28
31
  super(editor, page, props)
29
32
  this.halign = halign ?? "center"
30
- this.scale = scale ?? 1
33
+ this.size = size ?? 16
31
34
 
32
35
  this.tiptap.on("transaction", () => {
33
36
  const height = this.tiptap.view.dom.clientHeight
@@ -37,7 +40,13 @@ export class TextNode extends EditableNode {
37
40
 
38
41
  set contentRef(ref: HTMLElement | null) {
39
42
  super.contentRef = ref
40
- if (ref) this.contentHeight = ref.clientHeight
43
+
44
+ if (ref) {
45
+ this.observer.observe(ref)
46
+ this.contentHeight = ref.clientHeight
47
+ } else {
48
+ this.observer.disconnect()
49
+ }
41
50
  }
42
51
 
43
52
  @computed get contentRef() {
@@ -45,8 +54,8 @@ export class TextNode extends EditableNode {
45
54
  }
46
55
 
47
56
  props(): TextNodeProps {
48
- const { halign, scale } = this
49
- return { ...super.props(), halign, scale }
57
+ const { halign, size } = this
58
+ return { ...super.props(), halign, size }
50
59
  }
51
60
 
52
61
  serialize(): SerializedNode<this["name"], TextNodeProps> {