@zseven-w/pen-renderer 0.0.1
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/README.md +64 -0
- package/package.json +31 -0
- package/src/document-flattener.ts +340 -0
- package/src/font-manager.ts +401 -0
- package/src/image-loader.ts +93 -0
- package/src/index.ts +60 -0
- package/src/init.ts +44 -0
- package/src/node-renderer.ts +599 -0
- package/src/paint-utils.ts +148 -0
- package/src/path-utils.ts +225 -0
- package/src/renderer.ts +374 -0
- package/src/spatial-index.ts +89 -0
- package/src/text-renderer.ts +531 -0
- package/src/types.ts +40 -0
- package/src/viewport.ts +102 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { CanvasKit } from 'canvaskit-wasm'
|
|
2
|
+
import type { PenFill, PenStroke } from '@zseven-w/pen-types'
|
|
3
|
+
import { DEFAULT_FILL, DEFAULT_STROKE_WIDTH } from '@zseven-w/pen-core'
|
|
4
|
+
|
|
5
|
+
export { cssFontFamily } from '@zseven-w/pen-core'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Color parsing — ck.Color4f takes 0-1 floats for all channels (r, g, b, a)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export function parseColor(ck: CanvasKit, color: string): Float32Array {
|
|
12
|
+
if (color.startsWith('#')) {
|
|
13
|
+
const hex = color.slice(1)
|
|
14
|
+
if (hex.length === 8) {
|
|
15
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
16
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
17
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
18
|
+
const a = parseInt(hex.slice(6, 8), 16) / 255
|
|
19
|
+
return ck.Color4f(r, g, b, a)
|
|
20
|
+
}
|
|
21
|
+
if (hex.length === 6) {
|
|
22
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
23
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
24
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
25
|
+
return ck.Color4f(r, g, b, 1)
|
|
26
|
+
}
|
|
27
|
+
if (hex.length === 3) {
|
|
28
|
+
const r = parseInt(hex[0] + hex[0], 16) / 255
|
|
29
|
+
const g = parseInt(hex[1] + hex[1], 16) / 255
|
|
30
|
+
const b = parseInt(hex[2] + hex[2], 16) / 255
|
|
31
|
+
return ck.Color4f(r, g, b, 1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (color === 'transparent') return ck.Color4f(0, 0, 0, 0)
|
|
35
|
+
if (color === 'white') return ck.Color4f(1, 1, 1, 1)
|
|
36
|
+
if (color === 'black') return ck.Color4f(0, 0, 0, 1)
|
|
37
|
+
// rgba() parsing
|
|
38
|
+
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
|
|
39
|
+
if (rgbaMatch) {
|
|
40
|
+
return ck.Color4f(
|
|
41
|
+
parseInt(rgbaMatch[1]) / 255,
|
|
42
|
+
parseInt(rgbaMatch[2]) / 255,
|
|
43
|
+
parseInt(rgbaMatch[3]) / 255,
|
|
44
|
+
rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return ck.Color4f(0.82, 0.835, 0.858, 1) // fallback #d1d5db
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Corner radius helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function cornerRadiusValue(cr: number | [number, number, number, number] | undefined): number {
|
|
55
|
+
if (cr === undefined) return 0
|
|
56
|
+
if (typeof cr === 'number') return cr
|
|
57
|
+
return cr[0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cornerRadii(cr: number | [number, number, number, number] | undefined): [number, number, number, number] {
|
|
61
|
+
if (cr === undefined) return [0, 0, 0, 0]
|
|
62
|
+
if (typeof cr === 'number') return [cr, cr, cr, cr]
|
|
63
|
+
return cr
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Fill / stroke helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export function resolveFillColor(fills?: PenFill[] | string): string {
|
|
71
|
+
if (typeof fills === 'string') return fills
|
|
72
|
+
if (!fills || fills.length === 0) return DEFAULT_FILL
|
|
73
|
+
const first = fills[0]
|
|
74
|
+
if (first.type === 'solid') return first.color
|
|
75
|
+
if (first.type === 'linear_gradient' || first.type === 'radial_gradient') {
|
|
76
|
+
return first.stops[0]?.color ?? DEFAULT_FILL
|
|
77
|
+
}
|
|
78
|
+
return DEFAULT_FILL
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveStrokeColor(stroke?: PenStroke): string | undefined {
|
|
82
|
+
if (!stroke) return undefined
|
|
83
|
+
if (typeof stroke.fill === 'string') return stroke.fill
|
|
84
|
+
if (stroke.fill && stroke.fill.length > 0) return resolveFillColor(stroke.fill)
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveStrokeWidth(stroke?: PenStroke): number {
|
|
89
|
+
if (!stroke) return 0
|
|
90
|
+
if (typeof stroke.thickness === 'number') return stroke.thickness
|
|
91
|
+
if (typeof stroke.thickness === 'object' && !Array.isArray(stroke.thickness)) return 0
|
|
92
|
+
return stroke.thickness?.[0] ?? DEFAULT_STROKE_WIDTH
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Text wrapping utilities
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** CJK character range check (for character-level line breaking). */
|
|
100
|
+
function isCJK(ch: string): boolean {
|
|
101
|
+
const c = ch.charCodeAt(0)
|
|
102
|
+
return (c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3400 && c <= 0x4DBF) ||
|
|
103
|
+
(c >= 0x3000 && c <= 0x303F) || (c >= 0xFF00 && c <= 0xFFEF) ||
|
|
104
|
+
(c >= 0x2E80 && c <= 0x2FDF)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Word-wrap a single line of text, appending wrapped lines to `out`. */
|
|
108
|
+
export function wrapLine(ctx: CanvasRenderingContext2D, text: string, maxW: number, out: string[]) {
|
|
109
|
+
if (ctx.measureText(text).width <= maxW) { out.push(text); return }
|
|
110
|
+
|
|
111
|
+
let current = ''
|
|
112
|
+
let i = 0
|
|
113
|
+
while (i < text.length) {
|
|
114
|
+
const ch = text[i]
|
|
115
|
+
if (isCJK(ch)) {
|
|
116
|
+
const test = current + ch
|
|
117
|
+
if (ctx.measureText(test).width > maxW && current) {
|
|
118
|
+
out.push(current)
|
|
119
|
+
current = ch
|
|
120
|
+
} else {
|
|
121
|
+
current = test
|
|
122
|
+
}
|
|
123
|
+
i++
|
|
124
|
+
} else if (ch === ' ') {
|
|
125
|
+
const test = current + ch
|
|
126
|
+
if (ctx.measureText(test).width > maxW && current) {
|
|
127
|
+
out.push(current)
|
|
128
|
+
current = ''
|
|
129
|
+
} else {
|
|
130
|
+
current = test
|
|
131
|
+
}
|
|
132
|
+
i++
|
|
133
|
+
} else {
|
|
134
|
+
let word = ''
|
|
135
|
+
while (i < text.length && text[i] !== ' ' && !isCJK(text[i])) {
|
|
136
|
+
word += text[i]; i++
|
|
137
|
+
}
|
|
138
|
+
const test = current + word
|
|
139
|
+
if (ctx.measureText(test).width > maxW && current) {
|
|
140
|
+
out.push(current)
|
|
141
|
+
current = word
|
|
142
|
+
} else {
|
|
143
|
+
current = test
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (current) out.push(current)
|
|
148
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { CanvasKit, Path } from 'canvaskit-wasm'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize SVG path data for CanvasKit's parser:
|
|
5
|
+
* - Add spaces between command letters and numbers
|
|
6
|
+
* - Handle negative-sign number separators (e.g. "10-5" -> "10 -5")
|
|
7
|
+
* - Normalize comma separators to spaces
|
|
8
|
+
* - Separate concatenated arc flags (e.g. "a2 2 0 012 2" -> "a2 2 0 0 1 2 2")
|
|
9
|
+
*/
|
|
10
|
+
export function sanitizeSvgPath(d: string): string {
|
|
11
|
+
let result = d
|
|
12
|
+
// Add space between command letter and following number/sign
|
|
13
|
+
.replace(/([MLCQZAHVSmlcqzahvsTt])([0-9.+-])/g, '$1 $2')
|
|
14
|
+
// Add space between digit and following negative sign (number separator)
|
|
15
|
+
.replace(/(\d)-/g, '$1 -')
|
|
16
|
+
// Replace commas with spaces
|
|
17
|
+
.replace(/,/g, ' ')
|
|
18
|
+
// Collapse multiple spaces
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim()
|
|
21
|
+
|
|
22
|
+
// Separate concatenated arc flags: in SVG arc commands, the large-arc and
|
|
23
|
+
// sweep flags are single digits (0 or 1) that may be concatenated with each
|
|
24
|
+
// other and with the following number. e.g. "a2 2 0 012 2" -> "a2 2 0 0 1 2 2"
|
|
25
|
+
result = result.replace(
|
|
26
|
+
/([aA])\s*([\d.e+-]+)\s+([\d.e+-]+)\s+([\d.e+-]+)\s+([01])([01])([\d.+-])/g,
|
|
27
|
+
'$1 $2 $3 $4 $5 $6 $7',
|
|
28
|
+
)
|
|
29
|
+
// Handle the case where all three (rotation + flags) are concatenated without spaces,
|
|
30
|
+
// e.g. "a4 4 0100-8" where 0100 = rotation=0, large-arc=1, sweep=0, then 0 is start of x
|
|
31
|
+
result = result.replace(
|
|
32
|
+
/([aA])\s*([\d.e+-]+)\s+([\d.e+-]+)\s+(\d)([01])([01])([\d.+-])/g,
|
|
33
|
+
'$1 $2 $3 $4 $5 $6 $7',
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Returns true if the path string contains NaN or Infinity values. */
|
|
40
|
+
export function hasInvalidNumbers(d: string): boolean {
|
|
41
|
+
return /NaN|Infinity/i.test(d)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert an SVG arc segment to cubic bezier curves and add them to the path.
|
|
46
|
+
* Based on the W3C SVG implementation note for arc-to-cubic conversion.
|
|
47
|
+
*/
|
|
48
|
+
function arcToCubics(
|
|
49
|
+
path: Path,
|
|
50
|
+
x1: number, y1: number,
|
|
51
|
+
rxIn: number, ryIn: number,
|
|
52
|
+
largeArc: boolean, sweep: boolean,
|
|
53
|
+
x2: number, y2: number,
|
|
54
|
+
): void {
|
|
55
|
+
// Degenerate: start == end
|
|
56
|
+
if (x1 === x2 && y1 === y2) return
|
|
57
|
+
|
|
58
|
+
let rx = Math.abs(rxIn)
|
|
59
|
+
let ry = Math.abs(ryIn)
|
|
60
|
+
|
|
61
|
+
const dx = (x1 - x2) / 2
|
|
62
|
+
const dy = (y1 - y2) / 2
|
|
63
|
+
// Simplified: ignore rotation (most icons use rotation=0)
|
|
64
|
+
const x1p = dx
|
|
65
|
+
const y1p = dy
|
|
66
|
+
|
|
67
|
+
// Correct radii
|
|
68
|
+
let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry)
|
|
69
|
+
if (lambda > 1) {
|
|
70
|
+
const s = Math.sqrt(lambda)
|
|
71
|
+
rx *= s
|
|
72
|
+
ry *= s
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rxSq = rx * rx
|
|
76
|
+
const rySq = ry * ry
|
|
77
|
+
const x1pSq = x1p * x1p
|
|
78
|
+
const y1pSq = y1p * y1p
|
|
79
|
+
|
|
80
|
+
let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq)
|
|
81
|
+
if (sq < 0) sq = 0
|
|
82
|
+
let root = Math.sqrt(sq)
|
|
83
|
+
if (largeArc === sweep) root = -root
|
|
84
|
+
|
|
85
|
+
const cxp = root * rx * y1p / ry
|
|
86
|
+
const cyp = -root * ry * x1p / rx
|
|
87
|
+
|
|
88
|
+
const cx = cxp + (x1 + x2) / 2
|
|
89
|
+
const cy = cyp + (y1 + y2) / 2
|
|
90
|
+
|
|
91
|
+
const angle = (ux: number, uy: number, vx: number, vy: number) => {
|
|
92
|
+
const n = Math.sqrt(ux * ux + uy * uy)
|
|
93
|
+
const d = Math.sqrt(vx * vx + vy * vy)
|
|
94
|
+
const c = (ux * vx + uy * vy) / (n * d)
|
|
95
|
+
const clamped = Math.max(-1, Math.min(1, c))
|
|
96
|
+
let a = Math.acos(clamped)
|
|
97
|
+
if (ux * vy - uy * vx < 0) a = -a
|
|
98
|
+
return a
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)
|
|
102
|
+
let dTheta = angle(
|
|
103
|
+
(x1p - cxp) / rx, (y1p - cyp) / ry,
|
|
104
|
+
(-x1p - cxp) / rx, (-y1p - cyp) / ry,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI
|
|
108
|
+
if (sweep && dTheta < 0) dTheta += 2 * Math.PI
|
|
109
|
+
|
|
110
|
+
// Split into segments of at most PI/2
|
|
111
|
+
const segments = Math.ceil(Math.abs(dTheta) / (Math.PI / 2))
|
|
112
|
+
const segAngle = dTheta / segments
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < segments; i++) {
|
|
115
|
+
const t1 = theta1 + i * segAngle
|
|
116
|
+
const t2 = t1 + segAngle
|
|
117
|
+
const alpha = Math.sin(segAngle) * (Math.sqrt(4 + 3 * Math.pow(Math.tan(segAngle / 2), 2)) - 1) / 3
|
|
118
|
+
|
|
119
|
+
const cos1 = Math.cos(t1), sin1 = Math.sin(t1)
|
|
120
|
+
const cos2 = Math.cos(t2), sin2 = Math.sin(t2)
|
|
121
|
+
|
|
122
|
+
const p1x = cx + rx * cos1
|
|
123
|
+
const p1y = cy + ry * sin1
|
|
124
|
+
const p2x = cx + rx * cos2
|
|
125
|
+
const p2y = cy + ry * sin2
|
|
126
|
+
|
|
127
|
+
const cp1x = p1x - alpha * rx * sin1
|
|
128
|
+
const cp1y = p1y + alpha * ry * cos1
|
|
129
|
+
const cp2x = p2x + alpha * rx * sin2
|
|
130
|
+
const cp2y = p2y - alpha * ry * cos2
|
|
131
|
+
|
|
132
|
+
path.cubicTo(cp1x, cp1y, cp2x, cp2y, p2x, p2y)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Try building a CanvasKit path manually by tokenizing the SVG path string.
|
|
138
|
+
* Handles edge cases that MakeFromSVGString may reject (e.g. missing spaces,
|
|
139
|
+
* numbers with leading dots like ".5", relative commands, arcs).
|
|
140
|
+
*/
|
|
141
|
+
export function tryManualPathParse(ck: CanvasKit, d: string): Path | null {
|
|
142
|
+
try {
|
|
143
|
+
const path = new ck.Path()
|
|
144
|
+
// Replace NaN/Infinity with 0 so commands keep their parameter count.
|
|
145
|
+
const cleaned = d.replace(/-?NaN/g, '0').replace(/-?Infinity/g, '0')
|
|
146
|
+
// Tokenize: split on commands and extract numbers
|
|
147
|
+
const tokens = cleaned.match(/[MLCQZAHVSmlcqzahvs]|[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g)
|
|
148
|
+
if (!tokens || tokens.length === 0) return null
|
|
149
|
+
|
|
150
|
+
let i = 0
|
|
151
|
+
let lastCmd = ''
|
|
152
|
+
let cx = 0, cy = 0 // current point
|
|
153
|
+
|
|
154
|
+
while (i < tokens.length) {
|
|
155
|
+
let cmd = tokens[i]
|
|
156
|
+
if (/^[MLCQZAHVSmlcqzahvs]$/.test(cmd)) {
|
|
157
|
+
lastCmd = cmd
|
|
158
|
+
i++
|
|
159
|
+
} else if (lastCmd) {
|
|
160
|
+
// Implicit repeat of last command (M becomes L after first pair)
|
|
161
|
+
cmd = lastCmd === 'M' ? 'L' : lastCmd === 'm' ? 'l' : lastCmd
|
|
162
|
+
} else {
|
|
163
|
+
i++
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const nums = (count: number): number[] => {
|
|
168
|
+
const result: number[] = []
|
|
169
|
+
for (let j = 0; j < count && i < tokens.length; j++) {
|
|
170
|
+
const n = parseFloat(tokens[i])
|
|
171
|
+
if (isNaN(n)) break
|
|
172
|
+
result.push(n)
|
|
173
|
+
i++
|
|
174
|
+
}
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
switch (cmd) {
|
|
179
|
+
case 'M': { const p = nums(2); if (p.length === 2) { path.moveTo(p[0], p[1]); cx = p[0]; cy = p[1]; lastCmd = 'L' } break }
|
|
180
|
+
case 'm': { const p = nums(2); if (p.length === 2) { path.moveTo(cx + p[0], cy + p[1]); cx += p[0]; cy += p[1]; lastCmd = 'l' } break }
|
|
181
|
+
case 'L': { const p = nums(2); if (p.length === 2) { path.lineTo(p[0], p[1]); cx = p[0]; cy = p[1] } break }
|
|
182
|
+
case 'l': { const p = nums(2); if (p.length === 2) { path.lineTo(cx + p[0], cy + p[1]); cx += p[0]; cy += p[1] } break }
|
|
183
|
+
case 'H': { const p = nums(1); if (p.length === 1) { path.lineTo(p[0], cy); cx = p[0] } break }
|
|
184
|
+
case 'h': { const p = nums(1); if (p.length === 1) { path.lineTo(cx + p[0], cy); cx += p[0] } break }
|
|
185
|
+
case 'V': { const p = nums(1); if (p.length === 1) { path.lineTo(cx, p[0]); cy = p[0] } break }
|
|
186
|
+
case 'v': { const p = nums(1); if (p.length === 1) { path.lineTo(cx, cy + p[0]); cy += p[0] } break }
|
|
187
|
+
case 'C': { const p = nums(6); if (p.length === 6) { path.cubicTo(p[0], p[1], p[2], p[3], p[4], p[5]); cx = p[4]; cy = p[5] } break }
|
|
188
|
+
case 'c': { const p = nums(6); if (p.length === 6) { path.cubicTo(cx+p[0], cy+p[1], cx+p[2], cy+p[3], cx+p[4], cy+p[5]); cx += p[4]; cy += p[5] } break }
|
|
189
|
+
case 'Q': { const p = nums(4); if (p.length === 4) { path.quadTo(p[0], p[1], p[2], p[3]); cx = p[2]; cy = p[3] } break }
|
|
190
|
+
case 'q': { const p = nums(4); if (p.length === 4) { path.quadTo(cx+p[0], cy+p[1], cx+p[2], cy+p[3]); cx += p[2]; cy += p[3] } break }
|
|
191
|
+
case 'S': { const p = nums(4); if (p.length === 4) { path.cubicTo(cx, cy, p[0], p[1], p[2], p[3]); cx = p[2]; cy = p[3] } break }
|
|
192
|
+
case 's': { const p = nums(4); if (p.length === 4) { path.cubicTo(cx, cy, cx+p[0], cy+p[1], cx+p[2], cy+p[3]); cx += p[2]; cy += p[3] } break }
|
|
193
|
+
case 'Z': case 'z': path.close(); break
|
|
194
|
+
case 'A': case 'a': {
|
|
195
|
+
// Arc: rx, ry, rotation, largeArc, sweep, x, y
|
|
196
|
+
const p = nums(7)
|
|
197
|
+
if (p.length === 7) {
|
|
198
|
+
const [rx, ry, , largeArc, sweep, ex, ey] = p
|
|
199
|
+
const endX = cmd === 'a' ? cx + ex : ex
|
|
200
|
+
const endY = cmd === 'a' ? cy + ey : ey
|
|
201
|
+
if (rx > 0 && ry > 0) {
|
|
202
|
+
arcToCubics(path, cx, cy, rx, ry, largeArc !== 0, sweep !== 0, endX, endY)
|
|
203
|
+
} else {
|
|
204
|
+
path.lineTo(endX, endY)
|
|
205
|
+
}
|
|
206
|
+
cx = endX
|
|
207
|
+
cy = endY
|
|
208
|
+
}
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
default: i++
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if path has any geometry
|
|
216
|
+
const bounds = path.getBounds()
|
|
217
|
+
if (bounds[2] - bounds[0] < 0.001 && bounds[3] - bounds[1] < 0.001) {
|
|
218
|
+
path.delete()
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
return path
|
|
222
|
+
} catch {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
}
|