js-draw 0.0.10 → 0.1.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/CHANGELOG.md +4 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +13 -7
- package/dist/src/EditorImage.d.ts +15 -7
- package/dist/src/EditorImage.js +41 -35
- package/dist/src/SVGLoader.d.ts +3 -2
- package/dist/src/SVGLoader.js +9 -7
- package/dist/src/Viewport.d.ts +4 -0
- package/dist/src/Viewport.js +41 -0
- package/dist/src/components/AbstractComponent.d.ts +3 -2
- package/dist/src/components/AbstractComponent.js +3 -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/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/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 +5 -3
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +28 -1
- package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
- package/dist/src/{Display.js → rendering/Display.js} +33 -4
- package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
- package/dist/src/rendering/caching/CacheRecord.js +51 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
- package/dist/src/rendering/caching/CacheRecordManager.js +39 -0
- package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
- package/dist/src/rendering/caching/RenderingCache.js +36 -0
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +294 -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 +20 -0
- package/dist/src/rendering/caching/types.js +1 -0
- package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +18 -8
- package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +36 -2
- 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} +4 -3
- package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +14 -11
- package/dist/src/testing/createEditor.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.js +1 -1
- package/dist/src/tools/SelectionTool.js +9 -24
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/Editor.ts +15 -8
- package/src/EditorImage.test.ts +2 -2
- package/src/EditorImage.ts +53 -41
- package/src/SVGLoader.ts +11 -8
- package/src/Viewport.ts +56 -0
- package/src/components/AbstractComponent.ts +6 -2
- package/src/components/SVGGlobalAttributesObject.ts +2 -2
- package/src/components/Stroke.ts +1 -1
- package/src/components/UnknownSVGObject.ts +2 -2
- package/src/components/builders/ArrowBuilder.ts +1 -1
- package/src/components/builders/FreehandLineBuilder.ts +1 -1
- 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.toString.test.ts +12 -2
- package/src/geometry/Path.ts +8 -4
- package/src/geometry/Rect2.test.ts +38 -8
- package/src/geometry/Rect2.ts +32 -1
- package/src/{Display.ts → rendering/Display.ts} +38 -6
- package/src/rendering/caching/CacheRecord.test.ts +49 -0
- package/src/rendering/caching/CacheRecord.ts +72 -0
- package/src/rendering/caching/CacheRecordManager.ts +55 -0
- package/src/rendering/caching/RenderingCache.test.ts +44 -0
- package/src/rendering/caching/RenderingCache.ts +56 -0
- package/src/rendering/caching/RenderingCacheNode.ts +365 -0
- package/src/rendering/caching/testUtils.ts +34 -0
- package/src/rendering/caching/types.ts +35 -0
- package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +55 -8
- 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} +17 -13
- package/src/testing/createEditor.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/tools/SelectionTool.test.ts +1 -1
- package/src/tools/SelectionTool.ts +12 -33
- 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
package/src/EditorImage.ts
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
import Editor from './Editor';
|
2
|
-
import AbstractRenderer from './rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
|
3
3
|
import Command from './commands/Command';
|
4
4
|
import Viewport from './Viewport';
|
5
5
|
import AbstractComponent from './components/AbstractComponent';
|
6
6
|
import Rect2 from './geometry/Rect2';
|
7
7
|
import { EditorLocalization } from './localization';
|
8
|
+
import RenderingCache from './rendering/caching/RenderingCache';
|
9
|
+
|
10
|
+
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
11
|
+
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
12
|
+
};
|
8
13
|
|
9
14
|
// Handles lookup/storage of elements in the image
|
10
15
|
export default class EditorImage {
|
@@ -20,7 +25,7 @@ export default class EditorImage {
|
|
20
25
|
|
21
26
|
// Returns the parent of the given element, if it exists.
|
22
27
|
public findParent(elem: AbstractComponent): ImageNode|null {
|
23
|
-
const candidates = this.root.
|
28
|
+
const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
|
24
29
|
for (const candidate of candidates) {
|
25
30
|
if (candidate.getContent() === elem) {
|
26
31
|
return candidate;
|
@@ -29,25 +34,18 @@ export default class EditorImage {
|
|
29
34
|
return null;
|
30
35
|
}
|
31
36
|
|
32
|
-
|
33
|
-
|
37
|
+
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
38
|
+
cache.render(screenRenderer, this.root, viewport);
|
34
39
|
}
|
35
40
|
|
36
|
-
public render(renderer: AbstractRenderer, viewport: Viewport
|
37
|
-
|
38
|
-
const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
|
39
|
-
this.sortLeaves(leaves);
|
40
|
-
|
41
|
-
for (const leaf of leaves) {
|
42
|
-
// Leaves by definition have content
|
43
|
-
leaf.getContent()!.render(renderer, viewport.visibleRect);
|
44
|
-
}
|
41
|
+
public render(renderer: AbstractRenderer, viewport: Viewport) {
|
42
|
+
this.root.render(renderer, viewport.visibleRect);
|
45
43
|
}
|
46
44
|
|
47
45
|
// Renders all nodes, even ones not within the viewport
|
48
46
|
public renderAll(renderer: AbstractRenderer) {
|
49
47
|
const leaves = this.root.getLeaves();
|
50
|
-
|
48
|
+
sortLeavesByZIndex(leaves);
|
51
49
|
|
52
50
|
for (const leaf of leaves) {
|
53
51
|
leaf.getContent()!.render(renderer, leaf.getBBox());
|
@@ -55,8 +53,9 @@ export default class EditorImage {
|
|
55
53
|
}
|
56
54
|
|
57
55
|
public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
|
58
|
-
const leaves = this.root.
|
59
|
-
|
56
|
+
const leaves = this.root.getLeavesIntersectingRegion(region);
|
57
|
+
sortLeavesByZIndex(leaves);
|
58
|
+
|
60
59
|
return leaves.map(leaf => leaf.getContent()!);
|
61
60
|
}
|
62
61
|
|
@@ -100,15 +99,17 @@ export default class EditorImage {
|
|
100
99
|
}
|
101
100
|
|
102
101
|
export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
|
102
|
+
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
103
103
|
|
104
|
-
|
104
|
+
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
|
105
105
|
export class ImageNode {
|
106
106
|
private content: AbstractComponent|null;
|
107
107
|
private bbox: Rect2;
|
108
108
|
private children: ImageNode[];
|
109
109
|
private targetChildCount: number = 30;
|
110
|
-
|
111
|
-
private
|
110
|
+
|
111
|
+
private id: number;
|
112
|
+
private static idCounter: number = 0;
|
112
113
|
|
113
114
|
public constructor(
|
114
115
|
private parent: ImageNode|null = null
|
@@ -117,8 +118,15 @@ export class ImageNode {
|
|
117
118
|
this.bbox = Rect2.empty;
|
118
119
|
this.content = null;
|
119
120
|
|
120
|
-
this.
|
121
|
-
|
121
|
+
this.id = ImageNode.idCounter++;
|
122
|
+
}
|
123
|
+
|
124
|
+
public getId() {
|
125
|
+
return this.id;
|
126
|
+
}
|
127
|
+
|
128
|
+
public onContentChange() {
|
129
|
+
this.id = ImageNode.idCounter++;
|
122
130
|
}
|
123
131
|
|
124
132
|
public getContent(): AbstractComponent|null {
|
@@ -129,18 +137,25 @@ export class ImageNode {
|
|
129
137
|
return this.parent;
|
130
138
|
}
|
131
139
|
|
132
|
-
|
140
|
+
public getChildrenInRegion(region: Rect2): ImageNode[] {
|
133
141
|
return this.children.filter(child => {
|
134
142
|
return child.getBBox().intersects(region);
|
135
143
|
});
|
136
144
|
}
|
137
145
|
|
146
|
+
public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
|
147
|
+
if (this.content) {
|
148
|
+
return [this];
|
149
|
+
}
|
150
|
+
return this.getChildrenInRegion(region);
|
151
|
+
}
|
152
|
+
|
138
153
|
// Returns a list of `ImageNode`s with content (and thus no children).
|
139
|
-
public
|
154
|
+
public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
|
140
155
|
const result: ImageNode[] = [];
|
141
156
|
|
142
157
|
// Don't render if too small
|
143
|
-
if (this.bbox
|
158
|
+
if (isTooSmall?.(this.bbox)) {
|
144
159
|
return [];
|
145
160
|
}
|
146
161
|
|
@@ -150,7 +165,7 @@ export class ImageNode {
|
|
150
165
|
|
151
166
|
const children = this.getChildrenInRegion(region);
|
152
167
|
for (const child of children) {
|
153
|
-
result.push(...child.
|
168
|
+
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
|
154
169
|
}
|
155
170
|
|
156
171
|
return result;
|
@@ -172,6 +187,8 @@ export class ImageNode {
|
|
172
187
|
}
|
173
188
|
|
174
189
|
public addLeaf(leaf: AbstractComponent): ImageNode {
|
190
|
+
this.onContentChange();
|
191
|
+
|
175
192
|
if (this.content === null && this.children.length === 0) {
|
176
193
|
this.content = leaf;
|
177
194
|
this.recomputeBBox(true);
|
@@ -239,12 +256,8 @@ export class ImageNode {
|
|
239
256
|
const oldBBox = this.bbox;
|
240
257
|
if (this.content !== null) {
|
241
258
|
this.bbox = this.content.getBBox();
|
242
|
-
this.minZIndex = this.content.zIndex;
|
243
|
-
this.maxZIndex = this.content.zIndex;
|
244
259
|
} else {
|
245
260
|
this.bbox = Rect2.empty;
|
246
|
-
this.minZIndex = null;
|
247
|
-
this.maxZIndex = null;
|
248
261
|
let isFirst = true;
|
249
262
|
|
250
263
|
for (const child of this.children) {
|
@@ -254,15 +267,6 @@ export class ImageNode {
|
|
254
267
|
} else {
|
255
268
|
this.bbox = this.bbox.union(child.getBBox());
|
256
269
|
}
|
257
|
-
|
258
|
-
this.minZIndex ??= child.minZIndex;
|
259
|
-
this.maxZIndex ??= child.maxZIndex;
|
260
|
-
if (child.minZIndex !== null && this.minZIndex !== null) {
|
261
|
-
this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
|
262
|
-
}
|
263
|
-
if (child.maxZIndex !== null && this.maxZIndex !== null) {
|
264
|
-
this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
|
265
|
-
}
|
266
270
|
}
|
267
271
|
}
|
268
272
|
|
@@ -295,9 +299,6 @@ export class ImageNode {
|
|
295
299
|
|
296
300
|
// Remove this node and all of its children
|
297
301
|
public remove() {
|
298
|
-
this.minZIndex = null;
|
299
|
-
this.maxZIndex = null;
|
300
|
-
|
301
302
|
if (!this.parent) {
|
302
303
|
this.content = null;
|
303
304
|
this.children = [];
|
@@ -322,4 +323,15 @@ export class ImageNode {
|
|
322
323
|
this.parent = null;
|
323
324
|
this.children = [];
|
324
325
|
}
|
326
|
+
|
327
|
+
public render(renderer: AbstractRenderer, visibleRect: Rect2) {
|
328
|
+
// Don't render components that are < 0.1% of the viewport.
|
329
|
+
const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
|
330
|
+
sortLeavesByZIndex(leaves);
|
331
|
+
|
332
|
+
for (const leaf of leaves) {
|
333
|
+
// Leaves by definition have content
|
334
|
+
leaf.getContent()!.render(renderer, visibleRect);
|
335
|
+
}
|
336
|
+
}
|
325
337
|
}
|
package/src/SVGLoader.ts
CHANGED
@@ -5,8 +5,8 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
|
5
5
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
6
6
|
import Path from './geometry/Path';
|
7
7
|
import Rect2 from './geometry/Rect2';
|
8
|
-
import { RenderablePathSpec, RenderingStyle } from './rendering/AbstractRenderer';
|
9
|
-
import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
|
8
|
+
import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
|
9
|
+
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
10
10
|
|
11
11
|
type OnFinishListener = ()=> void;
|
12
12
|
|
@@ -16,6 +16,8 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
|
16
16
|
export default class SVGLoader implements ImageLoader {
|
17
17
|
private onAddComponent: ComponentAddedListener|null = null;
|
18
18
|
private onProgress: OnProgressListener|null = null;
|
19
|
+
private onDetermineExportRect: OnDetermineExportRectListener|null = null;
|
20
|
+
|
19
21
|
private processedCount: number = 0;
|
20
22
|
private totalToProcess: number = 0;
|
21
23
|
private rootViewBox: Rect2|null;
|
@@ -126,6 +128,7 @@ export default class SVGLoader implements ImageLoader {
|
|
126
128
|
}
|
127
129
|
|
128
130
|
this.rootViewBox = new Rect2(x, y, width, height);
|
131
|
+
this.onDetermineExportRect?.(this.rootViewBox);
|
129
132
|
}
|
130
133
|
|
131
134
|
private updateSVGAttrs(node: SVGSVGElement) {
|
@@ -172,10 +175,12 @@ export default class SVGLoader implements ImageLoader {
|
|
172
175
|
}
|
173
176
|
|
174
177
|
public async start(
|
175
|
-
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
|
176
|
-
|
178
|
+
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener,
|
179
|
+
onDetermineExportRect: OnDetermineExportRectListener|null = null
|
180
|
+
): Promise<void> {
|
177
181
|
this.onAddComponent = onAddComponent;
|
178
182
|
this.onProgress = onProgress;
|
183
|
+
this.onDetermineExportRect = onDetermineExportRect;
|
179
184
|
|
180
185
|
// Estimate the number of tags to process.
|
181
186
|
this.totalToProcess = this.source.childElementCount;
|
@@ -185,14 +190,12 @@ export default class SVGLoader implements ImageLoader {
|
|
185
190
|
await this.visit(this.source);
|
186
191
|
|
187
192
|
const viewBox = this.rootViewBox;
|
188
|
-
let result = defaultSVGViewRect;
|
189
193
|
|
190
|
-
if (viewBox) {
|
191
|
-
|
194
|
+
if (!viewBox) {
|
195
|
+
this.onDetermineExportRect?.(defaultSVGViewRect);
|
192
196
|
}
|
193
197
|
|
194
198
|
this.onFinish?.();
|
195
|
-
return result;
|
196
199
|
}
|
197
200
|
|
198
201
|
// TODO: Handling unsafe data! Tripple-check that this is secure!
|
package/src/Viewport.ts
CHANGED
@@ -118,12 +118,20 @@ export class Viewport {
|
|
118
118
|
return this.transform;
|
119
119
|
}
|
120
120
|
|
121
|
+
public getResolution(): Vec2 {
|
122
|
+
return this.screenRect.size;
|
123
|
+
}
|
124
|
+
|
121
125
|
// Returns the amount a vector on the canvas is scaled to become a vector on the screen.
|
122
126
|
public getScaleFactor(): number {
|
123
127
|
// Use transformVec3 to avoid translating the vector
|
124
128
|
return this.transform.transformVec3(Vec3.unitX).magnitude();
|
125
129
|
}
|
126
130
|
|
131
|
+
public getSizeOfPixelOnCanvas(): number {
|
132
|
+
return 1/this.getScaleFactor();
|
133
|
+
}
|
134
|
+
|
127
135
|
// Returns the angle of the canvas in radians
|
128
136
|
public getRotationAngle(): number {
|
129
137
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
@@ -158,6 +166,54 @@ export class Viewport {
|
|
158
166
|
public roundPoint(point: Point2): Point2 {
|
159
167
|
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
160
168
|
}
|
169
|
+
|
170
|
+
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
|
171
|
+
// centered in the viewport.
|
172
|
+
// Returns null if no transformation is necessary
|
173
|
+
public zoomTo(toMakeVisible: Rect2): Command {
|
174
|
+
let transform = Mat33.identity;
|
175
|
+
|
176
|
+
// Try to move the selection within the center 2/3rds of the viewport.
|
177
|
+
const recomputeTargetRect = () => {
|
178
|
+
// transform transforms objects on the canvas. As such, we need to invert it
|
179
|
+
// to transform the viewport.
|
180
|
+
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
|
181
|
+
return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
|
182
|
+
};
|
183
|
+
|
184
|
+
let targetRect = recomputeTargetRect();
|
185
|
+
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
186
|
+
|
187
|
+
// Ensure that toMakeVisible is at least 1/8th of the visible region.
|
188
|
+
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
|
189
|
+
|
190
|
+
if (largerThanTarget || muchSmallerThanTarget) {
|
191
|
+
// If larger than the target, ensure that the longest axis is visible.
|
192
|
+
// If smaller, shrink the visible rectangle as much as possible
|
193
|
+
const multiplier = (largerThanTarget ? Math.max : Math.min)(
|
194
|
+
toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
|
195
|
+
);
|
196
|
+
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
|
197
|
+
const viewportContentTransform = visibleRectTransform.inverse();
|
198
|
+
|
199
|
+
transform = transform.rightMul(viewportContentTransform);
|
200
|
+
}
|
201
|
+
|
202
|
+
targetRect = recomputeTargetRect();
|
203
|
+
|
204
|
+
// Ensure that the center of the region is visible
|
205
|
+
if (!targetRect.containsRect(toMakeVisible)) {
|
206
|
+
// target position - current position
|
207
|
+
const translation = toMakeVisible.center.minus(targetRect.center);
|
208
|
+
const visibleRectTransform = Mat33.translation(translation);
|
209
|
+
const viewportContentTransform = visibleRectTransform.inverse();
|
210
|
+
|
211
|
+
transform = transform.rightMul(viewportContentTransform);
|
212
|
+
}
|
213
|
+
|
214
|
+
|
215
|
+
return new Viewport.ViewportTransform(transform);
|
216
|
+
}
|
161
217
|
}
|
162
218
|
|
163
219
|
export namespace Viewport { // eslint-disable-line
|
@@ -4,13 +4,13 @@ import EditorImage from '../EditorImage';
|
|
4
4
|
import LineSegment2 from '../geometry/LineSegment2';
|
5
5
|
import Mat33 from '../geometry/Mat33';
|
6
6
|
import Rect2 from '../geometry/Rect2';
|
7
|
-
import AbstractRenderer from '../rendering/AbstractRenderer';
|
7
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
8
8
|
import { ImageComponentLocalization } from './localization';
|
9
9
|
|
10
10
|
export default abstract class AbstractComponent {
|
11
11
|
protected lastChangedTime: number;
|
12
12
|
protected abstract contentBBox: Rect2;
|
13
|
-
|
13
|
+
private zIndex: number;
|
14
14
|
|
15
15
|
// Topmost z-index
|
16
16
|
private static zIndexCounter: number = 0;
|
@@ -20,6 +20,10 @@ export default abstract class AbstractComponent {
|
|
20
20
|
this.zIndex = AbstractComponent.zIndexCounter++;
|
21
21
|
}
|
22
22
|
|
23
|
+
public getZIndex(): number {
|
24
|
+
return this.zIndex;
|
25
|
+
}
|
26
|
+
|
23
27
|
public getBBox(): Rect2 {
|
24
28
|
return this.contentBBox;
|
25
29
|
}
|
@@ -1,8 +1,8 @@
|
|
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';
|
5
|
-
import SVGRenderer from '../rendering/SVGRenderer';
|
4
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
6
6
|
import AbstractComponent from './AbstractComponent';
|
7
7
|
import { ImageComponentLocalization } from './localization';
|
8
8
|
|
package/src/components/Stroke.ts
CHANGED
@@ -2,7 +2,7 @@ import LineSegment2 from '../geometry/LineSegment2';
|
|
2
2
|
import Mat33 from '../geometry/Mat33';
|
3
3
|
import Path from '../geometry/Path';
|
4
4
|
import Rect2 from '../geometry/Rect2';
|
5
|
-
import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/AbstractRenderer';
|
5
|
+
import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/renderers/AbstractRenderer';
|
6
6
|
import AbstractComponent from './AbstractComponent';
|
7
7
|
import { ImageComponentLocalization } from './localization';
|
8
8
|
|
@@ -1,8 +1,8 @@
|
|
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';
|
5
|
-
import SVGRenderer from '../rendering/SVGRenderer';
|
4
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
6
6
|
import AbstractComponent from './AbstractComponent';
|
7
7
|
import { ImageComponentLocalization } from './localization';
|
8
8
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { PathCommandType } from '../../geometry/Path';
|
2
2
|
import Rect2 from '../../geometry/Rect2';
|
3
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
3
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
4
4
|
import { StrokeDataPoint } from '../../types';
|
5
5
|
import Viewport from '../../Viewport';
|
6
6
|
import AbstractComponent from '../AbstractComponent';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Bezier } from 'bezier-js';
|
2
|
-
import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
|
3
3
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
4
4
|
import Rect2 from '../../geometry/Rect2';
|
5
5
|
import { PathCommand, PathCommandType } from '../../geometry/Path';
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { PathCommandType } from '../../geometry/Path';
|
2
2
|
import Rect2 from '../../geometry/Rect2';
|
3
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
3
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
4
4
|
import { StrokeDataPoint } from '../../types';
|
5
5
|
import Viewport from '../../Viewport';
|
6
6
|
import AbstractComponent from '../AbstractComponent';
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import Path from '../../geometry/Path';
|
2
2
|
import Rect2 from '../../geometry/Rect2';
|
3
|
-
import AbstractRenderer from '../../rendering/AbstractRenderer';
|
3
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
4
4
|
import { StrokeDataPoint } from '../../types';
|
5
5
|
import Viewport from '../../Viewport';
|
6
6
|
import AbstractComponent from '../AbstractComponent';
|
@@ -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';
|
package/src/geometry/Mat33.ts
CHANGED
@@ -19,13 +19,23 @@ describe('Path.toString', () => {
|
|
19
19
|
});
|
20
20
|
|
21
21
|
it('should fix rounding errors', () => {
|
22
|
-
const path = new Path(Vec2.of(0.
|
22
|
+
const path = new Path(Vec2.of(0.10000001, 0.19999999), [
|
23
23
|
{
|
24
24
|
kind: PathCommandType.QuadraticBezierTo,
|
25
25
|
controlPoint: Vec2.of(9999, -10.999999995),
|
26
|
-
endPoint: Vec2.of(0.000300001, 1.
|
26
|
+
endPoint: Vec2.of(0.000300001, 1.40000002),
|
27
27
|
},
|
28
28
|
]);
|
29
29
|
expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4');
|
30
30
|
});
|
31
|
+
|
32
|
+
it('should not remove trailing zeroes before decimal points', () => {
|
33
|
+
const path = new Path(Vec2.of(1000, 2_000_000), [
|
34
|
+
{
|
35
|
+
kind: PathCommandType.LineTo,
|
36
|
+
point: Vec2.of(30.0001, 40.000000001),
|
37
|
+
},
|
38
|
+
]);
|
39
|
+
expect(path.toString()).toBe('M1000,2000000L30.0001,40');
|
40
|
+
});
|
31
41
|
});
|
package/src/geometry/Path.ts
CHANGED
@@ -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';
|
@@ -288,8 +288,8 @@ export default class Path {
|
|
288
288
|
const toRoundedString = (num: number): string => {
|
289
289
|
// Try to remove rounding errors. If the number ends in at least three/four zeroes
|
290
290
|
// (or nines) just one or two digits, it's probably a rounding error.
|
291
|
-
const fixRoundingUpExp = /^([-]?\d
|
292
|
-
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d
|
291
|
+
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
|
292
|
+
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
|
293
293
|
|
294
294
|
let text = num.toString();
|
295
295
|
if (text.indexOf('.') === -1) {
|
@@ -314,7 +314,11 @@ export default class Path {
|
|
314
314
|
}
|
315
315
|
|
316
316
|
text = text.replace(fixRoundingUpExp, '$1');
|
317
|
-
|
317
|
+
|
318
|
+
// Remove trailing zeroes
|
319
|
+
text = text.replace(/([.][^0]*)0+$/, '$1');
|
320
|
+
|
321
|
+
// Remove trailing period
|
318
322
|
return text.replace(/[.]$/, '');
|
319
323
|
};
|
320
324
|
|
@@ -6,8 +6,8 @@ import Mat33 from './Mat33';
|
|
6
6
|
|
7
7
|
loadExpectExtensions();
|
8
8
|
|
9
|
-
describe('Rect2
|
10
|
-
it('
|
9
|
+
describe('Rect2', () => {
|
10
|
+
it('width, height should always be positive', () => {
|
11
11
|
expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
|
12
12
|
expect(new Rect2(0, 0, 0, 0).size).objEq(Vec2.zero);
|
13
13
|
expect(Rect2.fromCorners(
|
@@ -19,7 +19,7 @@ describe('Rect2 tests', () => {
|
|
19
19
|
));
|
20
20
|
});
|
21
21
|
|
22
|
-
it('
|
22
|
+
it('bounding boxes should be correctly computed', () => {
|
23
23
|
expect(Rect2.bboxOf([
|
24
24
|
Vec2.zero,
|
25
25
|
])).objEq(Rect2.empty);
|
@@ -42,14 +42,14 @@ describe('Rect2 tests', () => {
|
|
42
42
|
));
|
43
43
|
});
|
44
44
|
|
45
|
-
it('"union"
|
45
|
+
it('"union"s should contain both composite rectangles.', () => {
|
46
46
|
expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(
|
47
47
|
new Rect2(0, 0, 3, 3)
|
48
48
|
);
|
49
49
|
expect(Rect2.empty.union(Rect2.empty)).objEq(Rect2.empty);
|
50
50
|
});
|
51
51
|
|
52
|
-
it('
|
52
|
+
it('should contain points that are within a rectangle', () => {
|
53
53
|
expect(new Rect2(-1, -1, 2, 2).containsPoint(Vec2.zero)).toBe(true);
|
54
54
|
expect(new Rect2(-1, -1, 0, 0).containsPoint(Vec2.zero)).toBe(false);
|
55
55
|
expect(new Rect2(1, 2, 3, 4).containsRect(Rect2.empty)).toBe(false);
|
@@ -67,14 +67,15 @@ describe('Rect2 tests', () => {
|
|
67
67
|
expect(Rect2.empty.containsRect(new Rect2(-1, -1, 3, 3))).toBe(false);
|
68
68
|
});
|
69
69
|
|
70
|
-
it('
|
70
|
+
it('intersecting rectangles should be identified as intersecting', () => {
|
71
71
|
expect(new Rect2(-1, -1, 2, 2).intersects(Rect2.empty)).toBe(true);
|
72
72
|
expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 1, 1))).toBe(true);
|
73
73
|
expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 10, 10))).toBe(true);
|
74
74
|
expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(3, 3, 10, 10))).toBe(false);
|
75
|
+
expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0.2, 0.1, 0, 0))).toBe(true);
|
75
76
|
});
|
76
77
|
|
77
|
-
it('
|
78
|
+
it('intersecting rectangles should have their intersections correctly computed', () => {
|
78
79
|
expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
|
79
80
|
expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
|
80
81
|
new Rect2(0, 0, 1, 1)
|
@@ -93,7 +94,7 @@ describe('Rect2 tests', () => {
|
|
93
94
|
expect(transformedBBox.containsRect(rect)).toBe(true);
|
94
95
|
});
|
95
96
|
|
96
|
-
describe('
|
97
|
+
describe('should correctly expand to include a given point', () => {
|
97
98
|
it('Growing an empty rectange to include (1, 0)', () => {
|
98
99
|
const originalRect = Rect2.empty;
|
99
100
|
const grownRect = originalRect.grownToPoint(Vec2.unitX);
|
@@ -118,4 +119,33 @@ describe('Rect2 tests', () => {
|
|
118
119
|
expect(grown).objEq(new Rect2(0, 0, 2, 2));
|
119
120
|
});
|
120
121
|
});
|
122
|
+
|
123
|
+
describe('divideIntoGrid', () => {
|
124
|
+
it('division of unit square', () => {
|
125
|
+
expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject(
|
126
|
+
[
|
127
|
+
new Rect2(0, 0, 0.5, 0.5), new Rect2(0.5, 0, 0.5, 0.5),
|
128
|
+
new Rect2(0, 0.5, 0.5, 0.5), new Rect2(0.5, 0.5, 0.5, 0.5),
|
129
|
+
]
|
130
|
+
);
|
131
|
+
expect(Rect2.unitSquare.divideIntoGrid(0, 0).length).toBe(0);
|
132
|
+
expect(Rect2.unitSquare.divideIntoGrid(100, 0).length).toBe(0);
|
133
|
+
expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject(
|
134
|
+
[
|
135
|
+
new Rect2(0, 0, 0.25, 1), new Rect2(0.25, 0, 0.25, 1),
|
136
|
+
new Rect2(0.5, 0, 0.25, 1), new Rect2(0.75, 0, 0.25, 1),
|
137
|
+
]
|
138
|
+
);
|
139
|
+
});
|
140
|
+
it('division of translated square', () => {
|
141
|
+
expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject(
|
142
|
+
[
|
143
|
+
new Rect2(3, -3, 2, 4), new Rect2(5, -3, 2, 4),
|
144
|
+
]
|
145
|
+
);
|
146
|
+
});
|
147
|
+
it('division of empty square', () => {
|
148
|
+
expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
|
149
|
+
});
|
150
|
+
});
|
121
151
|
});
|
package/src/geometry/Rect2.ts
CHANGED
@@ -71,7 +71,7 @@ export default class Rect2 {
|
|
71
71
|
}
|
72
72
|
|
73
73
|
// Returns the overlap of this and [other], or null, if no such
|
74
|
-
//
|
74
|
+
// overlap exists
|
75
75
|
public intersection(other: Rect2): Rect2|null {
|
76
76
|
const topLeft = this.topLeft.zip(other.topLeft, Math.max);
|
77
77
|
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
|
@@ -97,6 +97,37 @@ export default class Rect2 {
|
|
97
97
|
);
|
98
98
|
}
|
99
99
|
|
100
|
+
// Returns a the subdivision of this into [columns] columns
|
101
|
+
// and [rows] rows. For example,
|
102
|
+
// Rect2.unitSquare.divideIntoGrid(2, 2)
|
103
|
+
// -> [ 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) ]
|
104
|
+
// The rectangles are ordered in row-major order.
|
105
|
+
public divideIntoGrid(columns: number, rows: number): Rect2[] {
|
106
|
+
const result: Rect2[] = [];
|
107
|
+
if (columns <= 0 || rows <= 0) {
|
108
|
+
return result;
|
109
|
+
}
|
110
|
+
|
111
|
+
const eachRectWidth = this.w / columns;
|
112
|
+
const eachRectHeight = this.h / rows;
|
113
|
+
|
114
|
+
if (eachRectWidth === 0) {
|
115
|
+
columns = 1;
|
116
|
+
}
|
117
|
+
if (eachRectHeight === 0) {
|
118
|
+
rows = 1;
|
119
|
+
}
|
120
|
+
|
121
|
+
for (let j = 0; j < rows; j++) {
|
122
|
+
for (let i = 0; i < columns; i++) {
|
123
|
+
const x = eachRectWidth * i + this.x;
|
124
|
+
const y = eachRectHeight * j + this.y;
|
125
|
+
result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
|
126
|
+
}
|
127
|
+
}
|
128
|
+
return result;
|
129
|
+
}
|
130
|
+
|
100
131
|
// Returns a rectangle containing this and [point].
|
101
132
|
// [margin] is the minimum distance between the new point and the edge
|
102
133
|
// of the resultant rectangle.
|