js-draw 0.1.0 → 0.1.3
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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +6 -3
- package/dist/src/EditorImage.d.ts +1 -1
- package/dist/src/EditorImage.js +6 -3
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +11 -0
- package/dist/src/SVGLoader.js +113 -4
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +12 -2
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +109 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/geometry/Mat33.d.ts +1 -0
- package/dist/src/geometry/Mat33.js +30 -0
- package/dist/src/geometry/Path.js +105 -67
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +25 -8
- package/dist/src/rendering/Display.js +4 -3
- package/dist/src/rendering/caching/CacheRecord.js +2 -1
- package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
- package/dist/src/rendering/caching/RenderingCache.js +10 -4
- package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +1 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +216 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +197 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +29 -0
- package/dist/src/tools/TextTool.js +154 -0
- package/dist/src/tools/ToolController.d.ts +5 -5
- package/dist/src/tools/ToolController.js +10 -9
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/package.json +1 -1
- package/src/Editor.ts +7 -3
- package/src/EditorImage.ts +7 -3
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +146 -5
- package/src/Viewport.ts +15 -3
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Stroke.ts +1 -1
- package/src/components/Text.ts +136 -0
- package/src/components/builders/FreehandLineBuilder.ts +1 -1
- package/src/components/localization.ts +2 -0
- package/src/geometry/Mat33.test.ts +44 -0
- package/src/geometry/Mat33.ts +41 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +7 -3
- package/src/geometry/Path.ts +110 -68
- package/src/geometry/Rect2.test.ts +9 -0
- package/src/geometry/Rect2.ts +33 -8
- package/src/rendering/Display.ts +4 -3
- package/src/rendering/caching/CacheRecord.ts +2 -1
- package/src/rendering/caching/CacheRecordManager.ts +2 -12
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/caching/RenderingCache.ts +11 -4
- package/src/rendering/caching/RenderingCacheNode.ts +16 -3
- package/src/rendering/caching/testUtils.ts +1 -0
- package/src/rendering/caching/types.ts +4 -0
- package/src/rendering/renderers/AbstractRenderer.ts +18 -1
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +57 -10
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +262 -170
- package/src/toolbar/icons.ts +226 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +206 -0
- package/src/tools/ToolController.ts +7 -5
- package/src/tools/localization.ts +7 -0
@@ -141,4 +141,48 @@ describe('Mat33 tests', () => {
|
|
141
141
|
fullTransformInverse.transformVec2(fullTransform.transformVec2(Vec2.unitX))
|
142
142
|
).objEq(Vec2.unitX, fuzz);
|
143
143
|
});
|
144
|
+
|
145
|
+
it('should convert CSS matrix(...) strings to matricies', () => {
|
146
|
+
// From MDN:
|
147
|
+
// ⎡ a c e ⎤
|
148
|
+
// ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
|
149
|
+
// ⎣ 0 0 1 ⎦
|
150
|
+
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
|
151
|
+
expect(identity).objEq(Mat33.identity);
|
152
|
+
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
153
|
+
1, 3, 5,
|
154
|
+
2, 4, 6,
|
155
|
+
0, 0, 1,
|
156
|
+
));
|
157
|
+
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
158
|
+
1e2, 3, 5,
|
159
|
+
2, 4, 6,
|
160
|
+
0, 0, 1,
|
161
|
+
));
|
162
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
|
163
|
+
1.6, .3, 5,
|
164
|
+
2, 4, 6,
|
165
|
+
0, 0, 1,
|
166
|
+
));
|
167
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
|
168
|
+
-1, 0.03, -5.123,
|
169
|
+
2, 4, -6.5,
|
170
|
+
0, 0, 1,
|
171
|
+
));
|
172
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
|
173
|
+
1.6, .3, 5,
|
174
|
+
2, 4, 6,
|
175
|
+
0, 0, 1,
|
176
|
+
));
|
177
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
|
178
|
+
1.6, 3e-3, 5,
|
179
|
+
2, 4, 6,
|
180
|
+
0, 0, 1,
|
181
|
+
));
|
182
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
|
183
|
+
-1, 3E-2, -6.5e-1,
|
184
|
+
2e6, -5.123, 0.01,
|
185
|
+
0, 0, 1,
|
186
|
+
));
|
187
|
+
});
|
144
188
|
});
|
package/src/geometry/Mat33.ts
CHANGED
@@ -268,4 +268,45 @@ export default class Mat33 {
|
|
268
268
|
// Translate such that [center] goes to (0, 0)
|
269
269
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
270
270
|
}
|
271
|
+
|
272
|
+
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
|
273
|
+
public static fromCSSMatrix(cssString: string): Mat33 {
|
274
|
+
if (cssString === '' || cssString === 'none') {
|
275
|
+
return Mat33.identity;
|
276
|
+
}
|
277
|
+
|
278
|
+
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
|
279
|
+
const numberSepExp = '[, \\t\\n]';
|
280
|
+
const regExpSource = `^\\s*matrix\\s*\\(${
|
281
|
+
[
|
282
|
+
// According to MDN, matrix(a,b,c,d,e,f) has form:
|
283
|
+
// ⎡ a c e ⎤
|
284
|
+
// ⎢ b d f ⎥
|
285
|
+
// ⎣ 0 0 1 ⎦
|
286
|
+
numberExp, numberExp, numberExp, // a, c, e
|
287
|
+
numberExp, numberExp, numberExp, // b, d, f
|
288
|
+
].join(`${numberSepExp}+`)
|
289
|
+
}${numberSepExp}*\\)\\s*$`;
|
290
|
+
const matrixExp = new RegExp(regExpSource, 'i');
|
291
|
+
const match = matrixExp.exec(cssString);
|
292
|
+
|
293
|
+
if (!match) {
|
294
|
+
throw new Error(`Unsupported transformation: ${cssString}`);
|
295
|
+
}
|
296
|
+
|
297
|
+
const matrixData = match.slice(1).map(entry => parseFloat(entry));
|
298
|
+
const a = matrixData[0];
|
299
|
+
const b = matrixData[1];
|
300
|
+
const c = matrixData[2];
|
301
|
+
const d = matrixData[3];
|
302
|
+
const e = matrixData[4];
|
303
|
+
const f = matrixData[5];
|
304
|
+
|
305
|
+
const transform = new Mat33(
|
306
|
+
a, c, e,
|
307
|
+
b, d, f,
|
308
|
+
0, 0, 1
|
309
|
+
);
|
310
|
+
return transform;
|
311
|
+
}
|
271
312
|
}
|
@@ -90,15 +90,38 @@ describe('Path.fromString', () => {
|
|
90
90
|
]);
|
91
91
|
});
|
92
92
|
|
93
|
+
it('should break compoents at -s', () => {
|
94
|
+
const path = Path.fromString('m1-1 L-1-1-3-4-5-6,5-1');
|
95
|
+
expect(path.parts.length).toBe(4);
|
96
|
+
expect(path.parts).toMatchObject([
|
97
|
+
{
|
98
|
+
kind: PathCommandType.LineTo,
|
99
|
+
point: Vec2.of(-1, -1),
|
100
|
+
},
|
101
|
+
{
|
102
|
+
kind: PathCommandType.LineTo,
|
103
|
+
point: Vec2.of(-3, -4),
|
104
|
+
},
|
105
|
+
{
|
106
|
+
kind: PathCommandType.LineTo,
|
107
|
+
point: Vec2.of(-5, -6),
|
108
|
+
},
|
109
|
+
{
|
110
|
+
kind: PathCommandType.LineTo,
|
111
|
+
point: Vec2.of(5, -1),
|
112
|
+
},
|
113
|
+
]);
|
114
|
+
});
|
115
|
+
|
93
116
|
it('should properly handle cubic Bézier curves', () => {
|
94
|
-
const path = Path.fromString('c1,1 0
|
117
|
+
const path = Path.fromString('m1,1 c1,1 0-3 4 5 C1,1 0.1, 0.1 0, 0');
|
95
118
|
expect(path.parts.length).toBe(2);
|
96
119
|
expect(path.parts).toMatchObject([
|
97
120
|
{
|
98
121
|
kind: PathCommandType.CubicBezierTo,
|
99
|
-
controlPoint1: Vec2.of(
|
122
|
+
controlPoint1: Vec2.of(2, 2),
|
100
123
|
controlPoint2: Vec2.of(1, -2),
|
101
|
-
endPoint: Vec2.of(5,
|
124
|
+
endPoint: Vec2.of(5, 6),
|
102
125
|
},
|
103
126
|
{
|
104
127
|
kind: PathCommandType.CubicBezierTo,
|
@@ -120,7 +143,7 @@ describe('Path.fromString', () => {
|
|
120
143
|
{
|
121
144
|
kind: PathCommandType.QuadraticBezierTo,
|
122
145
|
controlPoint: Vec2.of(1, 1),
|
123
|
-
endPoint: Vec2.of(-
|
146
|
+
endPoint: Vec2.of(-1, -1),
|
124
147
|
},
|
125
148
|
{
|
126
149
|
kind: PathCommandType.QuadraticBezierTo,
|
@@ -130,4 +153,71 @@ describe('Path.fromString', () => {
|
|
130
153
|
]);
|
131
154
|
expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
|
132
155
|
});
|
156
|
+
|
157
|
+
it('should correctly handle a command followed by multiple sets of arguments', () => {
|
158
|
+
// Commands followed by multiple sets of arguments, for example,
|
159
|
+
// l 5,10 5,4 3,2,
|
160
|
+
// should be interpreted as multiple commands. Our example, is therefore equivalent to,
|
161
|
+
// l 5,10 l 5,4 l 3,2
|
162
|
+
|
163
|
+
const path = Path.fromString(`
|
164
|
+
L5,10 1,1
|
165
|
+
2,2 -3,-1
|
166
|
+
q 1,2 1,1
|
167
|
+
-1,-1 -3,-4
|
168
|
+
h -4 -1
|
169
|
+
V 3 5 1
|
170
|
+
`);
|
171
|
+
expect(path.parts).toMatchObject([
|
172
|
+
{
|
173
|
+
kind: PathCommandType.LineTo,
|
174
|
+
point: Vec2.of(1, 1),
|
175
|
+
},
|
176
|
+
{
|
177
|
+
kind: PathCommandType.LineTo,
|
178
|
+
point: Vec2.of(2, 2),
|
179
|
+
},
|
180
|
+
{
|
181
|
+
kind: PathCommandType.LineTo,
|
182
|
+
point: Vec2.of(-3, -1),
|
183
|
+
},
|
184
|
+
|
185
|
+
// q 1,2 1,1 -1,-1 -3,-4
|
186
|
+
{
|
187
|
+
kind: PathCommandType.QuadraticBezierTo,
|
188
|
+
controlPoint: Vec2.of(-2, 1),
|
189
|
+
endPoint: Vec2.of(-2, 0),
|
190
|
+
},
|
191
|
+
{
|
192
|
+
kind: PathCommandType.QuadraticBezierTo,
|
193
|
+
controlPoint: Vec2.of(-3, -1),
|
194
|
+
endPoint: Vec2.of(-5, -4),
|
195
|
+
},
|
196
|
+
|
197
|
+
// h -4 -1
|
198
|
+
{
|
199
|
+
kind: PathCommandType.LineTo,
|
200
|
+
point: Vec2.of(-9, -4),
|
201
|
+
},
|
202
|
+
{
|
203
|
+
kind: PathCommandType.LineTo,
|
204
|
+
point: Vec2.of(-10, -4),
|
205
|
+
},
|
206
|
+
|
207
|
+
// V 3 5 1
|
208
|
+
{
|
209
|
+
kind: PathCommandType.LineTo,
|
210
|
+
point: Vec2.of(-10, 3),
|
211
|
+
},
|
212
|
+
{
|
213
|
+
kind: PathCommandType.LineTo,
|
214
|
+
point: Vec2.of(-10, 5),
|
215
|
+
},
|
216
|
+
{
|
217
|
+
kind: PathCommandType.LineTo,
|
218
|
+
point: Vec2.of(-10, 1),
|
219
|
+
},
|
220
|
+
]);
|
221
|
+
expect(path.startPoint).toMatchObject(Vec2.of(5, 10));
|
222
|
+
});
|
133
223
|
});
|
@@ -19,14 +19,18 @@ describe('Path.toString', () => {
|
|
19
19
|
});
|
20
20
|
|
21
21
|
it('should fix rounding errors', () => {
|
22
|
-
const path = new Path(Vec2.of(0.
|
22
|
+
const path = new Path(Vec2.of(0.100000001, 0.199999999), [
|
23
23
|
{
|
24
24
|
kind: PathCommandType.QuadraticBezierTo,
|
25
25
|
controlPoint: Vec2.of(9999, -10.999999995),
|
26
|
-
endPoint: Vec2.of(0.000300001, 1.
|
26
|
+
endPoint: Vec2.of(0.000300001, 1.400000002),
|
27
27
|
},
|
28
|
+
{
|
29
|
+
kind: PathCommandType.LineTo,
|
30
|
+
point: Vec2.of(184.00482359999998, 1)
|
31
|
+
}
|
28
32
|
]);
|
29
|
-
expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.
|
33
|
+
expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4L184.0048236,1');
|
30
34
|
});
|
31
35
|
|
32
36
|
it('should not remove trailing zeroes before decimal points', () => {
|
package/src/geometry/Path.ts
CHANGED
@@ -303,6 +303,8 @@ export default class Path {
|
|
303
303
|
const postDecimal = parseInt(roundingDownMatch[3], 10);
|
304
304
|
const preDecimal = parseInt(roundingDownMatch[2], 10);
|
305
305
|
|
306
|
+
const origPostDecimalString = roundingDownMatch[3];
|
307
|
+
|
306
308
|
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
|
307
309
|
let carry = 0;
|
308
310
|
if (newPostDecimal.length > postDecimal.toString().length) {
|
@@ -310,13 +312,21 @@ export default class Path {
|
|
310
312
|
newPostDecimal = newPostDecimal.substring(1);
|
311
313
|
carry = 1;
|
312
314
|
}
|
315
|
+
|
316
|
+
// parseInt(...).toString() removes leading zeroes. Add them back.
|
317
|
+
while (newPostDecimal.length < origPostDecimalString.length) {
|
318
|
+
newPostDecimal = carry.toString(10) + newPostDecimal;
|
319
|
+
carry = 0;
|
320
|
+
}
|
321
|
+
|
313
322
|
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
|
314
323
|
}
|
315
324
|
|
316
325
|
text = text.replace(fixRoundingUpExp, '$1');
|
317
326
|
|
318
327
|
// Remove trailing zeroes
|
319
|
-
text = text.replace(/([.][^0]
|
328
|
+
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
|
329
|
+
text = text.replace(/[.]0+$/, '.');
|
320
330
|
|
321
331
|
// Remove trailing period
|
322
332
|
return text.replace(/[.]$/, '');
|
@@ -371,6 +381,7 @@ export default class Path {
|
|
371
381
|
|
372
382
|
let lastPos: Point2 = Vec2.zero;
|
373
383
|
let firstPos: Point2|null = null;
|
384
|
+
let startPos: Point2|null = null;
|
374
385
|
let isFirstCommand: boolean = true;
|
375
386
|
const commands: PathCommand[] = [];
|
376
387
|
|
@@ -413,19 +424,67 @@ export default class Path {
|
|
413
424
|
endPoint,
|
414
425
|
});
|
415
426
|
};
|
427
|
+
const commandArgCounts: Record<string, number> = {
|
428
|
+
'm': 1,
|
429
|
+
'l': 1,
|
430
|
+
'c': 3,
|
431
|
+
'q': 2,
|
432
|
+
'z': 0,
|
433
|
+
'h': 1,
|
434
|
+
'v': 1,
|
435
|
+
};
|
416
436
|
|
417
437
|
// Each command: Command character followed by anything that isn't a command character
|
418
|
-
const commandExp = /([
|
438
|
+
const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
|
419
439
|
let current;
|
420
440
|
while ((current = commandExp.exec(pathString)) !== null) {
|
421
|
-
const argParts = current[2].trim().split(/[^0-
|
441
|
+
const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(
|
422
442
|
part => part.length > 0
|
423
|
-
)
|
424
|
-
|
443
|
+
).reduce((accumualtor: string[], current: string): string[] => {
|
444
|
+
// As of 09/2022, iOS Safari doesn't support support lookbehind in regular
|
445
|
+
// expressions. As such, we need an alternative.
|
446
|
+
// Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
|
447
|
+
// we need special cases:
|
448
|
+
current = current.replace(/([^eE])[-]/g, '$1 -');
|
449
|
+
const parts = current.split(' -');
|
450
|
+
if (parts[0] !== '') {
|
451
|
+
accumualtor.push(parts[0]);
|
452
|
+
}
|
453
|
+
accumualtor.push(...parts.slice(1).map(part => `-${part}`));
|
454
|
+
return accumualtor;
|
455
|
+
}, []);
|
456
|
+
|
457
|
+
let numericArgs = argParts.map(arg => parseFloat(arg));
|
458
|
+
|
459
|
+
let commandChar = current[1].toLowerCase();
|
460
|
+
let uppercaseCommand = current[1] !== commandChar;
|
461
|
+
|
462
|
+
// Convert commands that don't take points into commands that do.
|
463
|
+
if (commandChar === 'v' || commandChar === 'h') {
|
464
|
+
numericArgs = numericArgs.reduce((accumulator: number[], current: number): number[] => {
|
465
|
+
if (commandChar === 'v') {
|
466
|
+
return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
|
467
|
+
} else {
|
468
|
+
return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
|
469
|
+
}
|
470
|
+
}, []);
|
471
|
+
commandChar = 'l';
|
472
|
+
} else if (commandChar === 'z') {
|
473
|
+
if (firstPos) {
|
474
|
+
numericArgs = [ firstPos.x, firstPos.y ];
|
475
|
+
firstPos = lastPos;
|
476
|
+
} else {
|
477
|
+
continue;
|
478
|
+
}
|
479
|
+
|
480
|
+
// 'z' always acts like an uppercase lineTo(startPos)
|
481
|
+
uppercaseCommand = true;
|
482
|
+
commandChar = 'l';
|
483
|
+
}
|
484
|
+
|
425
485
|
|
426
|
-
const
|
427
|
-
const
|
428
|
-
const args = numericArgs.reduce((
|
486
|
+
const commandArgCount: number = commandArgCounts[commandChar] ?? 0;
|
487
|
+
const allArgs = numericArgs.reduce((
|
429
488
|
accumulator: Point2[], current, index, parts
|
430
489
|
): Point2[] => {
|
431
490
|
if (index % 2 !== 0) {
|
@@ -435,82 +494,65 @@ export default class Path {
|
|
435
494
|
} else {
|
436
495
|
return accumulator;
|
437
496
|
}
|
438
|
-
}, []).map((coordinate
|
497
|
+
}, []).map((coordinate, index): Point2 => {
|
439
498
|
// Lowercase commands are relative, uppercase commands use absolute
|
440
499
|
// positioning
|
500
|
+
let newPos;
|
441
501
|
if (uppercaseCommand) {
|
442
|
-
|
443
|
-
return coordinate;
|
502
|
+
newPos = coordinate;
|
444
503
|
} else {
|
445
|
-
|
446
|
-
return lastPos;
|
504
|
+
newPos = lastPos.plus(coordinate);
|
447
505
|
}
|
448
|
-
});
|
449
|
-
|
450
|
-
let expectedPointArgCount;
|
451
506
|
|
452
|
-
|
453
|
-
|
454
|
-
expectedPointArgCount = 1;
|
455
|
-
moveTo(args[0]);
|
456
|
-
break;
|
457
|
-
case 'l':
|
458
|
-
expectedPointArgCount = 1;
|
459
|
-
lineTo(args[0]);
|
460
|
-
break;
|
461
|
-
case 'z':
|
462
|
-
expectedPointArgCount = 0;
|
463
|
-
// firstPos can be null if the stroke data is just 'z'.
|
464
|
-
if (firstPos) {
|
465
|
-
lineTo(firstPos);
|
507
|
+
if ((index + 1) % commandArgCount === 0) {
|
508
|
+
lastPos = newPos;
|
466
509
|
}
|
467
|
-
break;
|
468
|
-
case 'c':
|
469
|
-
expectedPointArgCount = 3;
|
470
|
-
cubicBezierTo(args[0], args[1], args[2]);
|
471
|
-
break;
|
472
|
-
case 'q':
|
473
|
-
expectedPointArgCount = 2;
|
474
|
-
quadraticBeierTo(args[0], args[1]);
|
475
|
-
break;
|
476
|
-
|
477
|
-
// Horizontal line
|
478
|
-
case 'h':
|
479
|
-
expectedPointArgCount = 0;
|
480
510
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
511
|
+
return newPos;
|
512
|
+
});
|
513
|
+
|
514
|
+
if (allArgs.length % commandArgCount !== 0) {
|
515
|
+
throw new Error([
|
516
|
+
`Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
|
517
|
+
`The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
|
518
|
+
`Command: ${current[0]}`,
|
519
|
+
].join('\n'));
|
520
|
+
}
|
487
521
|
|
488
|
-
|
489
|
-
|
490
|
-
expectedPointArgCount = 0;
|
522
|
+
for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
|
523
|
+
const args = allArgs.slice(argPos, argPos + commandArgCount);
|
491
524
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
525
|
+
switch (commandChar.toLowerCase()) {
|
526
|
+
case 'm':
|
527
|
+
if (argPos === 0) {
|
528
|
+
moveTo(args[0]);
|
529
|
+
} else {
|
530
|
+
lineTo(args[0]);
|
531
|
+
}
|
532
|
+
break;
|
533
|
+
case 'l':
|
534
|
+
lineTo(args[0]);
|
535
|
+
break;
|
536
|
+
case 'c':
|
537
|
+
cubicBezierTo(args[0], args[1], args[2]);
|
538
|
+
break;
|
539
|
+
case 'q':
|
540
|
+
quadraticBeierTo(args[0], args[1]);
|
541
|
+
break;
|
542
|
+
default:
|
543
|
+
throw new Error(`Unknown path command ${commandChar}`);
|
496
544
|
}
|
497
|
-
break;
|
498
|
-
default:
|
499
|
-
throw new Error(`Unknown path command ${commandChar}`);
|
500
|
-
}
|
501
545
|
|
502
|
-
|
503
|
-
throw new Error(`
|
504
|
-
Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
|
505
|
-
`.trim());
|
546
|
+
isFirstCommand = false;
|
506
547
|
}
|
507
548
|
|
508
|
-
if (
|
509
|
-
firstPos ??=
|
549
|
+
if (allArgs.length > 0) {
|
550
|
+
firstPos ??= allArgs[0];
|
551
|
+
startPos ??= firstPos;
|
552
|
+
lastPos = allArgs[allArgs.length - 1];
|
510
553
|
}
|
511
|
-
isFirstCommand = false;
|
512
554
|
}
|
513
555
|
|
514
|
-
return new Path(
|
556
|
+
return new Path(startPos ?? Vec2.zero, commands);
|
515
557
|
}
|
516
558
|
}
|
@@ -148,4 +148,13 @@ describe('Rect2', () => {
|
|
148
148
|
expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
|
149
149
|
});
|
150
150
|
});
|
151
|
+
|
152
|
+
it('division of rectangle', () => {
|
153
|
+
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
|
154
|
+
[
|
155
|
+
new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
|
156
|
+
new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
|
157
|
+
]
|
158
|
+
);
|
159
|
+
});
|
151
160
|
});
|
package/src/geometry/Rect2.ts
CHANGED
@@ -67,22 +67,39 @@ export default class Rect2 {
|
|
67
67
|
}
|
68
68
|
|
69
69
|
public intersects(other: Rect2): boolean {
|
70
|
-
|
70
|
+
// Project along x/y axes.
|
71
|
+
const thisMinX = this.x;
|
72
|
+
const thisMaxX = thisMinX + this.w;
|
73
|
+
const otherMinX = other.x;
|
74
|
+
const otherMaxX = other.x + other.w;
|
75
|
+
|
76
|
+
if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
|
77
|
+
return false;
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
const thisMinY = this.y;
|
82
|
+
const thisMaxY = thisMinY + this.h;
|
83
|
+
const otherMinY = other.y;
|
84
|
+
const otherMaxY = other.y + other.h;
|
85
|
+
|
86
|
+
if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
|
87
|
+
return false;
|
88
|
+
}
|
89
|
+
|
90
|
+
return true;
|
71
91
|
}
|
72
92
|
|
73
93
|
// Returns the overlap of this and [other], or null, if no such
|
74
94
|
// overlap exists
|
75
95
|
public intersection(other: Rect2): Rect2|null {
|
76
|
-
|
77
|
-
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
|
78
|
-
|
79
|
-
// The intersection can't be outside of this rectangle
|
80
|
-
if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
|
81
|
-
return null;
|
82
|
-
} else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
|
96
|
+
if (!this.intersects(other)) {
|
83
97
|
return null;
|
84
98
|
}
|
85
99
|
|
100
|
+
const topLeft = this.topLeft.zip(other.topLeft, Math.max);
|
101
|
+
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
|
102
|
+
|
86
103
|
return Rect2.fromCorners(topLeft, bottomRight);
|
87
104
|
}
|
88
105
|
|
@@ -167,6 +184,14 @@ export default class Rect2 {
|
|
167
184
|
return this.topLeft.plus(Vec2.of(0, this.h));
|
168
185
|
}
|
169
186
|
|
187
|
+
public get width() {
|
188
|
+
return this.w;
|
189
|
+
}
|
190
|
+
|
191
|
+
public get height() {
|
192
|
+
return this.h;
|
193
|
+
}
|
194
|
+
|
170
195
|
// Returns edges in the order
|
171
196
|
// [ rightEdge, topEdge, leftEdge, bottomEdge ]
|
172
197
|
public getEdges(): LineSegment2[] {
|
package/src/rendering/Display.ts
CHANGED
@@ -31,7 +31,7 @@ export default class Display {
|
|
31
31
|
throw new Error(`Unknown rendering mode, ${mode}!`);
|
32
32
|
}
|
33
33
|
|
34
|
-
const cacheBlockResolution = Vec2.of(
|
34
|
+
const cacheBlockResolution = Vec2.of(600, 600);
|
35
35
|
this.cache = new RenderingCache({
|
36
36
|
createRenderer: () => {
|
37
37
|
if (mode === RenderingMode.DummyRenderer) {
|
@@ -54,8 +54,9 @@ export default class Display {
|
|
54
54
|
},
|
55
55
|
blockResolution: cacheBlockResolution,
|
56
56
|
cacheSize: 500 * 500 * 4 * 200,
|
57
|
-
maxScale: 1.
|
58
|
-
minComponentsPerCache:
|
57
|
+
maxScale: 1.5,
|
58
|
+
minComponentsPerCache: 50,
|
59
|
+
minComponentsToUseCache: 120,
|
59
60
|
});
|
60
61
|
|
61
62
|
this.editor.notifier.on(EditorEventType.DisplayResized, event => {
|
@@ -21,7 +21,7 @@ export default class CacheRecord {
|
|
21
21
|
}
|
22
22
|
|
23
23
|
public startRender(): AbstractRenderer {
|
24
|
-
this.lastUsedCycle = this.cacheState.currentRenderingCycle
|
24
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
25
25
|
if (!this.allocd) {
|
26
26
|
throw new Error('Only alloc\'d canvases can be rendered to');
|
27
27
|
}
|
@@ -45,6 +45,7 @@ export default class CacheRecord {
|
|
45
45
|
}
|
46
46
|
this.allocd = true;
|
47
47
|
this.onBeforeDeallocCallback = newDeallocCallback;
|
48
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
48
49
|
}
|
49
50
|
|
50
51
|
public getLastUsedCycle(): number {
|
@@ -39,17 +39,7 @@ export class CacheRecordManager {
|
|
39
39
|
|
40
40
|
// Returns null if there are no cache records. Returns an unalloc'd record if one exists.
|
41
41
|
private getLeastRecentlyUsedRecord(): CacheRecord|null {
|
42
|
-
|
43
|
-
|
44
|
-
if (!rec.isAllocd()) {
|
45
|
-
return rec;
|
46
|
-
}
|
47
|
-
|
48
|
-
if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
|
49
|
-
lruSoFar = rec;
|
50
|
-
}
|
51
|
-
}
|
52
|
-
|
53
|
-
return lruSoFar;
|
42
|
+
this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
|
43
|
+
return this.cacheRecords[0];
|
54
44
|
}
|
55
45
|
}
|
@@ -10,7 +10,7 @@ import Viewport from '../../Viewport';
|
|
10
10
|
import Mat33 from '../../geometry/Mat33';
|
11
11
|
|
12
12
|
describe('RenderingCache', () => {
|
13
|
-
const testPath = Path.fromString('M0,0 l100,500 l-20,20');
|
13
|
+
const testPath = Path.fromString('M0,0 l100,500 l-20,20 L-100,-100');
|
14
14
|
const testStroke = new Stroke([ testPath.toRenderable({ fill: Color4.purple }) ]);
|
15
15
|
|
16
16
|
it('should create a root node large enough to contain the viewport', () => {
|
@@ -37,11 +37,12 @@ export default class RenderingCache {
|
|
37
37
|
}
|
38
38
|
|
39
39
|
if (!this.rootNode) {
|
40
|
-
//
|
41
|
-
const
|
40
|
+
// Adjust the node so that it has the correct aspect ratio
|
41
|
+
const res = this.partialSharedState.props.blockResolution;
|
42
|
+
|
42
43
|
const topLeft = visibleRect.topLeft;
|
43
44
|
this.rootNode = new RenderingCacheNode(
|
44
|
-
new Rect2(topLeft.x, topLeft.y,
|
45
|
+
new Rect2(topLeft.x, topLeft.y, res.x, res.y),
|
45
46
|
this.getSharedState()
|
46
47
|
);
|
47
48
|
}
|
@@ -51,6 +52,12 @@ export default class RenderingCache {
|
|
51
52
|
}
|
52
53
|
|
53
54
|
this.rootNode = this.rootNode!.smallestChildContaining(visibleRect) ?? this.rootNode;
|
54
|
-
|
55
|
+
|
56
|
+
const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
|
57
|
+
if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
|
58
|
+
this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
|
59
|
+
} else {
|
60
|
+
image.render(screenRenderer, visibleRect);
|
61
|
+
}
|
55
62
|
}
|
56
63
|
}
|