@emasoft/svg-matrix 1.0.10 → 1.0.12
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/bin/svg-matrix.js +222 -123
- package/package.json +1 -1
- package/scripts/test-postinstall.js +93 -0
- package/src/flatten-pipeline.js +992 -0
- package/src/index.js +15 -4
- package/src/svg-parser.js +730 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten Pipeline - Comprehensive SVG flattening with all transform dependencies resolved
|
|
3
|
+
*
|
|
4
|
+
* A TRUE flattened SVG has:
|
|
5
|
+
* - NO transform attributes anywhere
|
|
6
|
+
* - NO clipPath references (boolean operations pre-applied)
|
|
7
|
+
* - NO mask references (converted to clipped geometry)
|
|
8
|
+
* - NO use/symbol references (instances pre-expanded)
|
|
9
|
+
* - NO pattern fill references (patterns pre-tiled)
|
|
10
|
+
* - NO marker references (markers pre-instantiated)
|
|
11
|
+
* - NO gradient transforms (gradientTransform pre-baked)
|
|
12
|
+
*
|
|
13
|
+
* @module flatten-pipeline
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import Decimal from 'decimal.js';
|
|
17
|
+
import { Matrix } from './matrix.js';
|
|
18
|
+
import * as Transforms2D from './transforms2d.js';
|
|
19
|
+
import * as SVGFlatten from './svg-flatten.js';
|
|
20
|
+
import * as ClipPathResolver from './clip-path-resolver.js';
|
|
21
|
+
import * as MaskResolver from './mask-resolver.js';
|
|
22
|
+
import * as UseSymbolResolver from './use-symbol-resolver.js';
|
|
23
|
+
import * as PatternResolver from './pattern-resolver.js';
|
|
24
|
+
import * as MarkerResolver from './marker-resolver.js';
|
|
25
|
+
import * as MeshGradient from './mesh-gradient.js';
|
|
26
|
+
import * as GeometryToPath from './geometry-to-path.js';
|
|
27
|
+
import { parseSVG, SVGElement, buildDefsMap, parseUrlReference, serializeSVG, findElementsWithAttribute } from './svg-parser.js';
|
|
28
|
+
import { Logger } from './logger.js';
|
|
29
|
+
|
|
30
|
+
Decimal.set({ precision: 80 });
|
|
31
|
+
|
|
32
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default options for flatten pipeline.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_OPTIONS = {
|
|
38
|
+
precision: 6, // Decimal places in output coordinates
|
|
39
|
+
curveSegments: 20, // Samples per curve for polygon conversion
|
|
40
|
+
resolveUse: true, // Expand <use> elements
|
|
41
|
+
resolveMarkers: true, // Expand marker instances
|
|
42
|
+
resolvePatterns: true, // Expand pattern fills to geometry
|
|
43
|
+
resolveMasks: true, // Convert masks to clip paths
|
|
44
|
+
resolveClipPaths: true, // Apply clipPath boolean operations
|
|
45
|
+
flattenTransforms: true, // Bake transform attributes into coordinates
|
|
46
|
+
bakeGradients: true, // Bake gradientTransform into gradient coords
|
|
47
|
+
removeUnusedDefs: true, // Remove defs that are no longer referenced
|
|
48
|
+
preserveIds: false, // Keep original IDs on expanded elements
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Flatten an SVG string completely - no transform dependencies remain.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} svgString - Raw SVG content
|
|
55
|
+
* @param {Object} [options] - Pipeline options
|
|
56
|
+
* @param {number} [options.precision=6] - Decimal places in output
|
|
57
|
+
* @param {number} [options.curveSegments=20] - Samples per curve
|
|
58
|
+
* @param {boolean} [options.resolveUse=true] - Expand use elements
|
|
59
|
+
* @param {boolean} [options.resolveMarkers=true] - Expand markers
|
|
60
|
+
* @param {boolean} [options.resolvePatterns=true] - Expand patterns
|
|
61
|
+
* @param {boolean} [options.resolveMasks=true] - Convert masks to clips
|
|
62
|
+
* @param {boolean} [options.resolveClipPaths=true] - Apply clipPath booleans
|
|
63
|
+
* @param {boolean} [options.flattenTransforms=true] - Bake transforms
|
|
64
|
+
* @param {boolean} [options.bakeGradients=true] - Bake gradient transforms
|
|
65
|
+
* @param {boolean} [options.removeUnusedDefs=true] - Clean up defs
|
|
66
|
+
* @returns {{svg: string, stats: Object}} Flattened SVG and statistics
|
|
67
|
+
*/
|
|
68
|
+
export function flattenSVG(svgString, options = {}) {
|
|
69
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
70
|
+
const stats = {
|
|
71
|
+
useResolved: 0,
|
|
72
|
+
markersResolved: 0,
|
|
73
|
+
patternsResolved: 0,
|
|
74
|
+
masksResolved: 0,
|
|
75
|
+
clipPathsApplied: 0,
|
|
76
|
+
transformsFlattened: 0,
|
|
77
|
+
gradientsProcessed: 0,
|
|
78
|
+
defsRemoved: 0,
|
|
79
|
+
errors: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Parse SVG
|
|
84
|
+
const root = parseSVG(svgString);
|
|
85
|
+
if (root.tagName !== 'svg') {
|
|
86
|
+
throw new Error('Root element must be <svg>');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build defs map
|
|
90
|
+
let defsMap = buildDefsMap(root);
|
|
91
|
+
|
|
92
|
+
// Step 1: Resolve <use> elements (must be first - creates new geometry)
|
|
93
|
+
if (opts.resolveUse) {
|
|
94
|
+
const result = resolveAllUseElements(root, defsMap, opts);
|
|
95
|
+
stats.useResolved = result.count;
|
|
96
|
+
stats.errors.push(...result.errors);
|
|
97
|
+
defsMap = buildDefsMap(root); // Rebuild after modifications
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 2: Resolve markers (adds geometry to paths)
|
|
101
|
+
if (opts.resolveMarkers) {
|
|
102
|
+
const result = resolveAllMarkers(root, defsMap, opts);
|
|
103
|
+
stats.markersResolved = result.count;
|
|
104
|
+
stats.errors.push(...result.errors);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 3: Resolve patterns (expand pattern fills)
|
|
108
|
+
if (opts.resolvePatterns) {
|
|
109
|
+
const result = resolveAllPatterns(root, defsMap, opts);
|
|
110
|
+
stats.patternsResolved = result.count;
|
|
111
|
+
stats.errors.push(...result.errors);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Step 4: Resolve masks (convert to clip geometry)
|
|
115
|
+
if (opts.resolveMasks) {
|
|
116
|
+
const result = resolveAllMasks(root, defsMap, opts);
|
|
117
|
+
stats.masksResolved = result.count;
|
|
118
|
+
stats.errors.push(...result.errors);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Step 5: Apply clipPaths (boolean intersection)
|
|
122
|
+
if (opts.resolveClipPaths) {
|
|
123
|
+
const result = applyAllClipPaths(root, defsMap, opts);
|
|
124
|
+
stats.clipPathsApplied = result.count;
|
|
125
|
+
stats.errors.push(...result.errors);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 6: Flatten transforms (bake into coordinates)
|
|
129
|
+
if (opts.flattenTransforms) {
|
|
130
|
+
const result = flattenAllTransforms(root, opts);
|
|
131
|
+
stats.transformsFlattened = result.count;
|
|
132
|
+
stats.errors.push(...result.errors);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Step 7: Bake gradient transforms
|
|
136
|
+
if (opts.bakeGradients) {
|
|
137
|
+
const result = bakeAllGradientTransforms(root, opts);
|
|
138
|
+
stats.gradientsProcessed = result.count;
|
|
139
|
+
stats.errors.push(...result.errors);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Step 8: Remove unused defs
|
|
143
|
+
if (opts.removeUnusedDefs) {
|
|
144
|
+
const result = removeUnusedDefinitions(root);
|
|
145
|
+
stats.defsRemoved = result.count;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Serialize back to SVG
|
|
149
|
+
const svg = serializeSVG(root);
|
|
150
|
+
|
|
151
|
+
return { svg, stats };
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
stats.errors.push(`Pipeline error: ${error.message}`);
|
|
155
|
+
return { svg: svgString, stats }; // Return original on failure
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// STEP 1: RESOLVE USE ELEMENTS
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve all <use> elements by expanding them inline.
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
function resolveAllUseElements(root, defsMap, opts) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
let count = 0;
|
|
170
|
+
|
|
171
|
+
const useElements = root.getElementsByTagName('use');
|
|
172
|
+
|
|
173
|
+
for (const useEl of [...useElements]) { // Clone array since we modify DOM
|
|
174
|
+
try {
|
|
175
|
+
const href = useEl.getAttribute('href') || useEl.getAttribute('xlink:href');
|
|
176
|
+
if (!href) continue;
|
|
177
|
+
|
|
178
|
+
const refId = href.replace(/^#/, '');
|
|
179
|
+
const refEl = defsMap.get(refId);
|
|
180
|
+
|
|
181
|
+
if (!refEl) {
|
|
182
|
+
errors.push(`use: referenced element #${refId} not found`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Parse use element data
|
|
187
|
+
const useData = UseSymbolResolver.parseUseElement(useEl);
|
|
188
|
+
|
|
189
|
+
// Resolve the use
|
|
190
|
+
const resolved = UseSymbolResolver.resolveUse(useData, Object.fromEntries(defsMap), {
|
|
191
|
+
samples: opts.curveSegments
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!resolved) {
|
|
195
|
+
errors.push(`use: failed to resolve #${refId}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Convert resolved to path data
|
|
200
|
+
const pathData = UseSymbolResolver.resolvedUseToPathData(resolved, opts.curveSegments);
|
|
201
|
+
|
|
202
|
+
if (pathData) {
|
|
203
|
+
// Create new path element to replace <use>
|
|
204
|
+
const pathEl = new SVGElement('path', {
|
|
205
|
+
d: pathData,
|
|
206
|
+
...extractPresentationAttrs(useEl)
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Copy style attributes from resolved
|
|
210
|
+
if (resolved.style) {
|
|
211
|
+
for (const [key, val] of Object.entries(resolved.style)) {
|
|
212
|
+
if (val && !pathEl.hasAttribute(key)) {
|
|
213
|
+
pathEl.setAttribute(key, val);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Replace use with path
|
|
219
|
+
if (useEl.parentNode) {
|
|
220
|
+
useEl.parentNode.replaceChild(pathEl, useEl);
|
|
221
|
+
count++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
errors.push(`use: ${e.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { count, errors };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// STEP 2: RESOLVE MARKERS
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve all marker references by instantiating marker geometry.
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
function resolveAllMarkers(root, defsMap, opts) {
|
|
241
|
+
const errors = [];
|
|
242
|
+
let count = 0;
|
|
243
|
+
|
|
244
|
+
// Find all elements with marker attributes
|
|
245
|
+
const markerAttrs = ['marker-start', 'marker-mid', 'marker-end', 'marker'];
|
|
246
|
+
|
|
247
|
+
for (const attrName of markerAttrs) {
|
|
248
|
+
const elements = findElementsWithAttribute(root, attrName);
|
|
249
|
+
|
|
250
|
+
for (const el of elements) {
|
|
251
|
+
if (el.tagName !== 'path' && el.tagName !== 'line' && el.tagName !== 'polyline' && el.tagName !== 'polygon') {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Resolve markers for this element
|
|
257
|
+
const markerInstances = MarkerResolver.resolveMarkers(el, Object.fromEntries(defsMap));
|
|
258
|
+
|
|
259
|
+
if (!markerInstances || markerInstances.length === 0) continue;
|
|
260
|
+
|
|
261
|
+
// Convert markers to path data
|
|
262
|
+
const markerPathData = MarkerResolver.markersToPathData(markerInstances, opts.precision);
|
|
263
|
+
|
|
264
|
+
if (markerPathData) {
|
|
265
|
+
// Create new path element for marker geometry
|
|
266
|
+
const markerPath = new SVGElement('path', {
|
|
267
|
+
d: markerPathData,
|
|
268
|
+
fill: el.getAttribute('stroke') || 'currentColor', // Markers typically use stroke color
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Insert after the original element
|
|
272
|
+
if (el.parentNode) {
|
|
273
|
+
const nextSibling = el.nextSibling;
|
|
274
|
+
if (nextSibling) {
|
|
275
|
+
el.parentNode.insertBefore(markerPath, nextSibling);
|
|
276
|
+
} else {
|
|
277
|
+
el.parentNode.appendChild(markerPath);
|
|
278
|
+
}
|
|
279
|
+
count++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Remove marker attributes from original element
|
|
284
|
+
for (const attr of markerAttrs) {
|
|
285
|
+
el.removeAttribute(attr);
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
errors.push(`marker: ${e.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { count, errors };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// STEP 3: RESOLVE PATTERNS
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Resolve pattern fills by expanding to tiled geometry.
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
function resolveAllPatterns(root, defsMap, opts) {
|
|
305
|
+
const errors = [];
|
|
306
|
+
let count = 0;
|
|
307
|
+
|
|
308
|
+
const elementsWithFill = findElementsWithAttribute(root, 'fill');
|
|
309
|
+
|
|
310
|
+
for (const el of elementsWithFill) {
|
|
311
|
+
const fill = el.getAttribute('fill');
|
|
312
|
+
if (!fill || !fill.includes('url(')) continue;
|
|
313
|
+
|
|
314
|
+
const refId = parseUrlReference(fill);
|
|
315
|
+
if (!refId) continue;
|
|
316
|
+
|
|
317
|
+
const patternEl = defsMap.get(refId);
|
|
318
|
+
if (!patternEl || patternEl.tagName !== 'pattern') continue;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
// Get element bounding box (approximate from path data or attributes)
|
|
322
|
+
const bbox = getElementBBox(el);
|
|
323
|
+
if (!bbox) continue;
|
|
324
|
+
|
|
325
|
+
// Parse pattern
|
|
326
|
+
const patternData = PatternResolver.parsePatternElement(patternEl);
|
|
327
|
+
|
|
328
|
+
// Resolve pattern to path data
|
|
329
|
+
const patternPathData = PatternResolver.patternToPathData(patternData, bbox, {
|
|
330
|
+
samples: opts.curveSegments
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (patternPathData) {
|
|
334
|
+
// Create group with clipped pattern geometry
|
|
335
|
+
const patternGroup = new SVGElement('g', {});
|
|
336
|
+
|
|
337
|
+
const patternPath = new SVGElement('path', {
|
|
338
|
+
d: patternPathData,
|
|
339
|
+
fill: '#000', // Pattern content typically has its own fill
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
patternGroup.appendChild(patternPath);
|
|
343
|
+
|
|
344
|
+
// Replace fill with pattern geometry (clip to original shape)
|
|
345
|
+
el.setAttribute('fill', 'none');
|
|
346
|
+
el.setAttribute('stroke', el.getAttribute('stroke') || 'none');
|
|
347
|
+
|
|
348
|
+
if (el.parentNode) {
|
|
349
|
+
el.parentNode.insertBefore(patternGroup, el);
|
|
350
|
+
count++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
errors.push(`pattern: ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { count, errors };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// STEP 4: RESOLVE MASKS
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Resolve mask references by converting to clip geometry.
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
function resolveAllMasks(root, defsMap, opts) {
|
|
370
|
+
const errors = [];
|
|
371
|
+
let count = 0;
|
|
372
|
+
|
|
373
|
+
const elementsWithMask = findElementsWithAttribute(root, 'mask');
|
|
374
|
+
|
|
375
|
+
for (const el of elementsWithMask) {
|
|
376
|
+
const maskRef = el.getAttribute('mask');
|
|
377
|
+
if (!maskRef || !maskRef.includes('url(')) continue;
|
|
378
|
+
|
|
379
|
+
const refId = parseUrlReference(maskRef);
|
|
380
|
+
if (!refId) continue;
|
|
381
|
+
|
|
382
|
+
const maskEl = defsMap.get(refId);
|
|
383
|
+
if (!maskEl || maskEl.tagName !== 'mask') continue;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// Get element bounding box
|
|
387
|
+
const bbox = getElementBBox(el);
|
|
388
|
+
if (!bbox) continue;
|
|
389
|
+
|
|
390
|
+
// Parse mask
|
|
391
|
+
const maskData = MaskResolver.parseMaskElement(maskEl);
|
|
392
|
+
|
|
393
|
+
// Convert mask to clip path data
|
|
394
|
+
const clipPathData = MaskResolver.maskToPathData(maskData, bbox, {
|
|
395
|
+
samples: opts.curveSegments,
|
|
396
|
+
opacityThreshold: 0.5
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (clipPathData) {
|
|
400
|
+
// Get original element's path data
|
|
401
|
+
const origPathData = getElementPathData(el, opts.precision);
|
|
402
|
+
|
|
403
|
+
if (origPathData) {
|
|
404
|
+
// Perform boolean intersection
|
|
405
|
+
const origPolygon = ClipPathResolver.pathToPolygon(origPathData, opts.curveSegments);
|
|
406
|
+
const clipPolygon = ClipPathResolver.pathToPolygon(clipPathData, opts.curveSegments);
|
|
407
|
+
|
|
408
|
+
// Apply clip (intersection)
|
|
409
|
+
const clippedPolygon = intersectPolygons(origPolygon, clipPolygon);
|
|
410
|
+
|
|
411
|
+
if (clippedPolygon && clippedPolygon.length > 2) {
|
|
412
|
+
const clippedPath = ClipPathResolver.polygonToPathData(clippedPolygon, opts.precision);
|
|
413
|
+
el.setAttribute('d', clippedPath);
|
|
414
|
+
el.removeAttribute('mask');
|
|
415
|
+
count++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
errors.push(`mask: ${e.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { count, errors };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// STEP 5: APPLY CLIP PATHS
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Apply clipPath references by performing boolean intersection.
|
|
433
|
+
* @private
|
|
434
|
+
*/
|
|
435
|
+
function applyAllClipPaths(root, defsMap, opts) {
|
|
436
|
+
const errors = [];
|
|
437
|
+
let count = 0;
|
|
438
|
+
|
|
439
|
+
const elementsWithClip = findElementsWithAttribute(root, 'clip-path');
|
|
440
|
+
|
|
441
|
+
for (const el of elementsWithClip) {
|
|
442
|
+
const clipRef = el.getAttribute('clip-path');
|
|
443
|
+
if (!clipRef || !clipRef.includes('url(')) continue;
|
|
444
|
+
|
|
445
|
+
const refId = parseUrlReference(clipRef);
|
|
446
|
+
if (!refId) continue;
|
|
447
|
+
|
|
448
|
+
const clipPathEl = defsMap.get(refId);
|
|
449
|
+
if (!clipPathEl || clipPathEl.tagName !== 'clippath') continue;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Get element path data
|
|
453
|
+
const origPathData = getElementPathData(el, opts.precision);
|
|
454
|
+
if (!origPathData) continue;
|
|
455
|
+
|
|
456
|
+
// Get clip path data from clipPath element's children
|
|
457
|
+
let clipPathData = '';
|
|
458
|
+
for (const child of clipPathEl.children) {
|
|
459
|
+
if (child instanceof SVGElement) {
|
|
460
|
+
const childPath = getElementPathData(child, opts.precision);
|
|
461
|
+
if (childPath) {
|
|
462
|
+
clipPathData += (clipPathData ? ' ' : '') + childPath;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!clipPathData) continue;
|
|
468
|
+
|
|
469
|
+
// Convert to polygons
|
|
470
|
+
const origPolygon = ClipPathResolver.pathToPolygon(origPathData, opts.curveSegments);
|
|
471
|
+
const clipPolygon = ClipPathResolver.pathToPolygon(clipPathData, opts.curveSegments);
|
|
472
|
+
|
|
473
|
+
// Perform intersection
|
|
474
|
+
const clippedPolygon = intersectPolygons(origPolygon, clipPolygon);
|
|
475
|
+
|
|
476
|
+
if (clippedPolygon && clippedPolygon.length > 2) {
|
|
477
|
+
const clippedPath = ClipPathResolver.polygonToPathData(clippedPolygon, opts.precision);
|
|
478
|
+
|
|
479
|
+
// Update element
|
|
480
|
+
if (el.tagName === 'path') {
|
|
481
|
+
el.setAttribute('d', clippedPath);
|
|
482
|
+
} else {
|
|
483
|
+
// Convert shape to path
|
|
484
|
+
const newPath = new SVGElement('path', {
|
|
485
|
+
d: clippedPath,
|
|
486
|
+
...extractPresentationAttrs(el)
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
if (el.parentNode) {
|
|
490
|
+
el.parentNode.replaceChild(newPath, el);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
el.removeAttribute('clip-path');
|
|
495
|
+
count++;
|
|
496
|
+
}
|
|
497
|
+
} catch (e) {
|
|
498
|
+
errors.push(`clipPath: ${e.message}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { count, errors };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// STEP 6: FLATTEN TRANSFORMS
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Flatten all transform attributes by baking into coordinates.
|
|
511
|
+
* @private
|
|
512
|
+
*/
|
|
513
|
+
function flattenAllTransforms(root, opts) {
|
|
514
|
+
const errors = [];
|
|
515
|
+
let count = 0;
|
|
516
|
+
|
|
517
|
+
const elementsWithTransform = findElementsWithAttribute(root, 'transform');
|
|
518
|
+
|
|
519
|
+
for (const el of elementsWithTransform) {
|
|
520
|
+
const transform = el.getAttribute('transform');
|
|
521
|
+
if (!transform) continue;
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
// Parse transform to matrix
|
|
525
|
+
const ctm = SVGFlatten.parseTransformAttribute(transform);
|
|
526
|
+
|
|
527
|
+
// Get element path data
|
|
528
|
+
const pathData = getElementPathData(el, opts.precision);
|
|
529
|
+
if (!pathData) {
|
|
530
|
+
// For groups, propagate transform to children
|
|
531
|
+
if (el.tagName === 'g') {
|
|
532
|
+
propagateTransformToChildren(el, ctm, opts);
|
|
533
|
+
el.removeAttribute('transform');
|
|
534
|
+
count++;
|
|
535
|
+
}
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Transform the path data
|
|
540
|
+
const transformedPath = SVGFlatten.transformPathData(pathData, ctm, { precision: opts.precision });
|
|
541
|
+
|
|
542
|
+
// Update or replace element
|
|
543
|
+
if (el.tagName === 'path') {
|
|
544
|
+
el.setAttribute('d', transformedPath);
|
|
545
|
+
} else {
|
|
546
|
+
// Convert shape to path with transformed coordinates
|
|
547
|
+
const newPath = new SVGElement('path', {
|
|
548
|
+
d: transformedPath,
|
|
549
|
+
...extractPresentationAttrs(el)
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Remove shape-specific attributes
|
|
553
|
+
for (const attr of getShapeSpecificAttrs(el.tagName)) {
|
|
554
|
+
newPath.removeAttribute(attr);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (el.parentNode) {
|
|
558
|
+
el.parentNode.replaceChild(newPath, el);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
el.removeAttribute('transform');
|
|
563
|
+
count++;
|
|
564
|
+
} catch (e) {
|
|
565
|
+
errors.push(`transform: ${e.message}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { count, errors };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Propagate transform to all children of a group.
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
function propagateTransformToChildren(group, ctm, opts) {
|
|
577
|
+
for (const child of [...group.children]) {
|
|
578
|
+
if (!(child instanceof SVGElement)) continue;
|
|
579
|
+
|
|
580
|
+
if (child.tagName === 'g') {
|
|
581
|
+
// Nested group - compose transforms
|
|
582
|
+
const childTransform = child.getAttribute('transform');
|
|
583
|
+
if (childTransform) {
|
|
584
|
+
const childCtm = SVGFlatten.parseTransformAttribute(childTransform);
|
|
585
|
+
const combined = ctm.mul(childCtm);
|
|
586
|
+
child.setAttribute('transform', matrixToTransform(combined));
|
|
587
|
+
} else {
|
|
588
|
+
child.setAttribute('transform', matrixToTransform(ctm));
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// Shape or path - apply transform to coordinates
|
|
592
|
+
const pathData = getElementPathData(child, opts.precision);
|
|
593
|
+
if (pathData) {
|
|
594
|
+
// Compose with any existing transform
|
|
595
|
+
const childTransform = child.getAttribute('transform');
|
|
596
|
+
let combinedCtm = ctm;
|
|
597
|
+
if (childTransform) {
|
|
598
|
+
const childCtm = SVGFlatten.parseTransformAttribute(childTransform);
|
|
599
|
+
combinedCtm = ctm.mul(childCtm);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const transformedPath = SVGFlatten.transformPathData(pathData, combinedCtm, { precision: opts.precision });
|
|
603
|
+
|
|
604
|
+
if (child.tagName === 'path') {
|
|
605
|
+
child.setAttribute('d', transformedPath);
|
|
606
|
+
} else {
|
|
607
|
+
// Replace with path element
|
|
608
|
+
const newPath = new SVGElement('path', {
|
|
609
|
+
d: transformedPath,
|
|
610
|
+
...extractPresentationAttrs(child)
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
group.replaceChild(newPath, child);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
child.removeAttribute('transform');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================================================
|
|
623
|
+
// STEP 7: BAKE GRADIENT TRANSFORMS
|
|
624
|
+
// ============================================================================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Bake gradientTransform into gradient coordinates.
|
|
628
|
+
* @private
|
|
629
|
+
*/
|
|
630
|
+
function bakeAllGradientTransforms(root, opts) {
|
|
631
|
+
const errors = [];
|
|
632
|
+
let count = 0;
|
|
633
|
+
|
|
634
|
+
// Process linearGradient elements
|
|
635
|
+
const linearGradients = root.getElementsByTagName('linearGradient');
|
|
636
|
+
for (const grad of linearGradients) {
|
|
637
|
+
const gradientTransform = grad.getAttribute('gradientTransform');
|
|
638
|
+
if (!gradientTransform) continue;
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
const ctm = SVGFlatten.parseTransformAttribute(gradientTransform);
|
|
642
|
+
|
|
643
|
+
// Transform x1,y1,x2,y2
|
|
644
|
+
const x1 = parseFloat(grad.getAttribute('x1') || '0');
|
|
645
|
+
const y1 = parseFloat(grad.getAttribute('y1') || '0');
|
|
646
|
+
const x2 = parseFloat(grad.getAttribute('x2') || '1');
|
|
647
|
+
const y2 = parseFloat(grad.getAttribute('y2') || '0');
|
|
648
|
+
|
|
649
|
+
const [tx1, ty1] = Transforms2D.applyTransform(ctm, x1, y1);
|
|
650
|
+
const [tx2, ty2] = Transforms2D.applyTransform(ctm, x2, y2);
|
|
651
|
+
|
|
652
|
+
grad.setAttribute('x1', tx1.toFixed(opts.precision));
|
|
653
|
+
grad.setAttribute('y1', ty1.toFixed(opts.precision));
|
|
654
|
+
grad.setAttribute('x2', tx2.toFixed(opts.precision));
|
|
655
|
+
grad.setAttribute('y2', ty2.toFixed(opts.precision));
|
|
656
|
+
grad.removeAttribute('gradientTransform');
|
|
657
|
+
count++;
|
|
658
|
+
} catch (e) {
|
|
659
|
+
errors.push(`linearGradient: ${e.message}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Process radialGradient elements
|
|
664
|
+
const radialGradients = root.getElementsByTagName('radialGradient');
|
|
665
|
+
for (const grad of radialGradients) {
|
|
666
|
+
const gradientTransform = grad.getAttribute('gradientTransform');
|
|
667
|
+
if (!gradientTransform) continue;
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const ctm = SVGFlatten.parseTransformAttribute(gradientTransform);
|
|
671
|
+
|
|
672
|
+
// Transform cx,cy,fx,fy and scale r
|
|
673
|
+
const cx = parseFloat(grad.getAttribute('cx') || '0.5');
|
|
674
|
+
const cy = parseFloat(grad.getAttribute('cy') || '0.5');
|
|
675
|
+
const fx = parseFloat(grad.getAttribute('fx') || cx.toString());
|
|
676
|
+
const fy = parseFloat(grad.getAttribute('fy') || cy.toString());
|
|
677
|
+
const r = parseFloat(grad.getAttribute('r') || '0.5');
|
|
678
|
+
|
|
679
|
+
const [tcx, tcy] = Transforms2D.applyTransform(ctm, cx, cy);
|
|
680
|
+
const [tfx, tfy] = Transforms2D.applyTransform(ctm, fx, fy);
|
|
681
|
+
|
|
682
|
+
// Scale radius by average scale factor
|
|
683
|
+
const scale = Math.sqrt(Math.abs(ctm.data[0][0].toNumber() * ctm.data[1][1].toNumber()));
|
|
684
|
+
const tr = r * scale;
|
|
685
|
+
|
|
686
|
+
grad.setAttribute('cx', tcx.toFixed(opts.precision));
|
|
687
|
+
grad.setAttribute('cy', tcy.toFixed(opts.precision));
|
|
688
|
+
grad.setAttribute('fx', tfx.toFixed(opts.precision));
|
|
689
|
+
grad.setAttribute('fy', tfy.toFixed(opts.precision));
|
|
690
|
+
grad.setAttribute('r', tr.toFixed(opts.precision));
|
|
691
|
+
grad.removeAttribute('gradientTransform');
|
|
692
|
+
count++;
|
|
693
|
+
} catch (e) {
|
|
694
|
+
errors.push(`radialGradient: ${e.message}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return { count, errors };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// STEP 8: REMOVE UNUSED DEFS
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Remove defs that are no longer referenced.
|
|
707
|
+
* @private
|
|
708
|
+
*/
|
|
709
|
+
function removeUnusedDefinitions(root) {
|
|
710
|
+
let count = 0;
|
|
711
|
+
|
|
712
|
+
// Collect all url() references in the document
|
|
713
|
+
const usedIds = new Set();
|
|
714
|
+
|
|
715
|
+
const collectReferences = (el) => {
|
|
716
|
+
for (const attrName of el.getAttributeNames()) {
|
|
717
|
+
const val = el.getAttribute(attrName);
|
|
718
|
+
if (val && val.includes('url(')) {
|
|
719
|
+
const refId = parseUrlReference(val);
|
|
720
|
+
if (refId) usedIds.add(refId);
|
|
721
|
+
}
|
|
722
|
+
if (attrName === 'href' || attrName === 'xlink:href') {
|
|
723
|
+
const refId = val?.replace(/^#/, '');
|
|
724
|
+
if (refId) usedIds.add(refId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for (const child of el.children) {
|
|
729
|
+
if (child instanceof SVGElement) {
|
|
730
|
+
collectReferences(child);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
collectReferences(root);
|
|
736
|
+
|
|
737
|
+
// Remove unreferenced defs
|
|
738
|
+
const defsElements = root.getElementsByTagName('defs');
|
|
739
|
+
for (const defs of defsElements) {
|
|
740
|
+
for (const child of [...defs.children]) {
|
|
741
|
+
if (child instanceof SVGElement) {
|
|
742
|
+
const id = child.getAttribute('id');
|
|
743
|
+
if (id && !usedIds.has(id)) {
|
|
744
|
+
defs.removeChild(child);
|
|
745
|
+
count++;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return { count };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ============================================================================
|
|
755
|
+
// UTILITY FUNCTIONS
|
|
756
|
+
// ============================================================================
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get path data from any shape element.
|
|
760
|
+
* @private
|
|
761
|
+
*/
|
|
762
|
+
function getElementPathData(el, precision) {
|
|
763
|
+
const tagName = el.tagName.toLowerCase();
|
|
764
|
+
|
|
765
|
+
if (tagName === 'path') {
|
|
766
|
+
return el.getAttribute('d');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Use GeometryToPath for shape conversion
|
|
770
|
+
const getAttr = (name, def = 0) => {
|
|
771
|
+
const val = el.getAttribute(name);
|
|
772
|
+
return val !== null ? parseFloat(val) : def;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
switch (tagName) {
|
|
776
|
+
case 'rect':
|
|
777
|
+
return GeometryToPath.rectToPathData(
|
|
778
|
+
getAttr('x'), getAttr('y'),
|
|
779
|
+
getAttr('width'), getAttr('height'),
|
|
780
|
+
getAttr('rx'), getAttr('ry') || null,
|
|
781
|
+
false, precision
|
|
782
|
+
);
|
|
783
|
+
case 'circle':
|
|
784
|
+
return GeometryToPath.circleToPathData(
|
|
785
|
+
getAttr('cx'), getAttr('cy'), getAttr('r'), precision
|
|
786
|
+
);
|
|
787
|
+
case 'ellipse':
|
|
788
|
+
return GeometryToPath.ellipseToPathData(
|
|
789
|
+
getAttr('cx'), getAttr('cy'),
|
|
790
|
+
getAttr('rx'), getAttr('ry'), precision
|
|
791
|
+
);
|
|
792
|
+
case 'line':
|
|
793
|
+
return GeometryToPath.lineToPathData(
|
|
794
|
+
getAttr('x1'), getAttr('y1'),
|
|
795
|
+
getAttr('x2'), getAttr('y2'), precision
|
|
796
|
+
);
|
|
797
|
+
case 'polyline':
|
|
798
|
+
return GeometryToPath.polylineToPathData(
|
|
799
|
+
el.getAttribute('points') || '', precision
|
|
800
|
+
);
|
|
801
|
+
case 'polygon':
|
|
802
|
+
return GeometryToPath.polygonToPathData(
|
|
803
|
+
el.getAttribute('points') || '', precision
|
|
804
|
+
);
|
|
805
|
+
default:
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get approximate bounding box of an element.
|
|
812
|
+
* @private
|
|
813
|
+
*/
|
|
814
|
+
function getElementBBox(el) {
|
|
815
|
+
const pathData = getElementPathData(el, 6);
|
|
816
|
+
if (!pathData) return null;
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
const polygon = ClipPathResolver.pathToPolygon(pathData, 10);
|
|
820
|
+
if (!polygon || polygon.length === 0) return null;
|
|
821
|
+
|
|
822
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
823
|
+
for (const pt of polygon) {
|
|
824
|
+
const x = pt.x instanceof Decimal ? pt.x.toNumber() : pt.x;
|
|
825
|
+
const y = pt.y instanceof Decimal ? pt.y.toNumber() : pt.y;
|
|
826
|
+
if (x < minX) minX = x;
|
|
827
|
+
if (y < minY) minY = y;
|
|
828
|
+
if (x > maxX) maxX = x;
|
|
829
|
+
if (y > maxY) maxY = y;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Extract presentation attributes from element.
|
|
840
|
+
* @private
|
|
841
|
+
*/
|
|
842
|
+
function extractPresentationAttrs(el) {
|
|
843
|
+
const presentationAttrs = [
|
|
844
|
+
'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
|
|
845
|
+
'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'stroke-opacity',
|
|
846
|
+
'fill-opacity', 'opacity', 'fill-rule', 'clip-rule', 'visibility', 'display',
|
|
847
|
+
'color', 'font-family', 'font-size', 'font-weight', 'font-style',
|
|
848
|
+
'text-anchor', 'dominant-baseline', 'class', 'style'
|
|
849
|
+
];
|
|
850
|
+
|
|
851
|
+
const attrs = {};
|
|
852
|
+
for (const name of presentationAttrs) {
|
|
853
|
+
const val = el.getAttribute(name);
|
|
854
|
+
if (val !== null) {
|
|
855
|
+
attrs[name] = val;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return attrs;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Get shape-specific attribute names.
|
|
863
|
+
* @private
|
|
864
|
+
*/
|
|
865
|
+
function getShapeSpecificAttrs(tagName) {
|
|
866
|
+
const attrs = {
|
|
867
|
+
rect: ['x', 'y', 'width', 'height', 'rx', 'ry'],
|
|
868
|
+
circle: ['cx', 'cy', 'r'],
|
|
869
|
+
ellipse: ['cx', 'cy', 'rx', 'ry'],
|
|
870
|
+
line: ['x1', 'y1', 'x2', 'y2'],
|
|
871
|
+
polyline: ['points'],
|
|
872
|
+
polygon: ['points'],
|
|
873
|
+
};
|
|
874
|
+
return attrs[tagName.toLowerCase()] || [];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Convert matrix to transform attribute string.
|
|
879
|
+
* @private
|
|
880
|
+
*/
|
|
881
|
+
function matrixToTransform(matrix) {
|
|
882
|
+
const a = matrix.data[0][0].toNumber();
|
|
883
|
+
const b = matrix.data[1][0].toNumber();
|
|
884
|
+
const c = matrix.data[0][1].toNumber();
|
|
885
|
+
const d = matrix.data[1][1].toNumber();
|
|
886
|
+
const e = matrix.data[0][2].toNumber();
|
|
887
|
+
const f = matrix.data[1][2].toNumber();
|
|
888
|
+
return `matrix(${a} ${b} ${c} ${d} ${e} ${f})`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Intersect two polygons using Sutherland-Hodgman algorithm.
|
|
893
|
+
* @private
|
|
894
|
+
*/
|
|
895
|
+
function intersectPolygons(subject, clip) {
|
|
896
|
+
// Use PolygonClip if available, otherwise simple implementation
|
|
897
|
+
try {
|
|
898
|
+
const PolygonClip = require('./polygon-clip.js');
|
|
899
|
+
if (PolygonClip.intersect) {
|
|
900
|
+
return PolygonClip.intersect(subject, clip);
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
// Fall through to simple implementation
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Simple convex clip implementation
|
|
907
|
+
if (!subject || subject.length < 3 || !clip || clip.length < 3) {
|
|
908
|
+
return subject;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let output = [...subject];
|
|
912
|
+
|
|
913
|
+
for (let i = 0; i < clip.length; i++) {
|
|
914
|
+
if (output.length === 0) break;
|
|
915
|
+
|
|
916
|
+
const input = output;
|
|
917
|
+
output = [];
|
|
918
|
+
|
|
919
|
+
const edgeStart = clip[i];
|
|
920
|
+
const edgeEnd = clip[(i + 1) % clip.length];
|
|
921
|
+
|
|
922
|
+
for (let j = 0; j < input.length; j++) {
|
|
923
|
+
const current = input[j];
|
|
924
|
+
const next = input[(j + 1) % input.length];
|
|
925
|
+
|
|
926
|
+
const currentInside = isInsideEdge(current, edgeStart, edgeEnd);
|
|
927
|
+
const nextInside = isInsideEdge(next, edgeStart, edgeEnd);
|
|
928
|
+
|
|
929
|
+
if (currentInside) {
|
|
930
|
+
output.push(current);
|
|
931
|
+
if (!nextInside) {
|
|
932
|
+
output.push(lineIntersect(current, next, edgeStart, edgeEnd));
|
|
933
|
+
}
|
|
934
|
+
} else if (nextInside) {
|
|
935
|
+
output.push(lineIntersect(current, next, edgeStart, edgeEnd));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return output;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Check if point is inside edge (left side).
|
|
945
|
+
* @private
|
|
946
|
+
*/
|
|
947
|
+
function isInsideEdge(point, edgeStart, edgeEnd) {
|
|
948
|
+
const px = point.x instanceof Decimal ? point.x.toNumber() : point.x;
|
|
949
|
+
const py = point.y instanceof Decimal ? point.y.toNumber() : point.y;
|
|
950
|
+
const sx = edgeStart.x instanceof Decimal ? edgeStart.x.toNumber() : edgeStart.x;
|
|
951
|
+
const sy = edgeStart.y instanceof Decimal ? edgeStart.y.toNumber() : edgeStart.y;
|
|
952
|
+
const ex = edgeEnd.x instanceof Decimal ? edgeEnd.x.toNumber() : edgeEnd.x;
|
|
953
|
+
const ey = edgeEnd.y instanceof Decimal ? edgeEnd.y.toNumber() : edgeEnd.y;
|
|
954
|
+
|
|
955
|
+
return (ex - sx) * (py - sy) - (ey - sy) * (px - sx) >= 0;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Find intersection point of two lines.
|
|
960
|
+
* @private
|
|
961
|
+
*/
|
|
962
|
+
function lineIntersect(p1, p2, p3, p4) {
|
|
963
|
+
const x1 = p1.x instanceof Decimal ? p1.x.toNumber() : p1.x;
|
|
964
|
+
const y1 = p1.y instanceof Decimal ? p1.y.toNumber() : p1.y;
|
|
965
|
+
const x2 = p2.x instanceof Decimal ? p2.x.toNumber() : p2.x;
|
|
966
|
+
const y2 = p2.y instanceof Decimal ? p2.y.toNumber() : p2.y;
|
|
967
|
+
const x3 = p3.x instanceof Decimal ? p3.x.toNumber() : p3.x;
|
|
968
|
+
const y3 = p3.y instanceof Decimal ? p3.y.toNumber() : p3.y;
|
|
969
|
+
const x4 = p4.x instanceof Decimal ? p4.x.toNumber() : p4.x;
|
|
970
|
+
const y4 = p4.y instanceof Decimal ? p4.y.toNumber() : p4.y;
|
|
971
|
+
|
|
972
|
+
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
973
|
+
if (Math.abs(denom) < 1e-10) {
|
|
974
|
+
return { x: D((x1 + x2) / 2), y: D((y1 + y2) / 2) };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
x: D(x1 + t * (x2 - x1)),
|
|
981
|
+
y: D(y1 + t * (y2 - y1))
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ============================================================================
|
|
986
|
+
// EXPORTS
|
|
987
|
+
// ============================================================================
|
|
988
|
+
|
|
989
|
+
export default {
|
|
990
|
+
flattenSVG,
|
|
991
|
+
DEFAULT_OPTIONS
|
|
992
|
+
};
|