@emasoft/svg-matrix 1.0.19 → 1.0.20
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Rendering Context - Tracks ALL SVG properties affecting rendered geometry
|
|
3
|
+
*
|
|
4
|
+
* This module provides a comprehensive representation of SVG rendering properties
|
|
5
|
+
* that affect the actual visible/filled area of an element. Every function that
|
|
6
|
+
* operates on SVG geometry MUST use this context to account for:
|
|
7
|
+
*
|
|
8
|
+
* 1. Fill properties (fill-rule, fill-opacity)
|
|
9
|
+
* 2. Stroke properties (width, linecap, linejoin, miterlimit, dasharray, dashoffset)
|
|
10
|
+
* 3. Markers (marker-start, marker-mid, marker-end)
|
|
11
|
+
* 4. Paint order (fill, stroke, markers rendering order)
|
|
12
|
+
* 5. Clipping (clip-path, clip-rule)
|
|
13
|
+
* 6. Masking (mask, mask-type)
|
|
14
|
+
* 7. Opacity (opacity, fill-opacity, stroke-opacity)
|
|
15
|
+
* 8. Transforms (transform, vector-effect)
|
|
16
|
+
* 9. Filters (filter)
|
|
17
|
+
*
|
|
18
|
+
* ## Critical Insight
|
|
19
|
+
*
|
|
20
|
+
* SVG boolean operations, collision detection, off-canvas detection, and path
|
|
21
|
+
* merging must ALL account for the RENDERED AREA, not just path geometry.
|
|
22
|
+
*
|
|
23
|
+
* A path with stroke-width: 50 extends 25 units beyond its geometric boundary.
|
|
24
|
+
* A path with markers has additional geometry at vertices.
|
|
25
|
+
* A path with evenodd fill-rule may have holes that don't participate in collisions.
|
|
26
|
+
*
|
|
27
|
+
* @module svg-rendering-context
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import Decimal from 'decimal.js';
|
|
31
|
+
import { FillRule, pointInPolygonWithRule, offsetPolygon, strokeToFilledPolygon } from './svg-boolean-ops.js';
|
|
32
|
+
import * as PolygonClip from './polygon-clip.js';
|
|
33
|
+
|
|
34
|
+
Decimal.set({ precision: 80 });
|
|
35
|
+
|
|
36
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default SVG property values per SVG 1.1/2.0 specification
|
|
40
|
+
*/
|
|
41
|
+
export const SVG_DEFAULTS = {
|
|
42
|
+
// Fill properties
|
|
43
|
+
fill: 'black',
|
|
44
|
+
'fill-rule': 'nonzero',
|
|
45
|
+
'fill-opacity': 1,
|
|
46
|
+
|
|
47
|
+
// Stroke properties
|
|
48
|
+
stroke: 'none',
|
|
49
|
+
'stroke-width': 1,
|
|
50
|
+
'stroke-linecap': 'butt', // butt | round | square
|
|
51
|
+
'stroke-linejoin': 'miter', // miter | round | bevel
|
|
52
|
+
'stroke-miterlimit': 4,
|
|
53
|
+
'stroke-dasharray': 'none',
|
|
54
|
+
'stroke-dashoffset': 0,
|
|
55
|
+
'stroke-opacity': 1,
|
|
56
|
+
|
|
57
|
+
// Markers
|
|
58
|
+
'marker-start': 'none',
|
|
59
|
+
'marker-mid': 'none',
|
|
60
|
+
'marker-end': 'none',
|
|
61
|
+
|
|
62
|
+
// Paint order (SVG 2)
|
|
63
|
+
'paint-order': 'normal', // normal = fill stroke markers
|
|
64
|
+
|
|
65
|
+
// Clipping
|
|
66
|
+
'clip-path': 'none',
|
|
67
|
+
'clip-rule': 'nonzero',
|
|
68
|
+
|
|
69
|
+
// Masking
|
|
70
|
+
'mask': 'none',
|
|
71
|
+
'mask-type': 'luminance', // luminance | alpha
|
|
72
|
+
|
|
73
|
+
// Opacity
|
|
74
|
+
'opacity': 1,
|
|
75
|
+
|
|
76
|
+
// Transform
|
|
77
|
+
'transform': 'none',
|
|
78
|
+
'vector-effect': 'none', // none | non-scaling-stroke
|
|
79
|
+
|
|
80
|
+
// Filter
|
|
81
|
+
'filter': 'none'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Properties that affect the geometric extent of an element
|
|
86
|
+
*/
|
|
87
|
+
export const GEOMETRY_AFFECTING_PROPERTIES = [
|
|
88
|
+
'stroke-width',
|
|
89
|
+
'stroke-linecap',
|
|
90
|
+
'stroke-linejoin',
|
|
91
|
+
'stroke-miterlimit',
|
|
92
|
+
'marker-start',
|
|
93
|
+
'marker-mid',
|
|
94
|
+
'marker-end',
|
|
95
|
+
'filter',
|
|
96
|
+
'transform'
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Properties that affect what's considered "inside" a shape
|
|
101
|
+
*/
|
|
102
|
+
export const FILL_AFFECTING_PROPERTIES = [
|
|
103
|
+
'fill-rule',
|
|
104
|
+
'clip-rule'
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* SVG Rendering Context class
|
|
109
|
+
*
|
|
110
|
+
* Extracts and stores all rendering properties from an SVG element,
|
|
111
|
+
* providing methods to compute the actual rendered geometry.
|
|
112
|
+
*/
|
|
113
|
+
export class SVGRenderingContext {
|
|
114
|
+
/**
|
|
115
|
+
* Create a rendering context from an SVG element
|
|
116
|
+
* @param {Object} element - SVG element (from svg-parser.js)
|
|
117
|
+
* @param {Object} inherited - Inherited properties from parent elements
|
|
118
|
+
* @param {Map} defsMap - Map of definitions (gradients, markers, clipPaths, etc.)
|
|
119
|
+
*/
|
|
120
|
+
constructor(element, inherited = {}, defsMap = null) {
|
|
121
|
+
this.element = element;
|
|
122
|
+
this.defsMap = defsMap || new Map();
|
|
123
|
+
|
|
124
|
+
// Extract all properties with inheritance
|
|
125
|
+
this.properties = this._extractProperties(element, inherited);
|
|
126
|
+
|
|
127
|
+
// Parse stroke-dasharray into array
|
|
128
|
+
this.dashArray = this._parseDashArray(this.properties['stroke-dasharray']);
|
|
129
|
+
this.dashOffset = D(this.properties['stroke-dashoffset'] || 0);
|
|
130
|
+
|
|
131
|
+
// Parse marker references
|
|
132
|
+
this.markers = {
|
|
133
|
+
start: this._parseMarkerRef(this.properties['marker-start']),
|
|
134
|
+
mid: this._parseMarkerRef(this.properties['marker-mid']),
|
|
135
|
+
end: this._parseMarkerRef(this.properties['marker-end'])
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Determine if element has visible fill
|
|
139
|
+
this.hasFill = this.properties.fill !== 'none' &&
|
|
140
|
+
this.properties['fill-opacity'] > 0;
|
|
141
|
+
|
|
142
|
+
// Determine if element has visible stroke
|
|
143
|
+
this.hasStroke = this.properties.stroke !== 'none' &&
|
|
144
|
+
D(this.properties['stroke-width']).gt(0) &&
|
|
145
|
+
this.properties['stroke-opacity'] > 0;
|
|
146
|
+
|
|
147
|
+
// Determine if element has markers
|
|
148
|
+
this.hasMarkers = this.markers.start || this.markers.mid || this.markers.end;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract all rendering properties from element with inheritance
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
_extractProperties(element, inherited) {
|
|
156
|
+
const props = { ...SVG_DEFAULTS, ...inherited };
|
|
157
|
+
|
|
158
|
+
// Get attributes from element
|
|
159
|
+
if (element && element.getAttributeNames) {
|
|
160
|
+
for (const attr of element.getAttributeNames()) {
|
|
161
|
+
const value = element.getAttribute(attr);
|
|
162
|
+
if (value !== null && value !== undefined) {
|
|
163
|
+
props[attr] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse style attribute
|
|
169
|
+
if (props.style) {
|
|
170
|
+
const styleProps = this._parseStyleAttribute(props.style);
|
|
171
|
+
Object.assign(props, styleProps);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Convert numeric properties
|
|
175
|
+
const numericProps = ['stroke-width', 'stroke-miterlimit', 'stroke-dashoffset',
|
|
176
|
+
'opacity', 'fill-opacity', 'stroke-opacity'];
|
|
177
|
+
for (const prop of numericProps) {
|
|
178
|
+
if (typeof props[prop] === 'string') {
|
|
179
|
+
const parsed = parseFloat(props[prop]);
|
|
180
|
+
if (!isNaN(parsed)) {
|
|
181
|
+
props[prop] = parsed;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return props;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse CSS style attribute into property object
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
_parseStyleAttribute(style) {
|
|
194
|
+
const props = {};
|
|
195
|
+
if (!style) return props;
|
|
196
|
+
|
|
197
|
+
const declarations = style.split(';');
|
|
198
|
+
for (const decl of declarations) {
|
|
199
|
+
const [prop, value] = decl.split(':').map(s => s.trim());
|
|
200
|
+
if (prop && value) {
|
|
201
|
+
props[prop] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return props;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse stroke-dasharray into array of Decimal values
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_parseDashArray(dasharray) {
|
|
212
|
+
if (!dasharray || dasharray === 'none') return null;
|
|
213
|
+
|
|
214
|
+
const parts = dasharray.toString().split(/[\s,]+/).filter(s => s);
|
|
215
|
+
const values = parts.map(s => D(parseFloat(s)));
|
|
216
|
+
|
|
217
|
+
// Per SVG spec, if odd number of values, duplicate the array
|
|
218
|
+
if (values.length % 2 === 1) {
|
|
219
|
+
return [...values, ...values];
|
|
220
|
+
}
|
|
221
|
+
return values;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parse marker reference URL
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
_parseMarkerRef(value) {
|
|
229
|
+
if (!value || value === 'none') return null;
|
|
230
|
+
const match = value.match(/url\(#?([^)]+)\)/);
|
|
231
|
+
return match ? match[1] : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get the fill rule for this element
|
|
236
|
+
* @returns {string} 'nonzero' or 'evenodd'
|
|
237
|
+
*/
|
|
238
|
+
get fillRule() {
|
|
239
|
+
return this.properties['fill-rule'] || 'nonzero';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get the clip rule for this element
|
|
244
|
+
* @returns {string} 'nonzero' or 'evenodd'
|
|
245
|
+
*/
|
|
246
|
+
get clipRule() {
|
|
247
|
+
return this.properties['clip-rule'] || 'nonzero';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get stroke width as Decimal
|
|
252
|
+
* @returns {Decimal}
|
|
253
|
+
*/
|
|
254
|
+
get strokeWidth() {
|
|
255
|
+
return D(this.properties['stroke-width'] || 0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get stroke linecap
|
|
260
|
+
* @returns {string} 'butt', 'round', or 'square'
|
|
261
|
+
*/
|
|
262
|
+
get strokeLinecap() {
|
|
263
|
+
return this.properties['stroke-linecap'] || 'butt';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get stroke linejoin
|
|
268
|
+
* @returns {string} 'miter', 'round', or 'bevel'
|
|
269
|
+
*/
|
|
270
|
+
get strokeLinejoin() {
|
|
271
|
+
return this.properties['stroke-linejoin'] || 'miter';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get stroke miterlimit
|
|
276
|
+
* @returns {Decimal}
|
|
277
|
+
*/
|
|
278
|
+
get strokeMiterlimit() {
|
|
279
|
+
return D(this.properties['stroke-miterlimit'] || 4);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Calculate the maximum extent that stroke adds to geometry
|
|
284
|
+
*
|
|
285
|
+
* For a path at position (x, y), the stroke can extend up to:
|
|
286
|
+
* - strokeWidth/2 for normal edges
|
|
287
|
+
* - strokeWidth/2 * miterlimit for miter joins (worst case)
|
|
288
|
+
* - strokeWidth/2 for round/bevel joins
|
|
289
|
+
* - strokeWidth/2 + strokeWidth/2 for square linecaps
|
|
290
|
+
*
|
|
291
|
+
* @returns {Decimal} Maximum stroke extent beyond path geometry
|
|
292
|
+
*/
|
|
293
|
+
getStrokeExtent() {
|
|
294
|
+
if (!this.hasStroke) return D(0);
|
|
295
|
+
|
|
296
|
+
const halfWidth = this.strokeWidth.div(2);
|
|
297
|
+
let extent = halfWidth;
|
|
298
|
+
|
|
299
|
+
// Miter joins can extend further
|
|
300
|
+
if (this.strokeLinejoin === 'miter') {
|
|
301
|
+
extent = halfWidth.times(this.strokeMiterlimit);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Square linecaps extend by half stroke width beyond endpoints
|
|
305
|
+
if (this.strokeLinecap === 'square') {
|
|
306
|
+
const capExtent = halfWidth;
|
|
307
|
+
if (capExtent.gt(extent)) {
|
|
308
|
+
extent = capExtent;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return extent;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Expand a bounding box to account for stroke
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} bbox - Bounding box {x, y, width, height} with Decimal values
|
|
319
|
+
* @returns {Object} Expanded bounding box
|
|
320
|
+
*/
|
|
321
|
+
expandBBoxForStroke(bbox) {
|
|
322
|
+
if (!this.hasStroke) return bbox;
|
|
323
|
+
|
|
324
|
+
const extent = this.getStrokeExtent();
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
x: bbox.x.minus(extent),
|
|
328
|
+
y: bbox.y.minus(extent),
|
|
329
|
+
width: bbox.width.plus(extent.times(2)),
|
|
330
|
+
height: bbox.height.plus(extent.times(2))
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Expand a bounding box to account for markers
|
|
336
|
+
*
|
|
337
|
+
* This is an approximation - for exact results, marker geometry must be resolved.
|
|
338
|
+
*
|
|
339
|
+
* @param {Object} bbox - Bounding box {x, y, width, height} with Decimal values
|
|
340
|
+
* @param {Object} markerSizes - Optional {start, mid, end} marker sizes
|
|
341
|
+
* @returns {Object} Expanded bounding box
|
|
342
|
+
*/
|
|
343
|
+
expandBBoxForMarkers(bbox, markerSizes = null) {
|
|
344
|
+
if (!this.hasMarkers) return bbox;
|
|
345
|
+
|
|
346
|
+
// If marker sizes provided, use them; otherwise estimate from marker definitions
|
|
347
|
+
let maxMarkerSize = D(0);
|
|
348
|
+
|
|
349
|
+
if (markerSizes) {
|
|
350
|
+
const sizes = [markerSizes.start, markerSizes.mid, markerSizes.end]
|
|
351
|
+
.filter(s => s)
|
|
352
|
+
.map(s => D(s));
|
|
353
|
+
if (sizes.length > 0) {
|
|
354
|
+
maxMarkerSize = Decimal.max(...sizes);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// Default marker size estimate based on stroke width
|
|
358
|
+
maxMarkerSize = this.strokeWidth.times(3);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (maxMarkerSize.lte(0)) return bbox;
|
|
362
|
+
|
|
363
|
+
const extent = maxMarkerSize.div(2);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
x: bbox.x.minus(extent),
|
|
367
|
+
y: bbox.y.minus(extent),
|
|
368
|
+
width: bbox.width.plus(extent.times(2)),
|
|
369
|
+
height: bbox.height.plus(extent.times(2))
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Expand a bounding box to account for filter effects
|
|
375
|
+
*
|
|
376
|
+
* SVG filters can extend the rendering area significantly (blur, drop-shadow, etc.)
|
|
377
|
+
*
|
|
378
|
+
* @param {Object} bbox - Bounding box {x, y, width, height} with Decimal values
|
|
379
|
+
* @param {Object} filterDef - Optional filter definition with primitive extents
|
|
380
|
+
* @returns {Object} Expanded bounding box
|
|
381
|
+
*/
|
|
382
|
+
expandBBoxForFilter(bbox, filterDef = null) {
|
|
383
|
+
const filterRef = this.properties.filter;
|
|
384
|
+
if (!filterRef || filterRef === 'none') return bbox;
|
|
385
|
+
|
|
386
|
+
// Default filter region is 10% larger on each side (per SVG spec)
|
|
387
|
+
// filterUnits="objectBoundingBox" x="-10%" y="-10%" width="120%" height="120%"
|
|
388
|
+
let extentX = bbox.width.times('0.1');
|
|
389
|
+
let extentY = bbox.height.times('0.1');
|
|
390
|
+
|
|
391
|
+
// If filter definition provided with explicit bounds, use those
|
|
392
|
+
if (filterDef) {
|
|
393
|
+
if (filterDef.x !== undefined) extentX = D(filterDef.x).abs();
|
|
394
|
+
if (filterDef.y !== undefined) extentY = D(filterDef.y).abs();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
x: bbox.x.minus(extentX),
|
|
399
|
+
y: bbox.y.minus(extentY),
|
|
400
|
+
width: bbox.width.plus(extentX.times(2)),
|
|
401
|
+
height: bbox.height.plus(extentY.times(2))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get the full rendered bounding box including all effects
|
|
407
|
+
*
|
|
408
|
+
* @param {Object} geometryBBox - Base geometry bounding box
|
|
409
|
+
* @param {Object} options - Options for marker/filter sizes
|
|
410
|
+
* @returns {Object} Full rendered bounding box
|
|
411
|
+
*/
|
|
412
|
+
getRenderedBBox(geometryBBox, options = {}) {
|
|
413
|
+
let bbox = { ...geometryBBox };
|
|
414
|
+
|
|
415
|
+
// Expand for stroke
|
|
416
|
+
bbox = this.expandBBoxForStroke(bbox);
|
|
417
|
+
|
|
418
|
+
// Expand for markers
|
|
419
|
+
bbox = this.expandBBoxForMarkers(bbox, options.markerSizes);
|
|
420
|
+
|
|
421
|
+
// Expand for filter
|
|
422
|
+
bbox = this.expandBBoxForFilter(bbox, options.filterDef);
|
|
423
|
+
|
|
424
|
+
return bbox;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Convert path geometry to filled polygon accounting for stroke
|
|
429
|
+
*
|
|
430
|
+
* @param {Array} polygon - Path geometry as polygon vertices
|
|
431
|
+
* @returns {Array} Polygon(s) representing the full rendered area
|
|
432
|
+
*/
|
|
433
|
+
getFilledArea(polygon) {
|
|
434
|
+
const areas = [];
|
|
435
|
+
|
|
436
|
+
// Add fill area (if filled)
|
|
437
|
+
if (this.hasFill) {
|
|
438
|
+
areas.push({
|
|
439
|
+
polygon: polygon,
|
|
440
|
+
fillRule: this.fillRule,
|
|
441
|
+
type: 'fill'
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Add stroke area (if stroked)
|
|
446
|
+
if (this.hasStroke) {
|
|
447
|
+
const strokePolygon = strokeToFilledPolygon(polygon, {
|
|
448
|
+
width: this.strokeWidth.toNumber(),
|
|
449
|
+
linecap: this.strokeLinecap,
|
|
450
|
+
linejoin: this.strokeLinejoin,
|
|
451
|
+
miterlimit: this.strokeMiterlimit.toNumber()
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (strokePolygon && strokePolygon.length > 0) {
|
|
455
|
+
areas.push({
|
|
456
|
+
polygon: strokePolygon,
|
|
457
|
+
fillRule: 'nonzero', // Stroke is always nonzero
|
|
458
|
+
type: 'stroke'
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return areas;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Test if a point is inside the rendered area of this element
|
|
468
|
+
*
|
|
469
|
+
* @param {Object} point - Point {x, y} with Decimal values
|
|
470
|
+
* @param {Array} polygon - Element geometry as polygon
|
|
471
|
+
* @returns {boolean} True if point is inside rendered area
|
|
472
|
+
*/
|
|
473
|
+
isPointInRenderedArea(point, polygon) {
|
|
474
|
+
// Check fill area
|
|
475
|
+
if (this.hasFill) {
|
|
476
|
+
const fillRule = this.fillRule === 'evenodd' ? FillRule.EVENODD : FillRule.NONZERO;
|
|
477
|
+
const inFill = pointInPolygonWithRule(point, polygon, fillRule);
|
|
478
|
+
if (inFill >= 0) return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check stroke area
|
|
482
|
+
if (this.hasStroke) {
|
|
483
|
+
const strokePolygon = strokeToFilledPolygon(polygon, {
|
|
484
|
+
width: this.strokeWidth.toNumber(),
|
|
485
|
+
linecap: this.strokeLinecap,
|
|
486
|
+
linejoin: this.strokeLinejoin,
|
|
487
|
+
miterlimit: this.strokeMiterlimit.toNumber()
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (strokePolygon && strokePolygon.length > 0) {
|
|
491
|
+
const inStroke = pointInPolygonWithRule(point, strokePolygon, FillRule.NONZERO);
|
|
492
|
+
if (inStroke >= 0) return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check if two elements with contexts can be merged
|
|
501
|
+
*
|
|
502
|
+
* Elements can only be merged if their rendering properties are compatible.
|
|
503
|
+
*
|
|
504
|
+
* @param {SVGRenderingContext} other - Other element's context
|
|
505
|
+
* @returns {Object} {canMerge: boolean, reason: string}
|
|
506
|
+
*/
|
|
507
|
+
canMergeWith(other) {
|
|
508
|
+
// Fill rules must match
|
|
509
|
+
if (this.fillRule !== other.fillRule) {
|
|
510
|
+
return { canMerge: false, reason: 'Different fill-rule' };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Stroke properties must match if either has stroke
|
|
514
|
+
if (this.hasStroke || other.hasStroke) {
|
|
515
|
+
if (this.hasStroke !== other.hasStroke) {
|
|
516
|
+
return { canMerge: false, reason: 'Different stroke presence' };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!this.strokeWidth.eq(other.strokeWidth)) {
|
|
520
|
+
return { canMerge: false, reason: 'Different stroke-width' };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (this.strokeLinecap !== other.strokeLinecap) {
|
|
524
|
+
return { canMerge: false, reason: 'Different stroke-linecap' };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (this.strokeLinejoin !== other.strokeLinejoin) {
|
|
528
|
+
return { canMerge: false, reason: 'Different stroke-linejoin' };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Neither can have markers (markers break continuity)
|
|
533
|
+
if (this.hasMarkers || other.hasMarkers) {
|
|
534
|
+
return { canMerge: false, reason: 'Has markers' };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Neither can have clip-path or mask
|
|
538
|
+
if (this.properties['clip-path'] !== 'none' || other.properties['clip-path'] !== 'none') {
|
|
539
|
+
return { canMerge: false, reason: 'Has clip-path' };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (this.properties.mask !== 'none' || other.properties.mask !== 'none') {
|
|
543
|
+
return { canMerge: false, reason: 'Has mask' };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Neither can have filter
|
|
547
|
+
if (this.properties.filter !== 'none' || other.properties.filter !== 'none') {
|
|
548
|
+
return { canMerge: false, reason: 'Has filter' };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { canMerge: true, reason: null };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Get a summary of this context for debugging
|
|
556
|
+
* @returns {Object}
|
|
557
|
+
*/
|
|
558
|
+
toSummary() {
|
|
559
|
+
return {
|
|
560
|
+
hasFill: this.hasFill,
|
|
561
|
+
hasStroke: this.hasStroke,
|
|
562
|
+
hasMarkers: this.hasMarkers,
|
|
563
|
+
fillRule: this.fillRule,
|
|
564
|
+
strokeWidth: this.hasStroke ? this.strokeWidth.toNumber() : 0,
|
|
565
|
+
strokeExtent: this.getStrokeExtent().toNumber(),
|
|
566
|
+
linecap: this.strokeLinecap,
|
|
567
|
+
linejoin: this.strokeLinejoin,
|
|
568
|
+
clipPath: this.properties['clip-path'],
|
|
569
|
+
filter: this.properties.filter
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Create a rendering context from an SVG element
|
|
576
|
+
*
|
|
577
|
+
* @param {Object} element - SVG element
|
|
578
|
+
* @param {Object} inherited - Inherited properties
|
|
579
|
+
* @param {Map} defsMap - Definitions map
|
|
580
|
+
* @returns {SVGRenderingContext}
|
|
581
|
+
*/
|
|
582
|
+
export function createRenderingContext(element, inherited = {}, defsMap = null) {
|
|
583
|
+
return new SVGRenderingContext(element, inherited, defsMap);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get inherited properties from parent chain
|
|
588
|
+
*
|
|
589
|
+
* @param {Object} element - SVG element
|
|
590
|
+
* @returns {Object} Inherited properties
|
|
591
|
+
*/
|
|
592
|
+
export function getInheritedProperties(element) {
|
|
593
|
+
const inherited = {};
|
|
594
|
+
|
|
595
|
+
// Inheritable properties per SVG spec
|
|
596
|
+
const inheritableProps = [
|
|
597
|
+
'fill', 'fill-rule', 'fill-opacity',
|
|
598
|
+
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
|
|
599
|
+
'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity',
|
|
600
|
+
'marker-start', 'marker-mid', 'marker-end',
|
|
601
|
+
'clip-rule', 'opacity', 'font-family', 'font-size', 'font-style', 'font-weight'
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
let current = element.parentNode;
|
|
605
|
+
while (current && current.tagName) {
|
|
606
|
+
for (const prop of inheritableProps) {
|
|
607
|
+
if (inherited[prop] === undefined) {
|
|
608
|
+
const value = current.getAttribute ? current.getAttribute(prop) : null;
|
|
609
|
+
if (value !== null && value !== undefined) {
|
|
610
|
+
inherited[prop] = value;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
current = current.parentNode;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return inherited;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export default {
|
|
621
|
+
SVGRenderingContext,
|
|
622
|
+
SVG_DEFAULTS,
|
|
623
|
+
GEOMETRY_AFFECTING_PROPERTIES,
|
|
624
|
+
FILL_AFFECTING_PROPERTIES,
|
|
625
|
+
createRenderingContext,
|
|
626
|
+
getInheritedProperties
|
|
627
|
+
};
|