js-draw 1.7.2 → 1.8.0
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/dist/Editor.css +1 -0
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/EventDispatcher.js +2 -1
- package/dist/cjs/Viewport.d.ts +2 -0
- package/dist/cjs/Viewport.js +2 -0
- package/dist/cjs/components/AbstractComponent.d.ts +9 -0
- package/dist/cjs/components/AbstractComponent.js +11 -0
- package/dist/cjs/components/Stroke.d.ts +3 -0
- package/dist/cjs/components/Stroke.js +55 -1
- package/dist/cjs/image/EditorImage.js +17 -1
- package/dist/cjs/rendering/RenderablePathSpec.d.ts +20 -2
- package/dist/cjs/rendering/RenderablePathSpec.js +72 -9
- package/dist/cjs/rendering/caching/CacheRecord.js +5 -3
- package/dist/cjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
- package/dist/cjs/rendering/renderers/AbstractRenderer.js +12 -3
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +2 -2
- package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +0 -10
- package/dist/cjs/rendering/renderers/SVGRenderer.js +0 -14
- package/dist/cjs/testing/startPinchGesture.d.ts +14 -0
- package/dist/cjs/testing/startPinchGesture.js +41 -0
- package/dist/cjs/tools/PanZoom.d.ts +4 -0
- package/dist/cjs/tools/PanZoom.js +21 -2
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/EventDispatcher.mjs +2 -1
- package/dist/mjs/Viewport.d.ts +2 -0
- package/dist/mjs/Viewport.mjs +2 -0
- package/dist/mjs/components/AbstractComponent.d.ts +9 -0
- package/dist/mjs/components/AbstractComponent.mjs +11 -0
- package/dist/mjs/components/Stroke.d.ts +3 -0
- package/dist/mjs/components/Stroke.mjs +56 -2
- package/dist/mjs/image/EditorImage.mjs +17 -1
- package/dist/mjs/rendering/RenderablePathSpec.d.ts +20 -2
- package/dist/mjs/rendering/RenderablePathSpec.mjs +71 -9
- package/dist/mjs/rendering/caching/CacheRecord.mjs +5 -3
- package/dist/mjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
- package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +12 -3
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +2 -2
- package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +0 -10
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +0 -14
- package/dist/mjs/testing/startPinchGesture.d.ts +14 -0
- package/dist/mjs/testing/startPinchGesture.mjs +36 -0
- package/dist/mjs/tools/PanZoom.d.ts +4 -0
- package/dist/mjs/tools/PanZoom.mjs +21 -2
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
- package/src/Editor.scss +2 -0
@@ -149,7 +149,7 @@ class CanvasRenderer extends AbstractRenderer_1.default {
|
|
149
149
|
return;
|
150
150
|
}
|
151
151
|
// If part of a huge object, it might be worth trimming the path
|
152
|
-
const visibleRect = this.
|
152
|
+
const visibleRect = this.getVisibleRect();
|
153
153
|
if (this.currentObjectBBox?.containsRect(visibleRect)) {
|
154
154
|
// Try to trim/remove parts of the path outside of the bounding box.
|
155
155
|
path = (0, RenderablePathSpec_1.visualEquivalent)(path, visibleRect);
|
@@ -189,7 +189,7 @@ class CanvasRenderer extends AbstractRenderer_1.default {
|
|
189
189
|
if (!this.ignoringObject && clip) {
|
190
190
|
// Don't clip if it would only remove content already trimmed by
|
191
191
|
// the edge of the screen.
|
192
|
-
const clippedIsOutsideScreen = boundingBox.containsRect(this.
|
192
|
+
const clippedIsOutsideScreen = boundingBox.containsRect(this.getVisibleRect());
|
193
193
|
if (!clippedIsOutsideScreen) {
|
194
194
|
this.clipLevels.push(this.objectLevel);
|
195
195
|
this.ctx.save();
|
@@ -59,16 +59,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
59
59
|
drawPoints(...points: Point2[]): void;
|
60
60
|
drawSVGElem(elem: SVGElement): void;
|
61
61
|
isTooSmallToRender(_rect: Rect2): boolean;
|
62
|
-
private visibleRectOverride;
|
63
|
-
/**
|
64
|
-
* Overrides the visible region returned by `getVisibleRect`.
|
65
|
-
*
|
66
|
-
* This is useful when the `viewport`'s transform has been modified,
|
67
|
-
* for example, to compensate for storing part of the image's
|
68
|
-
* transformation in an SVG property.
|
69
|
-
*/
|
70
|
-
private overrideVisibleRect;
|
71
|
-
getVisibleRect(): Rect2;
|
72
62
|
/**
|
73
63
|
* Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
|
74
64
|
* and other metadata attributes set for the given `Viewport`.
|
@@ -42,7 +42,6 @@ class SVGRenderer extends AbstractRenderer_1.default {
|
|
42
42
|
this.textContainer = null;
|
43
43
|
this.textContainerTransform = null;
|
44
44
|
this.textParentStyle = defaultTextStyle;
|
45
|
-
this.visibleRectOverride = null;
|
46
45
|
this.clear();
|
47
46
|
this.addStyleSheet();
|
48
47
|
}
|
@@ -345,19 +344,6 @@ class SVGRenderer extends AbstractRenderer_1.default {
|
|
345
344
|
isTooSmallToRender(_rect) {
|
346
345
|
return false;
|
347
346
|
}
|
348
|
-
/**
|
349
|
-
* Overrides the visible region returned by `getVisibleRect`.
|
350
|
-
*
|
351
|
-
* This is useful when the `viewport`'s transform has been modified,
|
352
|
-
* for example, to compensate for storing part of the image's
|
353
|
-
* transformation in an SVG property.
|
354
|
-
*/
|
355
|
-
overrideVisibleRect(newRect) {
|
356
|
-
this.visibleRectOverride = newRect;
|
357
|
-
}
|
358
|
-
getVisibleRect() {
|
359
|
-
return this.visibleRectOverride ?? super.getVisibleRect();
|
360
|
-
}
|
361
347
|
/**
|
362
348
|
* Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
|
363
349
|
* and other metadata attributes set for the given `Viewport`.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import type Editor from '../Editor';
|
2
|
+
import { Point2 } from '@js-draw/math';
|
3
|
+
/**
|
4
|
+
* Creates two pointers and sends the touch {@link InputEvtType.PointerDownEvt}s for them.
|
5
|
+
*
|
6
|
+
* Returns an object that allows continuing or ending the gesture.
|
7
|
+
*
|
8
|
+
* `initialRotation` should be in radians.
|
9
|
+
*/
|
10
|
+
declare const startPinchGesture: (editor: Editor, center: Point2, initialDistance: number, initialRotation: number) => {
|
11
|
+
update(center: Point2, distance: number, rotation: number): void;
|
12
|
+
end(): void;
|
13
|
+
};
|
14
|
+
export default startPinchGesture;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const math_1 = require("@js-draw/math");
|
7
|
+
const sendTouchEvent_1 = __importDefault(require("./sendTouchEvent"));
|
8
|
+
const inputEvents_1 = require("../inputEvents");
|
9
|
+
/**
|
10
|
+
* Creates two pointers and sends the touch {@link InputEvtType.PointerDownEvt}s for them.
|
11
|
+
*
|
12
|
+
* Returns an object that allows continuing or ending the gesture.
|
13
|
+
*
|
14
|
+
* `initialRotation` should be in radians.
|
15
|
+
*/
|
16
|
+
const startPinchGesture = (editor, center, initialDistance, initialRotation) => {
|
17
|
+
const computeTouchPoints = (center, distance, rotation) => {
|
18
|
+
const halfDisplacement = math_1.Mat33.zRotation(rotation).transformVec2(math_1.Vec2.of(0, distance / 2));
|
19
|
+
const point1 = center.plus(halfDisplacement);
|
20
|
+
const point2 = center.minus(halfDisplacement);
|
21
|
+
return [point1, point2];
|
22
|
+
};
|
23
|
+
let [touchPoint1, touchPoint2] = computeTouchPoints(center, initialDistance, initialRotation);
|
24
|
+
let firstPointer = (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerDownEvt, touchPoint1);
|
25
|
+
let secondPointer = (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerDownEvt, touchPoint2, [firstPointer]);
|
26
|
+
return {
|
27
|
+
update(center, distance, rotation) {
|
28
|
+
const eventType = inputEvents_1.InputEvtType.PointerMoveEvt;
|
29
|
+
const [newPoint1, newPoint2] = computeTouchPoints(center, distance, rotation);
|
30
|
+
touchPoint1 = newPoint1;
|
31
|
+
touchPoint2 = newPoint2;
|
32
|
+
firstPointer = (0, sendTouchEvent_1.default)(editor, eventType, newPoint1, [secondPointer]);
|
33
|
+
secondPointer = (0, sendTouchEvent_1.default)(editor, eventType, newPoint2, [firstPointer]);
|
34
|
+
},
|
35
|
+
end() {
|
36
|
+
(0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerUpEvt, touchPoint1, [secondPointer]);
|
37
|
+
(0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerUpEvt, touchPoint2);
|
38
|
+
},
|
39
|
+
};
|
40
|
+
};
|
41
|
+
exports.default = startPinchGesture;
|
@@ -21,6 +21,9 @@ export default class PanZoom extends BaseTool {
|
|
21
21
|
private editor;
|
22
22
|
private mode;
|
23
23
|
private transform;
|
24
|
+
private readonly initialRotationSnapAngle;
|
25
|
+
private readonly afterRotationStartSnapAngle;
|
26
|
+
private readonly pinchZoomStartThreshold;
|
24
27
|
private startDist;
|
25
28
|
private lastDist;
|
26
29
|
private lastScreenCenter;
|
@@ -29,6 +32,7 @@ export default class PanZoom extends BaseTool {
|
|
29
32
|
private initialTouchAngle;
|
30
33
|
private initialViewportRotation;
|
31
34
|
private isScaling;
|
35
|
+
private isRotating;
|
32
36
|
private inertialScroller;
|
33
37
|
private velocity;
|
34
38
|
constructor(editor: Editor, mode: PanZoomMode, description: string);
|
@@ -71,12 +71,19 @@ class PanZoom extends BaseTool_1.default {
|
|
71
71
|
this.editor = editor;
|
72
72
|
this.mode = mode;
|
73
73
|
this.transform = null;
|
74
|
+
// Constants
|
75
|
+
// initialRotationSnapAngle is larger than afterRotationStartSnapAngle to
|
76
|
+
// make it more difficult to start rotating (and easier to continue rotating).
|
77
|
+
this.initialRotationSnapAngle = 0.22; // radians
|
78
|
+
this.afterRotationStartSnapAngle = 0.07; // radians
|
79
|
+
this.pinchZoomStartThreshold = 1.08; // scale factor
|
74
80
|
this.lastPointerDownTimestamp = 0;
|
75
81
|
this.initialTouchAngle = 0;
|
76
82
|
this.initialViewportRotation = 0;
|
77
83
|
// Set to `true` only when scaling has started (if two fingers are down and have moved
|
78
84
|
// far enough).
|
79
85
|
this.isScaling = false;
|
86
|
+
this.isRotating = false;
|
80
87
|
this.inertialScroller = null;
|
81
88
|
this.velocity = null;
|
82
89
|
}
|
@@ -118,6 +125,9 @@ class PanZoom extends BaseTool_1.default {
|
|
118
125
|
this.initialTouchAngle = angle;
|
119
126
|
this.initialViewportRotation = this.editor.viewport.getRotationAngle();
|
120
127
|
this.isScaling = false;
|
128
|
+
// We're initially rotated if `initialViewportRotation` isn't near a multiple of pi/2.
|
129
|
+
// In other words, if sin(2 initialViewportRotation) is near zero.
|
130
|
+
this.isRotating = Math.abs(Math.sin(this.initialViewportRotation * 2)) > 1e-3;
|
121
131
|
handlingGesture = true;
|
122
132
|
}
|
123
133
|
else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
@@ -174,7 +184,9 @@ class PanZoom extends BaseTool_1.default {
|
|
174
184
|
const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
|
175
185
|
// The maximum angle for which we snap the given angle to a multiple of
|
176
186
|
// `snapToMultipleOf`.
|
177
|
-
|
187
|
+
// Use a smaller snap angle if already rotated (to avoid pinch zoom gestures from
|
188
|
+
// starting rotation).
|
189
|
+
const maxSnapAngle = this.isRotating ? this.afterRotationStartSnapAngle : this.initialRotationSnapAngle;
|
178
190
|
// Snap the rotation
|
179
191
|
if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
|
180
192
|
fullRotation = roundedFullRotation;
|
@@ -197,6 +209,11 @@ class PanZoom extends BaseTool_1.default {
|
|
197
209
|
else {
|
198
210
|
deltaRotation = this.toSnappedRotationDelta(angle);
|
199
211
|
}
|
212
|
+
// If any rotation, make a note of this (affects rotation snap
|
213
|
+
// angles).
|
214
|
+
if (Math.abs(deltaRotation) > 1e-8) {
|
215
|
+
this.isRotating = true;
|
216
|
+
}
|
200
217
|
this.updateVelocity(screenCenter);
|
201
218
|
let scaleFactor = 1;
|
202
219
|
if (this.isScaling) {
|
@@ -205,7 +222,9 @@ class PanZoom extends BaseTool_1.default {
|
|
205
222
|
else {
|
206
223
|
const initialScaleFactor = dist / this.startDist;
|
207
224
|
// Only start scaling if scaling done so far exceeds some threshold.
|
208
|
-
|
225
|
+
const upperBound = this.pinchZoomStartThreshold;
|
226
|
+
const lowerBound = 1 / this.pinchZoomStartThreshold;
|
227
|
+
if (initialScaleFactor > upperBound || initialScaleFactor < lowerBound) {
|
209
228
|
scaleFactor = initialScaleFactor;
|
210
229
|
this.isScaling = true;
|
211
230
|
}
|
package/dist/cjs/version.js
CHANGED
package/dist/mjs/Viewport.d.ts
CHANGED
@@ -56,6 +56,8 @@ export declare class Viewport {
|
|
56
56
|
/**
|
57
57
|
* @returns the angle of the canvas in radians.
|
58
58
|
* This is the angle by which the canvas is rotated relative to the screen.
|
59
|
+
*
|
60
|
+
* Returns an angle in the range $[-\pi, \pi]$ (the same range as {@link Vec3.angle}).
|
59
61
|
*/
|
60
62
|
getRotationAngle(): number;
|
61
63
|
/**
|
package/dist/mjs/Viewport.mjs
CHANGED
@@ -114,6 +114,8 @@ export class Viewport {
|
|
114
114
|
/**
|
115
115
|
* @returns the angle of the canvas in radians.
|
116
116
|
* This is the angle by which the canvas is rotated relative to the screen.
|
117
|
+
*
|
118
|
+
* Returns an angle in the range $[-\pi, \pi]$ (the same range as {@link Vec3.angle}).
|
117
119
|
*/
|
118
120
|
getRotationAngle() {
|
119
121
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
@@ -77,6 +77,15 @@ export default abstract class AbstractComponent {
|
|
77
77
|
* {@link EditorImage}.
|
78
78
|
*/
|
79
79
|
getSizingMode(): ComponentSizingMode;
|
80
|
+
/**
|
81
|
+
* **Optimization**
|
82
|
+
*
|
83
|
+
* Should return `true` if this component covers the entire `visibleRect`
|
84
|
+
* and would prevent anything below this component from being visible.
|
85
|
+
*
|
86
|
+
* Should return `false` otherwise.
|
87
|
+
*/
|
88
|
+
occludesEverythingBelowWhenRenderedInRect(_visibleRect: Rect2): boolean;
|
80
89
|
/** Called when this component is added to the given image. */
|
81
90
|
onAddToImage(_image: EditorImage): void;
|
82
91
|
onRemoveFromImage(): void;
|
@@ -102,6 +102,17 @@ class AbstractComponent {
|
|
102
102
|
getSizingMode() {
|
103
103
|
return ComponentSizingMode.BoundingBox;
|
104
104
|
}
|
105
|
+
/**
|
106
|
+
* **Optimization**
|
107
|
+
*
|
108
|
+
* Should return `true` if this component covers the entire `visibleRect`
|
109
|
+
* and would prevent anything below this component from being visible.
|
110
|
+
*
|
111
|
+
* Should return `false` otherwise.
|
112
|
+
*/
|
113
|
+
occludesEverythingBelowWhenRenderedInRect(_visibleRect) {
|
114
|
+
return false;
|
115
|
+
}
|
105
116
|
/** Called when this component is added to the given image. */
|
106
117
|
onAddToImage(_image) { }
|
107
118
|
onRemoveFromImage() { }
|
@@ -49,6 +49,9 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
49
49
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
50
50
|
intersects(line: LineSegment2): boolean;
|
51
51
|
intersectsRect(rect: Rect2): boolean;
|
52
|
+
private simplifiedPath;
|
53
|
+
private computeSimplifiedPathFor;
|
54
|
+
occludesEverythingBelowWhenRenderedInRect(rect: Rect2): boolean;
|
52
55
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
53
56
|
getProportionalRenderingTime(): number;
|
54
57
|
private bboxForPart;
|
@@ -2,7 +2,7 @@ import { Path, Rect2 } from '@js-draw/math';
|
|
2
2
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
|
3
3
|
import AbstractComponent from './AbstractComponent.mjs';
|
4
4
|
import { createRestyleComponentCommand } from './RestylableComponent.mjs';
|
5
|
-
import { pathFromRenderable, pathToRenderable } from '../rendering/RenderablePathSpec.mjs';
|
5
|
+
import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty } from '../rendering/RenderablePathSpec.mjs';
|
6
6
|
/**
|
7
7
|
* Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
|
8
8
|
*
|
@@ -41,6 +41,9 @@ export default class Stroke extends AbstractComponent {
|
|
41
41
|
// @internal
|
42
42
|
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
43
43
|
this.isRestylableComponent = true;
|
44
|
+
// A simplification of the path for a given visibleRect. Intended
|
45
|
+
// to help check for occlusion.
|
46
|
+
this.simplifiedPath = null;
|
44
47
|
this.approximateRenderingTime = 0;
|
45
48
|
this.parts = [];
|
46
49
|
for (const section of parts) {
|
@@ -166,9 +169,60 @@ export default class Stroke extends AbstractComponent {
|
|
166
169
|
}
|
167
170
|
return super.intersectsRect(rect);
|
168
171
|
}
|
172
|
+
computeSimplifiedPathFor(visibleRect) {
|
173
|
+
const simplifiedParts = [];
|
174
|
+
let occludes = false;
|
175
|
+
let skipSimplification = false;
|
176
|
+
for (const part of this.parts) {
|
177
|
+
if (skipSimplification
|
178
|
+
// Simplification currently only works for stroked paths
|
179
|
+
|| !part.style.stroke
|
180
|
+
// One of the main purposes of this is to check for occlusion.
|
181
|
+
// We can't occlude things if the stroke is partially transparent.
|
182
|
+
|| part.style.stroke.color.a < 0.99) {
|
183
|
+
simplifiedParts.push(part);
|
184
|
+
continue;
|
185
|
+
}
|
186
|
+
const mapping = simplifyPathToFullScreenOrEmpty(part, visibleRect);
|
187
|
+
if (mapping) {
|
188
|
+
simplifiedParts.push(mapping.path);
|
189
|
+
if (mapping.fullScreen) {
|
190
|
+
occludes = true;
|
191
|
+
skipSimplification = true;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
else {
|
195
|
+
simplifiedParts.push(part);
|
196
|
+
}
|
197
|
+
}
|
198
|
+
return {
|
199
|
+
forVisibleRect: visibleRect,
|
200
|
+
parts: simplifiedParts,
|
201
|
+
occludes,
|
202
|
+
};
|
203
|
+
}
|
204
|
+
occludesEverythingBelowWhenRenderedInRect(rect) {
|
205
|
+
// Can't occlude if doesn't contain.
|
206
|
+
if (!this.getBBox().containsRect(rect)) {
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
if (!this.simplifiedPath || !this.simplifiedPath.forVisibleRect.eq(rect)) {
|
210
|
+
this.simplifiedPath = this.computeSimplifiedPathFor(rect);
|
211
|
+
}
|
212
|
+
return this.simplifiedPath.occludes;
|
213
|
+
}
|
169
214
|
render(canvas, visibleRect) {
|
170
215
|
canvas.startObject(this.getBBox());
|
171
|
-
|
216
|
+
// Can we use a cached simplified path for faster rendering?
|
217
|
+
let parts = this.parts;
|
218
|
+
if (visibleRect && this.simplifiedPath?.forVisibleRect?.containsRect(visibleRect)) {
|
219
|
+
parts = this.simplifiedPath.parts;
|
220
|
+
}
|
221
|
+
else {
|
222
|
+
// Save memory
|
223
|
+
this.simplifiedPath = null;
|
224
|
+
}
|
225
|
+
for (const part of parts) {
|
172
226
|
const bbox = this.bboxForPart(part.path.bbox, part.style);
|
173
227
|
if (visibleRect) {
|
174
228
|
if (!bbox.intersects(visibleRect)) {
|
@@ -663,12 +663,28 @@ export class ImageNode {
|
|
663
663
|
leaves = this.getLeaves();
|
664
664
|
}
|
665
665
|
sortLeavesByZIndex(leaves);
|
666
|
-
|
666
|
+
// If some components hide others (and we're permitted to simplify,
|
667
|
+
// which is true in the case of visibleRect being defined), then only
|
668
|
+
// draw the non-hidden components:
|
669
|
+
let startIndex = 0;
|
670
|
+
if (visibleRect) {
|
671
|
+
for (let i = leaves.length - 1; i >= 1; i--) {
|
672
|
+
if (leaves[i].getContent()?.occludesEverythingBelowWhenRenderedInRect(visibleRect)) {
|
673
|
+
startIndex = i;
|
674
|
+
break;
|
675
|
+
}
|
676
|
+
}
|
677
|
+
}
|
678
|
+
for (let i = startIndex; i < leaves.length; i++) {
|
679
|
+
const leaf = leaves[i];
|
667
680
|
// Leaves by definition have content
|
668
681
|
leaf.getContent().render(renderer, visibleRect);
|
669
682
|
}
|
670
683
|
// Show debug information
|
671
684
|
if (debugMode && visibleRect) {
|
685
|
+
if (startIndex !== 0) {
|
686
|
+
console.log('EditorImage: skipped ', startIndex, 'nodes due to occlusion');
|
687
|
+
}
|
672
688
|
this.renderDebugBoundingBoxes(renderer, visibleRect);
|
673
689
|
}
|
674
690
|
}
|
@@ -6,11 +6,29 @@ interface RenderablePathSpec {
|
|
6
6
|
style: RenderingStyle;
|
7
7
|
path?: Path;
|
8
8
|
}
|
9
|
+
interface RenderablePathSpecWithPath extends RenderablePathSpec {
|
10
|
+
path: Path;
|
11
|
+
}
|
9
12
|
/** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
|
10
13
|
export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
|
11
|
-
export declare const pathToRenderable: (path: Path, style: RenderingStyle) =>
|
14
|
+
export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpecWithPath;
|
15
|
+
interface RectangleSimplificationResult {
|
16
|
+
rectangle: Rect2;
|
17
|
+
path: RenderablePathSpecWithPath;
|
18
|
+
fullScreen: boolean;
|
19
|
+
}
|
20
|
+
/**
|
21
|
+
* Tries to simplify the given path to a fullscreen rectangle.
|
22
|
+
* Returns `null` on failure.
|
23
|
+
*
|
24
|
+
* @internal
|
25
|
+
*/
|
26
|
+
export declare const simplifyPathToFullScreenOrEmpty: (renderablePath: RenderablePathSpec, visibleRect: Rect2, options?: {
|
27
|
+
fastCheck: boolean;
|
28
|
+
expensiveCheck: boolean;
|
29
|
+
}) => RectangleSimplificationResult | null;
|
12
30
|
/**
|
13
31
|
* @returns a Path that, when rendered, looks roughly equivalent to the given path.
|
14
32
|
*/
|
15
|
-
export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) =>
|
33
|
+
export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) => RenderablePathSpecWithPath;
|
16
34
|
export default RenderablePathSpec;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Mat33, Path, PathCommandType } from '@js-draw/math';
|
1
|
+
import { Color4, Mat33, Path, PathCommandType, Rect2 } from '@js-draw/math';
|
2
2
|
/** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
|
3
3
|
export const pathFromRenderable = (renderable) => {
|
4
4
|
if (renderable.path) {
|
@@ -15,33 +15,89 @@ export const pathToRenderable = (path, style) => {
|
|
15
15
|
};
|
16
16
|
};
|
17
17
|
/**
|
18
|
-
*
|
18
|
+
* Fills the optional `path` field in `RenderablePathSpec`
|
19
|
+
* with `path` if not already filled
|
19
20
|
*/
|
20
|
-
|
21
|
+
const pathIncluded = (renderablePath, path) => {
|
22
|
+
if (renderablePath.path) {
|
23
|
+
return renderablePath;
|
24
|
+
}
|
25
|
+
return {
|
26
|
+
...renderablePath,
|
27
|
+
path,
|
28
|
+
};
|
29
|
+
};
|
30
|
+
/**
|
31
|
+
* Tries to simplify the given path to a fullscreen rectangle.
|
32
|
+
* Returns `null` on failure.
|
33
|
+
*
|
34
|
+
* @internal
|
35
|
+
*/
|
36
|
+
export const simplifyPathToFullScreenOrEmpty = (renderablePath, visibleRect, options = { fastCheck: true, expensiveCheck: true }) => {
|
21
37
|
const path = pathFromRenderable(renderablePath);
|
22
38
|
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
23
39
|
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
24
40
|
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
25
41
|
// Are we close enough to the path that it fills the entire screen?
|
26
|
-
|
27
|
-
&& renderablePath.style.stroke
|
42
|
+
const isOnlyStrokedAndCouldFillScreen = (onlyStroked
|
28
43
|
&& strokeWidth > visibleRect.maxDimension
|
29
|
-
&& styledPathBBox.containsRect(visibleRect))
|
44
|
+
&& styledPathBBox.containsRect(visibleRect));
|
45
|
+
if (options.fastCheck && isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke) {
|
30
46
|
const strokeRadius = strokeWidth / 2;
|
47
|
+
// Are we completely within the stroke?
|
31
48
|
// Do a fast, but with many false negatives, check.
|
32
49
|
for (const point of path.startEndPoints()) {
|
33
50
|
// If within the strokeRadius of any point
|
34
51
|
if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
|
35
|
-
return
|
52
|
+
return {
|
53
|
+
rectangle: visibleRect,
|
54
|
+
path: pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
|
55
|
+
fullScreen: true,
|
56
|
+
};
|
36
57
|
}
|
37
58
|
}
|
38
59
|
}
|
60
|
+
// Try filtering again, but with slightly more expensive checks
|
61
|
+
if (options.expensiveCheck &&
|
62
|
+
isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke
|
63
|
+
&& strokeWidth > visibleRect.maxDimension * 3) {
|
64
|
+
const signedDist = path.signedDistance(visibleRect.center, strokeWidth / 2);
|
65
|
+
const margin = strokeWidth / 6;
|
66
|
+
if (signedDist < -visibleRect.maxDimension / 2 - margin) {
|
67
|
+
return {
|
68
|
+
path: pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
|
69
|
+
rectangle: visibleRect,
|
70
|
+
fullScreen: true,
|
71
|
+
};
|
72
|
+
}
|
73
|
+
else if (signedDist > visibleRect.maxDimension / 2 + margin) {
|
74
|
+
return {
|
75
|
+
path: pathToRenderable(Path.empty, { fill: Color4.transparent }),
|
76
|
+
rectangle: Rect2.empty,
|
77
|
+
fullScreen: false,
|
78
|
+
};
|
79
|
+
}
|
80
|
+
}
|
81
|
+
return null;
|
82
|
+
};
|
83
|
+
/**
|
84
|
+
* @returns a Path that, when rendered, looks roughly equivalent to the given path.
|
85
|
+
*/
|
86
|
+
export const visualEquivalent = (renderablePath, visibleRect) => {
|
87
|
+
const path = pathFromRenderable(renderablePath);
|
88
|
+
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
89
|
+
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
90
|
+
const styledPathBBox = path.bbox.grownBy(strokeWidth);
|
91
|
+
let rectangleSimplification = simplifyPathToFullScreenOrEmpty(renderablePath, visibleRect, { fastCheck: true, expensiveCheck: false, });
|
92
|
+
if (rectangleSimplification) {
|
93
|
+
return rectangleSimplification.path;
|
94
|
+
}
|
39
95
|
// Scale the expanded rect --- the visual equivalent is only close for huge strokes.
|
40
96
|
const expandedRect = visibleRect.grownBy(strokeWidth)
|
41
97
|
.transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
|
42
98
|
// TODO: Handle simplifying very small paths.
|
43
99
|
if (expandedRect.containsRect(styledPathBBox)) {
|
44
|
-
return renderablePath;
|
100
|
+
return pathIncluded(renderablePath, path);
|
45
101
|
}
|
46
102
|
const parts = [];
|
47
103
|
let startPoint = path.startPoint;
|
@@ -76,5 +132,11 @@ export const visualEquivalent = (renderablePath, visibleRect) => {
|
|
76
132
|
}
|
77
133
|
startPoint = endPoint;
|
78
134
|
}
|
79
|
-
|
135
|
+
const newPath = new Path(path.startPoint, parts);
|
136
|
+
const newStyle = renderablePath.style;
|
137
|
+
rectangleSimplification = simplifyPathToFullScreenOrEmpty(renderablePath, visibleRect, { fastCheck: false, expensiveCheck: true, });
|
138
|
+
if (rectangleSimplification) {
|
139
|
+
return rectangleSimplification.path;
|
140
|
+
}
|
141
|
+
return pathToRenderable(newPath, newStyle);
|
80
142
|
};
|
@@ -47,8 +47,10 @@ export default class CacheRecord {
|
|
47
47
|
return transform;
|
48
48
|
}
|
49
49
|
setRenderingRegion(drawTo) {
|
50
|
-
this.
|
51
|
-
|
52
|
-
|
50
|
+
const transform = this.getTransform(drawTo);
|
51
|
+
this.renderer.setTransform(transform);
|
52
|
+
// The visible region may be slightly larger than where we're actually drawing
|
53
|
+
// to (because of rounding).
|
54
|
+
this.renderer.overrideVisibleRect(drawTo.grownBy(1 / transform.getScaleFactor()));
|
53
55
|
}
|
54
56
|
}
|
@@ -20,6 +20,11 @@ export default abstract class AbstractRenderer {
|
|
20
20
|
private selfTransform;
|
21
21
|
private transformStack;
|
22
22
|
protected constructor(viewport: Viewport);
|
23
|
+
/**
|
24
|
+
* this.canvasToScreen, etc. should be used instead of the corresponding
|
25
|
+
* methods on `Viewport`, because the viewport may not accurately reflect
|
26
|
+
* what is rendered.
|
27
|
+
*/
|
23
28
|
protected getViewport(): Viewport;
|
24
29
|
abstract displaySize(): Vec2;
|
25
30
|
abstract clear(): void;
|
@@ -73,5 +78,10 @@ export default abstract class AbstractRenderer {
|
|
73
78
|
getCanvasToScreenTransform(): Mat33;
|
74
79
|
canvasToScreen(vec: Vec2): Vec2;
|
75
80
|
getSizeOfCanvasPixelOnScreen(): number;
|
81
|
+
private visibleRectOverride;
|
82
|
+
/**
|
83
|
+
* @internal
|
84
|
+
*/
|
85
|
+
overrideVisibleRect(rect: Rect2 | null): void;
|
76
86
|
getVisibleRect(): Rect2;
|
77
87
|
}
|
@@ -15,8 +15,11 @@ export default class AbstractRenderer {
|
|
15
15
|
this.objectLevel = 0;
|
16
16
|
this.currentPaths = null;
|
17
17
|
}
|
18
|
-
|
19
|
-
|
18
|
+
/**
|
19
|
+
* this.canvasToScreen, etc. should be used instead of the corresponding
|
20
|
+
* methods on `Viewport`, because the viewport may not accurately reflect
|
21
|
+
* what is rendered.
|
22
|
+
*/
|
20
23
|
getViewport() { return this.viewport; }
|
21
24
|
setDraftMode(_draftMode) { }
|
22
25
|
flushPath() {
|
@@ -158,12 +161,18 @@ export default class AbstractRenderer {
|
|
158
161
|
getSizeOfCanvasPixelOnScreen() {
|
159
162
|
return this.getCanvasToScreenTransform().transformVec3(Vec2.unitX).length();
|
160
163
|
}
|
164
|
+
/**
|
165
|
+
* @internal
|
166
|
+
*/
|
167
|
+
overrideVisibleRect(rect) {
|
168
|
+
this.visibleRectOverride = rect;
|
169
|
+
}
|
161
170
|
// Returns the region in canvas space that is visible within the viewport this
|
162
171
|
// canvas is rendering to.
|
163
172
|
//
|
164
173
|
// Note that in some cases this might not be the same as the `visibleRect` given
|
165
174
|
// to components in their `render` method.
|
166
175
|
getVisibleRect() {
|
167
|
-
return this.viewport.visibleRect;
|
176
|
+
return this.visibleRectOverride ?? this.viewport.visibleRect;
|
168
177
|
}
|
169
178
|
}
|
@@ -144,7 +144,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
144
144
|
return;
|
145
145
|
}
|
146
146
|
// If part of a huge object, it might be worth trimming the path
|
147
|
-
const visibleRect = this.
|
147
|
+
const visibleRect = this.getVisibleRect();
|
148
148
|
if (this.currentObjectBBox?.containsRect(visibleRect)) {
|
149
149
|
// Try to trim/remove parts of the path outside of the bounding box.
|
150
150
|
path = visualEquivalent(path, visibleRect);
|
@@ -184,7 +184,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
184
184
|
if (!this.ignoringObject && clip) {
|
185
185
|
// Don't clip if it would only remove content already trimmed by
|
186
186
|
// the edge of the screen.
|
187
|
-
const clippedIsOutsideScreen = boundingBox.containsRect(this.
|
187
|
+
const clippedIsOutsideScreen = boundingBox.containsRect(this.getVisibleRect());
|
188
188
|
if (!clippedIsOutsideScreen) {
|
189
189
|
this.clipLevels.push(this.objectLevel);
|
190
190
|
this.ctx.save();
|
@@ -59,16 +59,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
59
59
|
drawPoints(...points: Point2[]): void;
|
60
60
|
drawSVGElem(elem: SVGElement): void;
|
61
61
|
isTooSmallToRender(_rect: Rect2): boolean;
|
62
|
-
private visibleRectOverride;
|
63
|
-
/**
|
64
|
-
* Overrides the visible region returned by `getVisibleRect`.
|
65
|
-
*
|
66
|
-
* This is useful when the `viewport`'s transform has been modified,
|
67
|
-
* for example, to compensate for storing part of the image's
|
68
|
-
* transformation in an SVG property.
|
69
|
-
*/
|
70
|
-
private overrideVisibleRect;
|
71
|
-
getVisibleRect(): Rect2;
|
72
62
|
/**
|
73
63
|
* Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
|
74
64
|
* and other metadata attributes set for the given `Viewport`.
|