js-draw 0.0.10 → 0.1.2
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 +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +17 -7
- package/dist/src/EditorImage.d.ts +15 -7
- package/dist/src/EditorImage.js +46 -37
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +6 -2
- package/dist/src/SVGLoader.js +20 -8
- package/dist/src/Viewport.d.ts +4 -0
- package/dist/src/Viewport.js +51 -0
- package/dist/src/components/AbstractComponent.d.ts +9 -2
- package/dist/src/components/AbstractComponent.js +14 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
- package/dist/src/components/Stroke.d.ts +1 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.js +1 -1
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/geometry/Mat33.js +3 -0
- package/dist/src/geometry/Path.d.ts +1 -1
- package/dist/src/geometry/Path.js +102 -69
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +47 -9
- package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
- package/dist/src/{Display.js → rendering/Display.js} +34 -4
- package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
- package/dist/src/rendering/caching/CacheRecord.js +52 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
- package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
- package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
- package/dist/src/rendering/caching/RenderingCache.js +42 -0
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
- package/dist/src/rendering/caching/testUtils.d.ts +9 -0
- package/dist/src/rendering/caching/testUtils.js +20 -0
- package/dist/src/rendering/caching/types.d.ts +21 -0
- package/dist/src/rendering/caching/types.js +1 -0
- package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +20 -9
- package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -3
- package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
- package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
- package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
- package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
- package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +7 -5
- package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +35 -18
- package/dist/src/testing/createEditor.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +165 -154
- package/dist/src/toolbar/icons.d.ts +10 -0
- package/dist/src/toolbar/icons.js +180 -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 +9 -24
- package/dist/src/tools/ToolController.d.ts +5 -6
- package/dist/src/tools/ToolController.js +8 -10
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/Editor.ts +19 -8
- package/src/EditorImage.test.ts +2 -2
- package/src/EditorImage.ts +58 -42
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +36 -10
- package/src/Viewport.ts +68 -0
- package/src/components/AbstractComponent.ts +21 -2
- package/src/components/SVGGlobalAttributesObject.ts +2 -2
- package/src/components/Stroke.ts +2 -2
- package/src/components/UnknownSVGObject.ts +2 -2
- package/src/components/builders/ArrowBuilder.ts +1 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/builders/LineBuilder.ts +1 -1
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/builders/types.ts +1 -1
- package/src/geometry/Mat33.ts +3 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +12 -2
- package/src/geometry/Path.ts +107 -71
- package/src/geometry/Rect2.test.ts +47 -8
- package/src/geometry/Rect2.ts +57 -9
- package/src/{Display.ts → rendering/Display.ts} +39 -6
- package/src/rendering/caching/CacheRecord.test.ts +49 -0
- package/src/rendering/caching/CacheRecord.ts +73 -0
- package/src/rendering/caching/CacheRecordManager.ts +45 -0
- package/src/rendering/caching/RenderingCache.test.ts +44 -0
- package/src/rendering/caching/RenderingCache.ts +63 -0
- package/src/rendering/caching/RenderingCacheNode.ts +378 -0
- package/src/rendering/caching/testUtils.ts +35 -0
- package/src/rendering/caching/types.ts +39 -0
- package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -9
- package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
- package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
- package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
- package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +39 -23
- package/src/testing/createEditor.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +199 -170
- package/src/toolbar/icons.ts +203 -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.test.ts +1 -1
- package/src/tools/SelectionTool.ts +12 -33
- package/src/tools/ToolController.ts +3 -5
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +10 -3
- package/tsconfig.json +1 -0
- package/dist/__mocks__/coloris.d.ts +0 -2
- package/dist/__mocks__/coloris.js +0 -5
@@ -41,7 +41,7 @@ export default class Stroke extends AbstractComponent {
|
|
41
41
|
canvas.drawPath(part);
|
42
42
|
}
|
43
43
|
}
|
44
|
-
canvas.endObject();
|
44
|
+
canvas.endObject(this.getLoadSaveData());
|
45
45
|
}
|
46
46
|
// Grows the bounding box for a given stroke part based on that part's style.
|
47
47
|
bboxForPart(origBBox, style) {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import LineSegment2 from '../geometry/LineSegment2';
|
2
2
|
import Mat33 from '../geometry/Mat33';
|
3
3
|
import Rect2 from '../geometry/Rect2';
|
4
|
-
import AbstractRenderer from '../rendering/AbstractRenderer';
|
4
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import AbstractComponent from './AbstractComponent';
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
7
|
export default class UnknownSVGObject extends AbstractComponent {
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Rect2 from '../geometry/Rect2';
|
2
|
-
import SVGRenderer from '../rendering/SVGRenderer';
|
2
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
3
3
|
import AbstractComponent from './AbstractComponent';
|
4
4
|
export default class UnknownSVGObject extends AbstractComponent {
|
5
5
|
constructor(svgObject) {
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Rect2 from '../../geometry/Rect2';
|
2
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
3
3
|
import { StrokeDataPoint } from '../../types';
|
4
4
|
import AbstractComponent from '../AbstractComponent';
|
5
5
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
@@ -160,7 +160,7 @@ export default class FreehandLineBuilder {
|
|
160
160
|
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
161
161
|
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
162
162
|
// If the boundaries have two intersections, increasing the half vector's length could fix this.
|
163
|
-
if (upperBoundary.intersects(lowerBoundary).length
|
163
|
+
if (upperBoundary.intersects(lowerBoundary).length > 0) {
|
164
164
|
halfVec = halfVec.times(2);
|
165
165
|
}
|
166
166
|
const pathCommands = [
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Rect2 from '../../geometry/Rect2';
|
2
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
3
3
|
import { StrokeDataPoint } from '../../types';
|
4
4
|
import AbstractComponent from '../AbstractComponent';
|
5
5
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Rect2 from '../../geometry/Rect2';
|
2
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
3
3
|
import { StrokeDataPoint } from '../../types';
|
4
4
|
import AbstractComponent from '../AbstractComponent';
|
5
5
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Rect2 from '../../geometry/Rect2';
|
2
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
3
3
|
import { StrokeDataPoint } from '../../types';
|
4
4
|
import Viewport from '../../Viewport';
|
5
5
|
import AbstractComponent from '../AbstractComponent';
|
@@ -4,6 +4,9 @@ import Vec3 from './Vec3';
|
|
4
4
|
// a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
5
5
|
// **and** translates while a linear transformation just scales/rotates/shears).
|
6
6
|
export default class Mat33 {
|
7
|
+
// ⎡ a1 a2 a3 ⎤
|
8
|
+
// ⎢ b1 b2 b3 ⎥
|
9
|
+
// ⎣ c1 c2 c3 ⎦
|
7
10
|
constructor(a1, a2, a3, b1, b2, b3, c1, c2, c3) {
|
8
11
|
this.a1 = a1;
|
9
12
|
this.a2 = a2;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Bezier } from 'bezier-js';
|
2
|
-
import { RenderingStyle, RenderablePathSpec } from '../rendering/AbstractRenderer';
|
2
|
+
import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
|
3
3
|
import LineSegment2 from './LineSegment2';
|
4
4
|
import Mat33 from './Mat33';
|
5
5
|
import Rect2 from './Rect2';
|
@@ -208,8 +208,8 @@ export default class Path {
|
|
208
208
|
const toRoundedString = (num) => {
|
209
209
|
// Try to remove rounding errors. If the number ends in at least three/four zeroes
|
210
210
|
// (or nines) just one or two digits, it's probably a rounding error.
|
211
|
-
const fixRoundingUpExp = /^([-]?\d
|
212
|
-
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d
|
211
|
+
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
|
212
|
+
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
|
213
213
|
let text = num.toString();
|
214
214
|
if (text.indexOf('.') === -1) {
|
215
215
|
return text;
|
@@ -230,7 +230,9 @@ export default class Path {
|
|
230
230
|
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
|
231
231
|
}
|
232
232
|
text = text.replace(fixRoundingUpExp, '$1');
|
233
|
-
// Remove trailing
|
233
|
+
// Remove trailing zeroes
|
234
|
+
text = text.replace(/([.][^0]*)0+$/, '$1');
|
235
|
+
// Remove trailing period
|
234
236
|
return text.replace(/[.]$/, '');
|
235
237
|
};
|
236
238
|
const addCommand = (command, ...points) => {
|
@@ -273,10 +275,12 @@ export default class Path {
|
|
273
275
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
|
274
276
|
// and
|
275
277
|
// https://www.w3.org/TR/SVG2/paths.html
|
278
|
+
var _a;
|
276
279
|
// Remove linebreaks
|
277
280
|
pathString = pathString.split('\n').join(' ');
|
278
281
|
let lastPos = Vec2.zero;
|
279
282
|
let firstPos = null;
|
283
|
+
let startPos = null;
|
280
284
|
let isFirstCommand = true;
|
281
285
|
const commands = [];
|
282
286
|
const moveTo = (point) => {
|
@@ -315,15 +319,61 @@ export default class Path {
|
|
315
319
|
endPoint,
|
316
320
|
});
|
317
321
|
};
|
322
|
+
const commandArgCounts = {
|
323
|
+
'm': 1,
|
324
|
+
'l': 1,
|
325
|
+
'c': 3,
|
326
|
+
'q': 2,
|
327
|
+
'z': 0,
|
328
|
+
'h': 1,
|
329
|
+
'v': 1,
|
330
|
+
};
|
318
331
|
// Each command: Command character followed by anything that isn't a command character
|
319
|
-
const commandExp = /([
|
332
|
+
const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
|
320
333
|
let current;
|
321
334
|
while ((current = commandExp.exec(pathString)) !== null) {
|
322
|
-
const argParts = current[2].trim().split(/[^0-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
335
|
+
const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
|
336
|
+
// As of 09/2022, iOS Safari doesn't support support lookbehind in regular
|
337
|
+
// expressions. As such, we need an alternative.
|
338
|
+
// Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
|
339
|
+
// we need special cases:
|
340
|
+
current = current.replace(/([^eE])[-]/g, '$1 -');
|
341
|
+
const parts = current.split(' -');
|
342
|
+
if (parts[0] !== '') {
|
343
|
+
accumualtor.push(parts[0]);
|
344
|
+
}
|
345
|
+
accumualtor.push(...parts.slice(1).map(part => `-${part}`));
|
346
|
+
return accumualtor;
|
347
|
+
}, []);
|
348
|
+
let numericArgs = argParts.map(arg => parseFloat(arg));
|
349
|
+
let commandChar = current[1].toLowerCase();
|
350
|
+
let uppercaseCommand = current[1] !== commandChar;
|
351
|
+
// Convert commands that don't take points into commands that do.
|
352
|
+
if (commandChar === 'v' || commandChar === 'h') {
|
353
|
+
numericArgs = numericArgs.reduce((accumulator, current) => {
|
354
|
+
if (commandChar === 'v') {
|
355
|
+
return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
|
356
|
+
}
|
357
|
+
else {
|
358
|
+
return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
|
359
|
+
}
|
360
|
+
}, []);
|
361
|
+
commandChar = 'l';
|
362
|
+
}
|
363
|
+
else if (commandChar === 'z') {
|
364
|
+
if (firstPos) {
|
365
|
+
numericArgs = [firstPos.x, firstPos.y];
|
366
|
+
firstPos = lastPos;
|
367
|
+
}
|
368
|
+
else {
|
369
|
+
continue;
|
370
|
+
}
|
371
|
+
// 'z' always acts like an uppercase lineTo(startPos)
|
372
|
+
uppercaseCommand = true;
|
373
|
+
commandChar = 'l';
|
374
|
+
}
|
375
|
+
const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
|
376
|
+
const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
|
327
377
|
if (index % 2 !== 0) {
|
328
378
|
const currentAsFloat = current;
|
329
379
|
const prevAsFloat = parts[index - 1];
|
@@ -332,76 +382,59 @@ export default class Path {
|
|
332
382
|
else {
|
333
383
|
return accumulator;
|
334
384
|
}
|
335
|
-
}, []).map((coordinate) => {
|
385
|
+
}, []).map((coordinate, index) => {
|
336
386
|
// Lowercase commands are relative, uppercase commands use absolute
|
337
387
|
// positioning
|
388
|
+
let newPos;
|
338
389
|
if (uppercaseCommand) {
|
339
|
-
|
340
|
-
return coordinate;
|
390
|
+
newPos = coordinate;
|
341
391
|
}
|
342
392
|
else {
|
343
|
-
|
344
|
-
|
393
|
+
newPos = lastPos.plus(coordinate);
|
394
|
+
}
|
395
|
+
if ((index + 1) % commandArgCount === 0) {
|
396
|
+
lastPos = newPos;
|
345
397
|
}
|
398
|
+
return newPos;
|
346
399
|
});
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
case 'l':
|
354
|
-
expectedPointArgCount = 1;
|
355
|
-
lineTo(args[0]);
|
356
|
-
break;
|
357
|
-
case 'z':
|
358
|
-
expectedPointArgCount = 0;
|
359
|
-
// firstPos can be null if the stroke data is just 'z'.
|
360
|
-
if (firstPos) {
|
361
|
-
lineTo(firstPos);
|
362
|
-
}
|
363
|
-
break;
|
364
|
-
case 'c':
|
365
|
-
expectedPointArgCount = 3;
|
366
|
-
cubicBezierTo(args[0], args[1], args[2]);
|
367
|
-
break;
|
368
|
-
case 'q':
|
369
|
-
expectedPointArgCount = 2;
|
370
|
-
quadraticBeierTo(args[0], args[1]);
|
371
|
-
break;
|
372
|
-
// Horizontal line
|
373
|
-
case 'h':
|
374
|
-
expectedPointArgCount = 0;
|
375
|
-
if (uppercaseCommand) {
|
376
|
-
lineTo(Vec2.of(numericArgs[0], lastPos.y));
|
377
|
-
}
|
378
|
-
else {
|
379
|
-
lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
|
380
|
-
}
|
381
|
-
break;
|
382
|
-
// Vertical line
|
383
|
-
case 'v':
|
384
|
-
expectedPointArgCount = 0;
|
385
|
-
if (uppercaseCommand) {
|
386
|
-
lineTo(Vec2.of(lastPos.x, numericArgs[1]));
|
387
|
-
}
|
388
|
-
else {
|
389
|
-
lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
|
390
|
-
}
|
391
|
-
break;
|
392
|
-
default:
|
393
|
-
throw new Error(`Unknown path command ${commandChar}`);
|
400
|
+
if (allArgs.length % commandArgCount !== 0) {
|
401
|
+
throw new Error([
|
402
|
+
`Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
|
403
|
+
`The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
|
404
|
+
`Command: ${current[0]}`,
|
405
|
+
].join('\n'));
|
394
406
|
}
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
407
|
+
for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
|
408
|
+
const args = allArgs.slice(argPos, argPos + commandArgCount);
|
409
|
+
switch (commandChar.toLowerCase()) {
|
410
|
+
case 'm':
|
411
|
+
if (argPos === 0) {
|
412
|
+
moveTo(args[0]);
|
413
|
+
}
|
414
|
+
else {
|
415
|
+
lineTo(args[0]);
|
416
|
+
}
|
417
|
+
break;
|
418
|
+
case 'l':
|
419
|
+
lineTo(args[0]);
|
420
|
+
break;
|
421
|
+
case 'c':
|
422
|
+
cubicBezierTo(args[0], args[1], args[2]);
|
423
|
+
break;
|
424
|
+
case 'q':
|
425
|
+
quadraticBeierTo(args[0], args[1]);
|
426
|
+
break;
|
427
|
+
default:
|
428
|
+
throw new Error(`Unknown path command ${commandChar}`);
|
429
|
+
}
|
430
|
+
isFirstCommand = false;
|
399
431
|
}
|
400
|
-
if (
|
401
|
-
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos =
|
432
|
+
if (allArgs.length > 0) {
|
433
|
+
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
|
434
|
+
startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
|
435
|
+
lastPos = allArgs[allArgs.length - 1];
|
402
436
|
}
|
403
|
-
isFirstCommand = false;
|
404
437
|
}
|
405
|
-
return new Path(
|
438
|
+
return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
|
406
439
|
}
|
407
440
|
}
|
@@ -27,6 +27,7 @@ export default class Rect2 {
|
|
27
27
|
intersects(other: Rect2): boolean;
|
28
28
|
intersection(other: Rect2): Rect2 | null;
|
29
29
|
union(other: Rect2): Rect2;
|
30
|
+
divideIntoGrid(columns: number, rows: number): Rect2[];
|
30
31
|
grownToPoint(point: Point2, margin?: number): Rect2;
|
31
32
|
grownBy(margin: number): Rect2;
|
32
33
|
get corners(): Point2[];
|
@@ -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].
|
@@ -60,6 +71,33 @@ export default class Rect2 {
|
|
60
71
|
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.max);
|
61
72
|
return Rect2.fromCorners(topLeft, bottomRight);
|
62
73
|
}
|
74
|
+
// Returns a the subdivision of this into [columns] columns
|
75
|
+
// and [rows] rows. For example,
|
76
|
+
// Rect2.unitSquare.divideIntoGrid(2, 2)
|
77
|
+
// -> [ Rect2(0, 0, 0.5, 0.5), Rect2(0.5, 0, 0.5, 0.5), Rect2(0, 0.5, 0.5, 0.5), Rect2(0.5, 0.5, 0.5, 0.5) ]
|
78
|
+
// The rectangles are ordered in row-major order.
|
79
|
+
divideIntoGrid(columns, rows) {
|
80
|
+
const result = [];
|
81
|
+
if (columns <= 0 || rows <= 0) {
|
82
|
+
return result;
|
83
|
+
}
|
84
|
+
const eachRectWidth = this.w / columns;
|
85
|
+
const eachRectHeight = this.h / rows;
|
86
|
+
if (eachRectWidth === 0) {
|
87
|
+
columns = 1;
|
88
|
+
}
|
89
|
+
if (eachRectHeight === 0) {
|
90
|
+
rows = 1;
|
91
|
+
}
|
92
|
+
for (let j = 0; j < rows; j++) {
|
93
|
+
for (let i = 0; i < columns; i++) {
|
94
|
+
const x = eachRectWidth * i + this.x;
|
95
|
+
const y = eachRectHeight * j + this.y;
|
96
|
+
result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
|
97
|
+
}
|
98
|
+
}
|
99
|
+
return result;
|
100
|
+
}
|
63
101
|
// Returns a rectangle containing this and [point].
|
64
102
|
// [margin] is the minimum distance between the new point and the edge
|
65
103
|
// of the resultant rectangle.
|
@@ -1,5 +1,6 @@
|
|
1
|
-
import AbstractRenderer from './
|
2
|
-
import { Editor } from '
|
1
|
+
import AbstractRenderer from './renderers/AbstractRenderer';
|
2
|
+
import { Editor } from '../Editor';
|
3
|
+
import RenderingCache from './caching/RenderingCache';
|
3
4
|
export declare enum RenderingMode {
|
4
5
|
DummyRenderer = 0,
|
5
6
|
CanvasRenderer = 1
|
@@ -9,11 +10,13 @@ export default class Display {
|
|
9
10
|
private parent;
|
10
11
|
private dryInkRenderer;
|
11
12
|
private wetInkRenderer;
|
13
|
+
private cache;
|
12
14
|
private resizeSurfacesCallback?;
|
13
15
|
private flattenCallback?;
|
14
16
|
constructor(editor: Editor, mode: RenderingMode, parent: HTMLElement | null);
|
15
17
|
get width(): number;
|
16
18
|
get height(): number;
|
19
|
+
getCache(): RenderingCache;
|
17
20
|
private initializeCanvasRendering;
|
18
21
|
startRerender(): AbstractRenderer;
|
19
22
|
setDraftMode(draftMode: boolean): void;
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import CanvasRenderer from './
|
2
|
-
import { EditorEventType } from '
|
3
|
-
import DummyRenderer from './
|
4
|
-
import { Vec2 } from '
|
1
|
+
import CanvasRenderer from './renderers/CanvasRenderer';
|
2
|
+
import { EditorEventType } from '../types';
|
3
|
+
import DummyRenderer from './renderers/DummyRenderer';
|
4
|
+
import { Vec2 } from '../geometry/Vec2';
|
5
|
+
import RenderingCache from './caching/RenderingCache';
|
5
6
|
export var RenderingMode;
|
6
7
|
(function (RenderingMode) {
|
7
8
|
RenderingMode[RenderingMode["DummyRenderer"] = 0] = "DummyRenderer";
|
@@ -22,6 +23,32 @@ export default class Display {
|
|
22
23
|
else {
|
23
24
|
throw new Error(`Unknown rendering mode, ${mode}!`);
|
24
25
|
}
|
26
|
+
const cacheBlockResolution = Vec2.of(600, 600);
|
27
|
+
this.cache = new RenderingCache({
|
28
|
+
createRenderer: () => {
|
29
|
+
if (mode === RenderingMode.DummyRenderer) {
|
30
|
+
return new DummyRenderer(editor.viewport);
|
31
|
+
}
|
32
|
+
else if (mode !== RenderingMode.CanvasRenderer) {
|
33
|
+
throw new Error('Unspported rendering mode');
|
34
|
+
}
|
35
|
+
// Make the canvas slightly larger than each cache block to prevent
|
36
|
+
// seams.
|
37
|
+
const canvas = document.createElement('canvas');
|
38
|
+
canvas.width = cacheBlockResolution.x + 1;
|
39
|
+
canvas.height = cacheBlockResolution.y + 1;
|
40
|
+
const ctx = canvas.getContext('2d');
|
41
|
+
return new CanvasRenderer(ctx, editor.viewport);
|
42
|
+
},
|
43
|
+
isOfCorrectType: (renderer) => {
|
44
|
+
return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
|
45
|
+
},
|
46
|
+
blockResolution: cacheBlockResolution,
|
47
|
+
cacheSize: 500 * 500 * 4 * 200,
|
48
|
+
maxScale: 1.5,
|
49
|
+
minComponentsPerCache: 50,
|
50
|
+
minComponentsToUseCache: 120,
|
51
|
+
});
|
25
52
|
this.editor.notifier.on(EditorEventType.DisplayResized, event => {
|
26
53
|
var _a;
|
27
54
|
if (event.kind !== EditorEventType.DisplayResized) {
|
@@ -39,6 +66,9 @@ export default class Display {
|
|
39
66
|
get height() {
|
40
67
|
return this.dryInkRenderer.displaySize().y;
|
41
68
|
}
|
69
|
+
getCache() {
|
70
|
+
return this.cache;
|
71
|
+
}
|
42
72
|
initializeCanvasRendering() {
|
43
73
|
const dryInkCanvas = document.createElement('canvas');
|
44
74
|
const wetInkCanvas = document.createElement('canvas');
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import Mat33 from '../../geometry/Mat33';
|
2
|
+
import Rect2 from '../../geometry/Rect2';
|
3
|
+
import AbstractRenderer from '../renderers/AbstractRenderer';
|
4
|
+
import { BeforeDeallocCallback, CacheState } from './types';
|
5
|
+
export default class CacheRecord {
|
6
|
+
private onBeforeDeallocCallback;
|
7
|
+
private cacheState;
|
8
|
+
private renderer;
|
9
|
+
private lastUsedCycle;
|
10
|
+
private allocd;
|
11
|
+
constructor(onBeforeDeallocCallback: BeforeDeallocCallback | null, cacheState: CacheState);
|
12
|
+
startRender(): AbstractRenderer;
|
13
|
+
dealloc(): void;
|
14
|
+
isAllocd(): boolean;
|
15
|
+
realloc(newDeallocCallback: BeforeDeallocCallback): void;
|
16
|
+
getLastUsedCycle(): number;
|
17
|
+
getTransform(drawTo: Rect2): Mat33;
|
18
|
+
setRenderingRegion(drawTo: Rect2): void;
|
19
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import Mat33 from '../../geometry/Mat33';
|
2
|
+
// Represents a cached renderer/canvas
|
3
|
+
// This is not a [CacheNode] -- it handles cached renderers and does not have sub-renderers.
|
4
|
+
export default class CacheRecord {
|
5
|
+
constructor(onBeforeDeallocCallback, cacheState) {
|
6
|
+
this.onBeforeDeallocCallback = onBeforeDeallocCallback;
|
7
|
+
this.cacheState = cacheState;
|
8
|
+
this.allocd = false;
|
9
|
+
this.renderer = cacheState.props.createRenderer();
|
10
|
+
this.lastUsedCycle = -1;
|
11
|
+
this.allocd = true;
|
12
|
+
}
|
13
|
+
startRender() {
|
14
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
15
|
+
if (!this.allocd) {
|
16
|
+
throw new Error('Only alloc\'d canvases can be rendered to');
|
17
|
+
}
|
18
|
+
return this.renderer;
|
19
|
+
}
|
20
|
+
dealloc() {
|
21
|
+
var _a;
|
22
|
+
(_a = this.onBeforeDeallocCallback) === null || _a === void 0 ? void 0 : _a.call(this);
|
23
|
+
this.allocd = false;
|
24
|
+
this.onBeforeDeallocCallback = null;
|
25
|
+
this.lastUsedCycle = 0;
|
26
|
+
}
|
27
|
+
isAllocd() {
|
28
|
+
return this.allocd;
|
29
|
+
}
|
30
|
+
realloc(newDeallocCallback) {
|
31
|
+
if (this.allocd) {
|
32
|
+
this.dealloc();
|
33
|
+
}
|
34
|
+
this.allocd = true;
|
35
|
+
this.onBeforeDeallocCallback = newDeallocCallback;
|
36
|
+
this.lastUsedCycle = this.cacheState.currentRenderingCycle;
|
37
|
+
}
|
38
|
+
getLastUsedCycle() {
|
39
|
+
return this.lastUsedCycle;
|
40
|
+
}
|
41
|
+
// Returns the transformation that maps [drawTo] to this' renderable region
|
42
|
+
// (i.e. a [cacheProps.blockResolution]-sized rectangle with top left at (0, 0))
|
43
|
+
getTransform(drawTo) {
|
44
|
+
const transform = Mat33.scaling2D(this.cacheState.props.blockResolution.x / drawTo.size.x).rightMul(Mat33.translation(drawTo.topLeft.times(-1)));
|
45
|
+
return transform;
|
46
|
+
}
|
47
|
+
setRenderingRegion(drawTo) {
|
48
|
+
this.renderer.setTransform(
|
49
|
+
// Invert to map objects instead of the viewport
|
50
|
+
this.getTransform(drawTo));
|
51
|
+
}
|
52
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { BeforeDeallocCallback, PartialCacheState } from './types';
|
2
|
+
import CacheRecord from './CacheRecord';
|
3
|
+
import Rect2 from '../../geometry/Rect2';
|
4
|
+
export declare class CacheRecordManager {
|
5
|
+
private readonly cacheState;
|
6
|
+
private cacheRecords;
|
7
|
+
private maxCanvases;
|
8
|
+
constructor(cacheState: PartialCacheState);
|
9
|
+
allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord;
|
10
|
+
private getLeastRecentlyUsedRecord;
|
11
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import CacheRecord from './CacheRecord';
|
2
|
+
export class CacheRecordManager {
|
3
|
+
constructor(cacheState) {
|
4
|
+
this.cacheState = cacheState;
|
5
|
+
// Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
|
6
|
+
this.cacheRecords = [];
|
7
|
+
const cacheProps = cacheState.props;
|
8
|
+
this.maxCanvases = Math.ceil(
|
9
|
+
// Assuming four components per pixel:
|
10
|
+
cacheProps.cacheSize / 4 / cacheProps.blockResolution.x / cacheProps.blockResolution.y);
|
11
|
+
}
|
12
|
+
allocCanvas(drawTo, onDealloc) {
|
13
|
+
if (this.cacheRecords.length < this.maxCanvases) {
|
14
|
+
const record = new CacheRecord(onDealloc, Object.assign(Object.assign({}, this.cacheState), { recordManager: this }));
|
15
|
+
record.setRenderingRegion(drawTo);
|
16
|
+
this.cacheRecords.push(record);
|
17
|
+
return record;
|
18
|
+
}
|
19
|
+
else {
|
20
|
+
const lru = this.getLeastRecentlyUsedRecord();
|
21
|
+
lru.realloc(onDealloc);
|
22
|
+
lru.setRenderingRegion(drawTo);
|
23
|
+
return lru;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
// Returns null if there are no cache records. Returns an unalloc'd record if one exists.
|
27
|
+
getLeastRecentlyUsedRecord() {
|
28
|
+
this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
|
29
|
+
return this.cacheRecords[0];
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { ImageNode } from '../../EditorImage';
|
2
|
+
import Viewport from '../../Viewport';
|
3
|
+
import AbstractRenderer from '../renderers/AbstractRenderer';
|
4
|
+
import { CacheProps, CacheState } from './types';
|
5
|
+
export default class RenderingCache {
|
6
|
+
private partialSharedState;
|
7
|
+
private recordManager;
|
8
|
+
private rootNode;
|
9
|
+
constructor(cacheProps: CacheProps);
|
10
|
+
getSharedState(): CacheState;
|
11
|
+
render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
|
12
|
+
}
|