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
@@ -220,6 +220,7 @@ export default class Path {
|
|
220
220
|
const lastDigit = parseInt(text.charAt(text.length - 1), 10);
|
221
221
|
const postDecimal = parseInt(roundingDownMatch[3], 10);
|
222
222
|
const preDecimal = parseInt(roundingDownMatch[2], 10);
|
223
|
+
const origPostDecimalString = roundingDownMatch[3];
|
223
224
|
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
|
224
225
|
let carry = 0;
|
225
226
|
if (newPostDecimal.length > postDecimal.toString().length) {
|
@@ -227,11 +228,17 @@ export default class Path {
|
|
227
228
|
newPostDecimal = newPostDecimal.substring(1);
|
228
229
|
carry = 1;
|
229
230
|
}
|
231
|
+
// parseInt(...).toString() removes leading zeroes. Add them back.
|
232
|
+
while (newPostDecimal.length < origPostDecimalString.length) {
|
233
|
+
newPostDecimal = carry.toString(10) + newPostDecimal;
|
234
|
+
carry = 0;
|
235
|
+
}
|
230
236
|
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
|
231
237
|
}
|
232
238
|
text = text.replace(fixRoundingUpExp, '$1');
|
233
239
|
// Remove trailing zeroes
|
234
|
-
text = text.replace(/([.][^0]
|
240
|
+
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
|
241
|
+
text = text.replace(/[.]0+$/, '.');
|
235
242
|
// Remove trailing period
|
236
243
|
return text.replace(/[.]$/, '');
|
237
244
|
};
|
@@ -275,10 +282,12 @@ export default class Path {
|
|
275
282
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
276
283
|
// and
|
277
284
|
// https://www.w3.org/TR/SVG2/paths.html
|
285
|
+
var _a;
|
278
286
|
// Remove linebreaks
|
279
287
|
pathString = pathString.split('\n').join(' ');
|
280
288
|
let lastPos = Vec2.zero;
|
281
289
|
let firstPos = null;
|
290
|
+
let startPos = null;
|
282
291
|
let isFirstCommand = true;
|
283
292
|
const commands = [];
|
284
293
|
const moveTo = (point) => {
|
@@ -317,15 +326,61 @@ export default class Path {
|
|
317
326
|
endPoint,
|
318
327
|
});
|
319
328
|
};
|
329
|
+
const commandArgCounts = {
|
330
|
+
'm': 1,
|
331
|
+
'l': 1,
|
332
|
+
'c': 3,
|
333
|
+
'q': 2,
|
334
|
+
'z': 0,
|
335
|
+
'h': 1,
|
336
|
+
'v': 1,
|
337
|
+
};
|
320
338
|
// Each command: Command character followed by anything that isn't a command character
|
321
|
-
const commandExp = /([
|
339
|
+
const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
|
322
340
|
let current;
|
323
341
|
while ((current = commandExp.exec(pathString)) !== null) {
|
324
|
-
const argParts = current[2].trim().split(/[^0-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
342
|
+
const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
|
343
|
+
// As of 09/2022, iOS Safari doesn't support support lookbehind in regular
|
344
|
+
// expressions. As such, we need an alternative.
|
345
|
+
// Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
|
346
|
+
// we need special cases:
|
347
|
+
current = current.replace(/([^eE])[-]/g, '$1 -');
|
348
|
+
const parts = current.split(' -');
|
349
|
+
if (parts[0] !== '') {
|
350
|
+
accumualtor.push(parts[0]);
|
351
|
+
}
|
352
|
+
accumualtor.push(...parts.slice(1).map(part => `-${part}`));
|
353
|
+
return accumualtor;
|
354
|
+
}, []);
|
355
|
+
let numericArgs = argParts.map(arg => parseFloat(arg));
|
356
|
+
let commandChar = current[1].toLowerCase();
|
357
|
+
let uppercaseCommand = current[1] !== commandChar;
|
358
|
+
// Convert commands that don't take points into commands that do.
|
359
|
+
if (commandChar === 'v' || commandChar === 'h') {
|
360
|
+
numericArgs = numericArgs.reduce((accumulator, current) => {
|
361
|
+
if (commandChar === 'v') {
|
362
|
+
return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
|
363
|
+
}
|
364
|
+
else {
|
365
|
+
return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
|
366
|
+
}
|
367
|
+
}, []);
|
368
|
+
commandChar = 'l';
|
369
|
+
}
|
370
|
+
else if (commandChar === 'z') {
|
371
|
+
if (firstPos) {
|
372
|
+
numericArgs = [firstPos.x, firstPos.y];
|
373
|
+
firstPos = lastPos;
|
374
|
+
}
|
375
|
+
else {
|
376
|
+
continue;
|
377
|
+
}
|
378
|
+
// 'z' always acts like an uppercase lineTo(startPos)
|
379
|
+
uppercaseCommand = true;
|
380
|
+
commandChar = 'l';
|
381
|
+
}
|
382
|
+
const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
|
383
|
+
const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
|
329
384
|
if (index % 2 !== 0) {
|
330
385
|
const currentAsFloat = current;
|
331
386
|
const prevAsFloat = parts[index - 1];
|
@@ -334,76 +389,59 @@ export default class Path {
|
|
334
389
|
else {
|
335
390
|
return accumulator;
|
336
391
|
}
|
337
|
-
}, []).map((coordinate) => {
|
392
|
+
}, []).map((coordinate, index) => {
|
338
393
|
// Lowercase commands are relative, uppercase commands use absolute
|
339
394
|
// positioning
|
395
|
+
let newPos;
|
340
396
|
if (uppercaseCommand) {
|
341
|
-
|
342
|
-
return coordinate;
|
397
|
+
newPos = coordinate;
|
343
398
|
}
|
344
399
|
else {
|
345
|
-
|
346
|
-
|
400
|
+
newPos = lastPos.plus(coordinate);
|
401
|
+
}
|
402
|
+
if ((index + 1) % commandArgCount === 0) {
|
403
|
+
lastPos = newPos;
|
347
404
|
}
|
405
|
+
return newPos;
|
348
406
|
});
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
case 'l':
|
356
|
-
expectedPointArgCount = 1;
|
357
|
-
lineTo(args[0]);
|
358
|
-
break;
|
359
|
-
case 'z':
|
360
|
-
expectedPointArgCount = 0;
|
361
|
-
// firstPos can be null if the stroke data is just 'z'.
|
362
|
-
if (firstPos) {
|
363
|
-
lineTo(firstPos);
|
364
|
-
}
|
365
|
-
break;
|
366
|
-
case 'c':
|
367
|
-
expectedPointArgCount = 3;
|
368
|
-
cubicBezierTo(args[0], args[1], args[2]);
|
369
|
-
break;
|
370
|
-
case 'q':
|
371
|
-
expectedPointArgCount = 2;
|
372
|
-
quadraticBeierTo(args[0], args[1]);
|
373
|
-
break;
|
374
|
-
// Horizontal line
|
375
|
-
case 'h':
|
376
|
-
expectedPointArgCount = 0;
|
377
|
-
if (uppercaseCommand) {
|
378
|
-
lineTo(Vec2.of(numericArgs[0], lastPos.y));
|
379
|
-
}
|
380
|
-
else {
|
381
|
-
lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
|
382
|
-
}
|
383
|
-
break;
|
384
|
-
// Vertical line
|
385
|
-
case 'v':
|
386
|
-
expectedPointArgCount = 0;
|
387
|
-
if (uppercaseCommand) {
|
388
|
-
lineTo(Vec2.of(lastPos.x, numericArgs[1]));
|
389
|
-
}
|
390
|
-
else {
|
391
|
-
lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
|
392
|
-
}
|
393
|
-
break;
|
394
|
-
default:
|
395
|
-
throw new Error(`Unknown path command ${commandChar}`);
|
407
|
+
if (allArgs.length % commandArgCount !== 0) {
|
408
|
+
throw new Error([
|
409
|
+
`Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
|
410
|
+
`The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
|
411
|
+
`Command: ${current[0]}`,
|
412
|
+
].join('\n'));
|
396
413
|
}
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
414
|
+
for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
|
415
|
+
const args = allArgs.slice(argPos, argPos + commandArgCount);
|
416
|
+
switch (commandChar.toLowerCase()) {
|
417
|
+
case 'm':
|
418
|
+
if (argPos === 0) {
|
419
|
+
moveTo(args[0]);
|
420
|
+
}
|
421
|
+
else {
|
422
|
+
lineTo(args[0]);
|
423
|
+
}
|
424
|
+
break;
|
425
|
+
case 'l':
|
426
|
+
lineTo(args[0]);
|
427
|
+
break;
|
428
|
+
case 'c':
|
429
|
+
cubicBezierTo(args[0], args[1], args[2]);
|
430
|
+
break;
|
431
|
+
case 'q':
|
432
|
+
quadraticBeierTo(args[0], args[1]);
|
433
|
+
break;
|
434
|
+
default:
|
435
|
+
throw new Error(`Unknown path command ${commandChar}`);
|
436
|
+
}
|
437
|
+
isFirstCommand = false;
|
401
438
|
}
|
402
|
-
if (
|
403
|
-
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos =
|
439
|
+
if (allArgs.length > 0) {
|
440
|
+
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
|
441
|
+
startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
|
442
|
+
lastPos = allArgs[allArgs.length - 1];
|
404
443
|
}
|
405
|
-
isFirstCommand = false;
|
406
444
|
}
|
407
|
-
return new Path(
|
445
|
+
return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
|
408
446
|
}
|
409
447
|
}
|
@@ -34,6 +34,8 @@ export default class Rect2 {
|
|
34
34
|
get maxDimension(): number;
|
35
35
|
get topRight(): import("./Vec3").default;
|
36
36
|
get bottomLeft(): import("./Vec3").default;
|
37
|
+
get width(): number;
|
38
|
+
get height(): number;
|
37
39
|
getEdges(): LineSegment2[];
|
38
40
|
transformedBoundingBox(affineTransform: Mat33): Rect2;
|
39
41
|
/** @return true iff this is equal to [other] ± fuzz */
|
@@ -38,20 +38,31 @@ export default class Rect2 {
|
|
38
38
|
&& this.bottomRight.y >= other.bottomRight.y;
|
39
39
|
}
|
40
40
|
intersects(other) {
|
41
|
-
|
41
|
+
// Project along x/y axes.
|
42
|
+
const thisMinX = this.x;
|
43
|
+
const thisMaxX = thisMinX + this.w;
|
44
|
+
const otherMinX = other.x;
|
45
|
+
const otherMaxX = other.x + other.w;
|
46
|
+
if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
|
47
|
+
return false;
|
48
|
+
}
|
49
|
+
const thisMinY = this.y;
|
50
|
+
const thisMaxY = thisMinY + this.h;
|
51
|
+
const otherMinY = other.y;
|
52
|
+
const otherMaxY = other.y + other.h;
|
53
|
+
if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
|
54
|
+
return false;
|
55
|
+
}
|
56
|
+
return true;
|
42
57
|
}
|
43
58
|
// Returns the overlap of this and [other], or null, if no such
|
44
59
|
// overlap exists
|
45
60
|
intersection(other) {
|
46
|
-
|
47
|
-
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
|
48
|
-
// The intersection can't be outside of this rectangle
|
49
|
-
if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
|
50
|
-
return null;
|
51
|
-
}
|
52
|
-
else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
|
61
|
+
if (!this.intersects(other)) {
|
53
62
|
return null;
|
54
63
|
}
|
64
|
+
const topLeft = this.topLeft.zip(other.topLeft, Math.max);
|
65
|
+
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
|
55
66
|
return Rect2.fromCorners(topLeft, bottomRight);
|
56
67
|
}
|
57
68
|
// Returns a new rectangle containing both [this] and [other].
|
@@ -115,6 +126,12 @@ export default class Rect2 {
|
|
115
126
|
get bottomLeft() {
|
116
127
|
return this.topLeft.plus(Vec2.of(0, this.h));
|
117
128
|
}
|
129
|
+
get width() {
|
130
|
+
return this.w;
|
131
|
+
}
|
132
|
+
get height() {
|
133
|
+
return this.h;
|
134
|
+
}
|
118
135
|
// Returns edges in the order
|
119
136
|
// [ rightEdge, topEdge, leftEdge, bottomEdge ]
|
120
137
|
getEdges() {
|
@@ -23,7 +23,7 @@ export default class Display {
|
|
23
23
|
else {
|
24
24
|
throw new Error(`Unknown rendering mode, ${mode}!`);
|
25
25
|
}
|
26
|
-
const cacheBlockResolution = Vec2.of(
|
26
|
+
const cacheBlockResolution = Vec2.of(600, 600);
|
27
27
|
this.cache = new RenderingCache({
|
28
28
|
createRenderer: () => {
|
29
29
|
if (mode === RenderingMode.DummyRenderer) {
|
@@ -45,8 +45,9 @@ export default class Display {
|
|
45
45
|
},
|
46
46
|
blockResolution: cacheBlockResolution,
|
47
47
|
cacheSize: 500 * 500 * 4 * 200,
|
48
|
-
maxScale: 1.
|
49
|
-
minComponentsPerCache:
|
48
|
+
maxScale: 1.5,
|
49
|
+
minComponentsPerCache: 50,
|
50
|
+
minComponentsToUseCache: 120,
|
50
51
|
});
|
51
52
|
this.editor.notifier.on(EditorEventType.DisplayResized, event => {
|
52
53
|
var _a;
|
@@ -11,7 +11,7 @@ export default class CacheRecord {
|
|
11
11
|
this.allocd = true;
|
12
12
|
}
|
13
13
|
startRender() {
|
14
|
-
this.lastUsedCycle = this.cacheState.currentRenderingCycle
|
14
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
15
15
|
if (!this.allocd) {
|
16
16
|
throw new Error('Only alloc\'d canvases can be rendered to');
|
17
17
|
}
|
@@ -33,6 +33,7 @@ export default class CacheRecord {
|
|
33
33
|
}
|
34
34
|
this.allocd = true;
|
35
35
|
this.onBeforeDeallocCallback = newDeallocCallback;
|
36
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
36
37
|
}
|
37
38
|
getLastUsedCycle() {
|
38
39
|
return this.lastUsedCycle;
|
@@ -25,15 +25,7 @@ export class CacheRecordManager {
|
|
25
25
|
}
|
26
26
|
// Returns null if there are no cache records. Returns an unalloc'd record if one exists.
|
27
27
|
getLeastRecentlyUsedRecord() {
|
28
|
-
|
29
|
-
|
30
|
-
if (!rec.isAllocd()) {
|
31
|
-
return rec;
|
32
|
-
}
|
33
|
-
if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
|
34
|
-
lruSoFar = rec;
|
35
|
-
}
|
36
|
-
}
|
37
|
-
return lruSoFar;
|
28
|
+
this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
|
29
|
+
return this.cacheRecords[0];
|
38
30
|
}
|
39
31
|
}
|
@@ -22,15 +22,21 @@ export default class RenderingCache {
|
|
22
22
|
return;
|
23
23
|
}
|
24
24
|
if (!this.rootNode) {
|
25
|
-
//
|
26
|
-
const
|
25
|
+
// Adjust the node so that it has the correct aspect ratio
|
26
|
+
const res = this.partialSharedState.props.blockResolution;
|
27
27
|
const topLeft = visibleRect.topLeft;
|
28
|
-
this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y,
|
28
|
+
this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, res.x, res.y), this.getSharedState());
|
29
29
|
}
|
30
30
|
while (!this.rootNode.region.containsRect(visibleRect)) {
|
31
31
|
this.rootNode = this.rootNode.generateParent();
|
32
32
|
}
|
33
33
|
this.rootNode = (_a = this.rootNode.smallestChildContaining(visibleRect)) !== null && _a !== void 0 ? _a : this.rootNode;
|
34
|
-
|
34
|
+
const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
|
35
|
+
if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
|
36
|
+
this.rootNode.renderItems(screenRenderer, [image], viewport);
|
37
|
+
}
|
38
|
+
else {
|
39
|
+
image.render(screenRenderer, visibleRect);
|
40
|
+
}
|
35
41
|
}
|
36
42
|
}
|
@@ -133,7 +133,11 @@ export default class RenderingCacheNode {
|
|
133
133
|
const newItems = [];
|
134
134
|
// Divide [items] until nodes are leaves or smaller than this
|
135
135
|
for (const item of items) {
|
136
|
-
|
136
|
+
const bbox = item.getBBox();
|
137
|
+
if (!bbox.intersects(this.region)) {
|
138
|
+
continue;
|
139
|
+
}
|
140
|
+
if (bbox.maxDimension >= this.region.maxDimension) {
|
137
141
|
newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
|
138
142
|
}
|
139
143
|
else {
|
@@ -146,6 +150,9 @@ export default class RenderingCacheNode {
|
|
146
150
|
items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
|
147
151
|
return;
|
148
152
|
}
|
153
|
+
if (debugMode) {
|
154
|
+
screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
|
155
|
+
}
|
149
156
|
// Could we render direclty from [this] or do we need to recurse?
|
150
157
|
const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
|
151
158
|
if (!couldRender) {
|
@@ -159,7 +166,7 @@ export default class RenderingCacheNode {
|
|
159
166
|
// Determine whether we already have rendered the items
|
160
167
|
const leaves = [];
|
161
168
|
for (const item of items) {
|
162
|
-
leaves.push(...item.getLeavesIntersectingRegion(this.region));
|
169
|
+
leaves.push(...item.getLeavesIntersectingRegion(this.region, rect => rect.w / this.region.w < 2 / this.cacheState.props.blockResolution.x));
|
163
170
|
}
|
164
171
|
sortLeavesByZIndex(leaves);
|
165
172
|
const leavesByIds = this.computeSortedByLeafIds(leaves);
|
@@ -213,7 +220,7 @@ export default class RenderingCacheNode {
|
|
213
220
|
}
|
214
221
|
}
|
215
222
|
if (debugMode) {
|
216
|
-
screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.
|
223
|
+
screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
|
217
224
|
}
|
218
225
|
}
|
219
226
|
}
|
@@ -12,7 +12,7 @@ export const createCache = (onRenderAlloc, cacheOptions) => {
|
|
12
12
|
},
|
13
13
|
isOfCorrectType(renderer) {
|
14
14
|
return renderer instanceof DummyRenderer;
|
15
|
-
}, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0 }, cacheOptions));
|
15
|
+
}, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0, minComponentsToUseCache: 0 }, cacheOptions));
|
16
16
|
return {
|
17
17
|
cache,
|
18
18
|
editor
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
3
|
+
import { TextStyle } from '../../components/Text';
|
2
4
|
import Mat33 from '../../geometry/Mat33';
|
3
5
|
import { PathCommand } from '../../geometry/Path';
|
4
6
|
import Rect2 from '../../geometry/Rect2';
|
@@ -19,6 +21,7 @@ export interface RenderablePathSpec {
|
|
19
21
|
export default abstract class AbstractRenderer {
|
20
22
|
private viewport;
|
21
23
|
private selfTransform;
|
24
|
+
private transformStack;
|
22
25
|
protected constructor(viewport: Viewport);
|
23
26
|
protected getViewport(): Viewport;
|
24
27
|
abstract displaySize(): Vec2;
|
@@ -29,6 +32,7 @@ export default abstract class AbstractRenderer {
|
|
29
32
|
protected abstract moveTo(point: Point2): void;
|
30
33
|
protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
31
34
|
protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
35
|
+
abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
32
36
|
abstract isTooSmallToRender(rect: Rect2): boolean;
|
33
37
|
setDraftMode(_draftMode: boolean): void;
|
34
38
|
protected objectLevel: number;
|
@@ -37,12 +41,14 @@ export default abstract class AbstractRenderer {
|
|
37
41
|
drawPath(path: RenderablePathSpec): void;
|
38
42
|
drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
|
39
43
|
startObject(_boundingBox: Rect2, _clip?: boolean): void;
|
40
|
-
endObject(): void;
|
44
|
+
endObject(_loaderData?: LoadSaveDataTable): void;
|
41
45
|
protected getNestingLevel(): number;
|
42
46
|
abstract drawPoints(...points: Point2[]): void;
|
43
47
|
canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
|
44
48
|
renderFromOtherOfSameType(_renderTo: Mat33, other: AbstractRenderer): void;
|
45
49
|
setTransform(transform: Mat33 | null): void;
|
50
|
+
pushTransform(transform: Mat33): void;
|
51
|
+
popTransform(): void;
|
46
52
|
getCanvasToScreenTransform(): Mat33;
|
47
53
|
canvasToScreen(vec: Vec2): Vec2;
|
48
54
|
getSizeOfCanvasPixelOnScreen(): number;
|
@@ -11,6 +11,7 @@ export default class AbstractRenderer {
|
|
11
11
|
this.viewport = viewport;
|
12
12
|
// If null, this' transformation is linked to the Viewport
|
13
13
|
this.selfTransform = null;
|
14
|
+
this.transformStack = [];
|
14
15
|
this.objectLevel = 0;
|
15
16
|
this.currentPaths = null;
|
16
17
|
}
|
@@ -79,7 +80,7 @@ export default class AbstractRenderer {
|
|
79
80
|
this.currentPaths = [];
|
80
81
|
this.objectLevel++;
|
81
82
|
}
|
82
|
-
endObject() {
|
83
|
+
endObject(_loaderData) {
|
83
84
|
// Render the paths all at once
|
84
85
|
this.flushPath();
|
85
86
|
this.currentPaths = null;
|
@@ -104,6 +105,17 @@ export default class AbstractRenderer {
|
|
104
105
|
setTransform(transform) {
|
105
106
|
this.selfTransform = transform;
|
106
107
|
}
|
108
|
+
pushTransform(transform) {
|
109
|
+
this.transformStack.push(this.selfTransform);
|
110
|
+
this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
|
111
|
+
}
|
112
|
+
popTransform() {
|
113
|
+
var _a;
|
114
|
+
if (this.transformStack.length === 0) {
|
115
|
+
throw new Error('Unable to pop more transforms than have been pushed!');
|
116
|
+
}
|
117
|
+
this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
|
118
|
+
}
|
107
119
|
// Get the matrix that transforms a vector on the canvas to a vector on this'
|
108
120
|
// rendering target.
|
109
121
|
getCanvasToScreenTransform() {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
1
2
|
import Mat33 from '../../geometry/Mat33';
|
2
3
|
import Rect2 from '../../geometry/Rect2';
|
3
4
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
@@ -12,6 +13,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
12
13
|
private minRenderSizeAnyDimen;
|
13
14
|
private minRenderSizeBothDimens;
|
14
15
|
constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
|
16
|
+
private transformBy;
|
15
17
|
canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
|
16
18
|
renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;
|
17
19
|
setDraftMode(draftMode: boolean): void;
|
@@ -24,6 +26,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
24
26
|
protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
25
27
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
26
28
|
drawPath(path: RenderablePathSpec): void;
|
29
|
+
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
27
30
|
private clipLevels;
|
28
31
|
startObject(boundingBox: Rect2, clip: boolean): void;
|
29
32
|
endObject(): void;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import Text from '../../components/Text';
|
2
3
|
import { Vec2 } from '../../geometry/Vec2';
|
3
4
|
import AbstractRenderer from './AbstractRenderer';
|
4
5
|
export default class CanvasRenderer extends AbstractRenderer {
|
@@ -10,6 +11,16 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
10
11
|
this.clipLevels = [];
|
11
12
|
this.setDraftMode(false);
|
12
13
|
}
|
14
|
+
transformBy(transformBy) {
|
15
|
+
// From MDN, transform(a,b,c,d,e,f)
|
16
|
+
// takes input such that
|
17
|
+
// ⎡ a c e ⎤
|
18
|
+
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
19
|
+
// ⎣ 0 0 1 ⎦
|
20
|
+
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
21
|
+
transformBy.a2, transformBy.b2, // c, d
|
22
|
+
transformBy.a3, transformBy.b3);
|
23
|
+
}
|
13
24
|
canRenderFromWithoutDataLoss(other) {
|
14
25
|
return other instanceof CanvasRenderer;
|
15
26
|
}
|
@@ -19,14 +30,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
19
30
|
}
|
20
31
|
transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
|
21
32
|
this.ctx.save();
|
22
|
-
|
23
|
-
// takes input such that
|
24
|
-
// ⎡ a c e ⎤
|
25
|
-
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
26
|
-
// ⎣ 0 0 1 ⎦
|
27
|
-
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
28
|
-
transformBy.a2, transformBy.b2, // c, d
|
29
|
-
transformBy.a3, transformBy.b3);
|
33
|
+
this.transformBy(transformBy);
|
30
34
|
this.ctx.drawImage(other.ctx.canvas, 0, 0);
|
31
35
|
this.ctx.restore();
|
32
36
|
}
|
@@ -105,6 +109,22 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
105
109
|
}
|
106
110
|
super.drawPath(path);
|
107
111
|
}
|
112
|
+
drawText(text, transform, style) {
|
113
|
+
this.ctx.save();
|
114
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
115
|
+
this.transformBy(transform);
|
116
|
+
Text.applyTextStyles(this.ctx, style);
|
117
|
+
if (style.renderingStyle.fill.a !== 0) {
|
118
|
+
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
|
119
|
+
this.ctx.fillText(text, 0, 0);
|
120
|
+
}
|
121
|
+
if (style.renderingStyle.stroke) {
|
122
|
+
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
|
123
|
+
this.ctx.lineWidth = style.renderingStyle.stroke.width;
|
124
|
+
this.ctx.strokeText(text, 0, 0);
|
125
|
+
}
|
126
|
+
this.ctx.restore();
|
127
|
+
}
|
108
128
|
startObject(boundingBox, clip) {
|
109
129
|
if (this.isTooSmallToRender(boundingBox)) {
|
110
130
|
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
1
2
|
import Mat33 from '../../geometry/Mat33';
|
2
3
|
import Rect2 from '../../geometry/Rect2';
|
3
4
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
@@ -10,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
10
11
|
lastFillStyle: RenderingStyle | null;
|
11
12
|
lastPoint: Point2 | null;
|
12
13
|
objectNestingLevel: number;
|
14
|
+
lastText: string | null;
|
13
15
|
pointBuffer: Point2[];
|
14
16
|
constructor(viewport: Viewport);
|
15
17
|
displaySize(): Vec2;
|
@@ -21,6 +23,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
21
23
|
protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
|
22
24
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
23
25
|
drawPoints(..._points: Vec3[]): void;
|
26
|
+
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
|
24
27
|
startObject(boundingBox: Rect2, _clip: boolean): void;
|
25
28
|
endObject(): void;
|
26
29
|
isTooSmallToRender(_rect: Rect2): boolean;
|
@@ -10,6 +10,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
10
10
|
this.lastFillStyle = null;
|
11
11
|
this.lastPoint = null;
|
12
12
|
this.objectNestingLevel = 0;
|
13
|
+
this.lastText = null;
|
13
14
|
// List of points drawn since the last clear.
|
14
15
|
this.pointBuffer = [];
|
15
16
|
}
|
@@ -28,6 +29,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
28
29
|
this.clearedCount++;
|
29
30
|
this.renderedPathCount = 0;
|
30
31
|
this.pointBuffer = [];
|
32
|
+
this.lastText = null;
|
31
33
|
// Ensure all objects finished rendering
|
32
34
|
if (this.objectNestingLevel > 0) {
|
33
35
|
throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
|
@@ -68,6 +70,9 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
68
70
|
// drawPoints is intended for debugging.
|
69
71
|
// As such, it is unlikely to be the target of automated tests.
|
70
72
|
}
|
73
|
+
drawText(text, _transform, _style) {
|
74
|
+
this.lastText = text;
|
75
|
+
}
|
71
76
|
startObject(boundingBox, _clip) {
|
72
77
|
super.startObject(boundingBox);
|
73
78
|
this.objectNestingLevel += 1;
|