Socket
Socket
Sign inDemoInstall

js-draw

Package Overview
Dependencies
Maintainers
1
Versions
117
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

js-draw - npm Package Compare versions

Comparing version 0.0.10 to 0.1.0

dist/src/rendering/caching/CacheRecord.d.ts

4

CHANGELOG.md
# 0.1.0
* Zoom to import/export region just after importing.
* Rendered strokes are cached if possible for better performance.
# 0.0.10

@@ -3,0 +7,0 @@ * Prefer higher quality rendering except during touchscreen gestures and large groups of commands.

5

dist/src/components/AbstractComponent.d.ts

@@ -5,3 +5,3 @@ import Command from '../commands/Command';

import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import { ImageComponentLocalization } from './localization';

@@ -11,5 +11,6 @@ export default abstract class AbstractComponent {

protected abstract contentBBox: Rect2;
zIndex: number;
private zIndex;
private static zIndexCounter;
protected constructor();
getZIndex(): number;
getBBox(): Rect2;

@@ -16,0 +17,0 @@ abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;

@@ -7,2 +7,5 @@ import EditorImage from '../EditorImage';

}
getZIndex() {
return this.zIndex;
}
getBBox() {

@@ -9,0 +12,0 @@ return this.contentBBox;

import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -4,0 +4,0 @@ import AbstractComponent from '../AbstractComponent';

@@ -1,2 +0,2 @@

import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import Rect2 from '../../geometry/Rect2';

@@ -3,0 +3,0 @@ import Stroke from '../Stroke';

import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -4,0 +4,0 @@ import AbstractComponent from '../AbstractComponent';

import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -4,0 +4,0 @@ import AbstractComponent from '../AbstractComponent';

import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -4,0 +4,0 @@ import Viewport from '../../Viewport';

import LineSegment2 from '../geometry/LineSegment2';
import Mat33 from '../geometry/Mat33';
import Rect2 from '../geometry/Rect2';
import AbstractRenderer, { RenderablePathSpec } from '../rendering/AbstractRenderer';
import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
import AbstractComponent from './AbstractComponent';

@@ -6,0 +6,0 @@ import { ImageComponentLocalization } from './localization';

import LineSegment2 from '../geometry/LineSegment2';
import Mat33 from '../geometry/Mat33';
import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import AbstractComponent from './AbstractComponent';

@@ -6,0 +6,0 @@ import { ImageComponentLocalization } from './localization';

import Rect2 from '../geometry/Rect2';
import SVGRenderer from '../rendering/SVGRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import AbstractComponent from './AbstractComponent';

@@ -4,0 +4,0 @@ // Stores global SVG attributes (e.g. namespace identifiers.)

import LineSegment2 from '../geometry/LineSegment2';
import Mat33 from '../geometry/Mat33';
import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import AbstractComponent from './AbstractComponent';

@@ -6,0 +6,0 @@ import { ImageComponentLocalization } from './localization';

import Rect2 from '../geometry/Rect2';
import SVGRenderer from '../rendering/SVGRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import AbstractComponent from './AbstractComponent';

@@ -4,0 +4,0 @@ export default class UnknownSVGObject extends AbstractComponent {

@@ -9,4 +9,4 @@ import EditorImage from './EditorImage';

import HTMLToolbar from './toolbar/HTMLToolbar';
import { RenderablePathSpec } from './rendering/AbstractRenderer';
import Display, { RenderingMode } from './Display';
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
import Display, { RenderingMode } from './rendering/Display';
import Pointer from './Pointer';

@@ -13,0 +13,0 @@ import Rect2 from './geometry/Rect2';

@@ -19,4 +19,4 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import HTMLToolbar from './toolbar/HTMLToolbar';
import Display, { RenderingMode } from './Display';
import SVGRenderer from './rendering/SVGRenderer';
import Display, { RenderingMode } from './rendering/Display';
import SVGRenderer from './rendering/renderers/SVGRenderer';
import Color4 from './Color4';

@@ -286,3 +286,4 @@ import SVGLoader from './SVGLoader';

}
this.image.render(renderer, this.viewport);
//this.image.render(renderer, this.viewport);
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
this.rerenderQueued = false;

@@ -349,8 +350,9 @@ }

this.showLoadingWarning(0);
const imageRect = yield loader.start((component) => {
this.display.setDraftMode(true);
yield loader.start((component) => {
(new EditorImage.AddElementCommand(component)).apply(this);
}, (countProcessed, totalToProcess) => {
if (countProcessed % 100 === 0) {
if (countProcessed % 500 === 0) {
this.showLoadingWarning(countProcessed / totalToProcess);
this.rerender(false);
this.rerender();
return new Promise(resolve => {

@@ -361,5 +363,9 @@ requestAnimationFrame(() => resolve());

return null;
}, (importExportRect) => {
this.setImportExportRect(importExportRect).apply(this);
this.viewport.zoomTo(importExportRect).apply(this);
});
this.hideLoadingWarning();
this.setImportExportRect(imageRect).apply(this);
this.display.setDraftMode(false);
this.queueRerender();
});

@@ -366,0 +372,0 @@ }

import Editor from './Editor';
import AbstractRenderer from './rendering/AbstractRenderer';
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
import Viewport from './Viewport';

@@ -7,2 +7,4 @@ import AbstractComponent from './components/AbstractComponent';

import { EditorLocalization } from './localization';
import RenderingCache from './rendering/caching/RenderingCache';
export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
export default class EditorImage {

@@ -13,4 +15,4 @@ private root;

findParent(elem: AbstractComponent): ImageNode | null;
private sortLeaves;
render(renderer: AbstractRenderer, viewport: Viewport, minFraction?: number): void;
renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
render(renderer: AbstractRenderer, viewport: Viewport): void;
renderAll(renderer: AbstractRenderer): void;

@@ -29,2 +31,3 @@ getElementsIntersectingRegion(region: Rect2): AbstractComponent[];

export declare type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
declare type TooSmallToRenderCheck = (rect: Rect2) => boolean;
export declare class ImageNode {

@@ -36,9 +39,12 @@ private parent;

private targetChildCount;
private minZIndex;
private maxZIndex;
private id;
private static idCounter;
constructor(parent?: ImageNode | null);
getId(): number;
onContentChange(): void;
getContent(): AbstractComponent | null;
getParent(): ImageNode | null;
private getChildrenInRegion;
getLeavesInRegion(region: Rect2, minFractionOfRegion?: number): ImageNode[];
getChildrenInRegion(region: Rect2): ImageNode[];
getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
getLeaves(): ImageNode[];

@@ -50,2 +56,4 @@ addLeaf(leaf: AbstractComponent): ImageNode;

remove(): void;
render(renderer: AbstractRenderer, visibleRect: Rect2): void;
}
export {};

@@ -14,2 +14,5 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {

import Rect2 from './geometry/Rect2';
export const sortLeavesByZIndex = (leaves) => {
leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
};
// Handles lookup/storage of elements in the image

@@ -25,3 +28,3 @@ export default class EditorImage {

findParent(elem) {
const candidates = this.root.getLeavesInRegion(elem.getBBox());
const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
for (const candidate of candidates) {

@@ -34,13 +37,7 @@ if (candidate.getContent() === elem) {

}
sortLeaves(leaves) {
leaves.sort((a, b) => a.getContent().zIndex - b.getContent().zIndex);
renderWithCache(screenRenderer, cache, viewport) {
cache.render(screenRenderer, this.root, viewport);
}
render(renderer, viewport, minFraction = 0.001) {
// Don't render components that are < 0.1% of the viewport.
const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
this.sortLeaves(leaves);
for (const leaf of leaves) {
// Leaves by definition have content
leaf.getContent().render(renderer, viewport.visibleRect);
}
render(renderer, viewport) {
this.root.render(renderer, viewport.visibleRect);
}

@@ -50,3 +47,3 @@ // Renders all nodes, even ones not within the viewport

const leaves = this.root.getLeaves();
this.sortLeaves(leaves);
sortLeavesByZIndex(leaves);
for (const leaf of leaves) {

@@ -57,4 +54,4 @@ leaf.getContent().render(renderer, leaf.getBBox());

getElementsIntersectingRegion(region) {
const leaves = this.root.getLeavesInRegion(region);
this.sortLeaves(leaves);
const leaves = this.root.getLeavesIntersectingRegion(region);
sortLeavesByZIndex(leaves);
return leaves.map(leaf => leaf.getContent());

@@ -96,2 +93,3 @@ }

_a);
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
export class ImageNode {

@@ -104,5 +102,10 @@ constructor(parent = null) {

this.content = null;
this.minZIndex = null;
this.maxZIndex = null;
this.id = ImageNode.idCounter++;
}
getId() {
return this.id;
}
onContentChange() {
this.id = ImageNode.idCounter++;
}
getContent() {

@@ -119,7 +122,13 @@ return this.content;

}
getChildrenOrSelfIntersectingRegion(region) {
if (this.content) {
return [this];
}
return this.getChildrenInRegion(region);
}
// Returns a list of `ImageNode`s with content (and thus no children).
getLeavesInRegion(region, minFractionOfRegion = 0) {
getLeavesIntersectingRegion(region, isTooSmall) {
const result = [];
// Don't render if too small
if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
if (isTooSmall === null || isTooSmall === void 0 ? void 0 : isTooSmall(this.bbox)) {
return [];

@@ -132,3 +141,3 @@ }

for (const child of children) {
result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
}

@@ -150,2 +159,3 @@ return result;

addLeaf(leaf) {
this.onContentChange();
if (this.content === null && this.children.length === 0) {

@@ -199,13 +209,9 @@ this.content = leaf;

recomputeBBox(bubbleUp) {
var _a, _b, _c;
var _a;
const oldBBox = this.bbox;
if (this.content !== null) {
this.bbox = this.content.getBBox();
this.minZIndex = this.content.zIndex;
this.maxZIndex = this.content.zIndex;
}
else {
this.bbox = Rect2.empty;
this.minZIndex = null;
this.maxZIndex = null;
let isFirst = true;

@@ -220,14 +226,6 @@ for (const child of this.children) {

}
(_a = this.minZIndex) !== null && _a !== void 0 ? _a : (this.minZIndex = child.minZIndex);
(_b = this.maxZIndex) !== null && _b !== void 0 ? _b : (this.maxZIndex = child.maxZIndex);
if (child.minZIndex !== null && this.minZIndex !== null) {
this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
}
if (child.maxZIndex !== null && this.maxZIndex !== null) {
this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
}
}
}
if (bubbleUp && !oldBBox.eq(this.bbox)) {
(_c = this.parent) === null || _c === void 0 ? void 0 : _c.recomputeBBox(true);
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
}

@@ -258,4 +256,2 @@ }

remove() {
this.minZIndex = null;
this.maxZIndex = null;
if (!this.parent) {

@@ -280,2 +276,12 @@ this.content = null;

}
render(renderer, visibleRect) {
// Don't render components that are < 0.1% of the viewport.
const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
sortLeavesByZIndex(leaves);
for (const leaf of leaves) {
// Leaves by definition have content
leaf.getContent().render(renderer, visibleRect);
}
}
}
ImageNode.idCounter = 0;

@@ -7,2 +7,5 @@ import { Vec2 } from './Vec2';

export default class Mat33 {
// ⎡ a1 a2 a3 ⎤
// ⎢ b1 b2 b3 ⎥
// ⎣ c1 c2 c3 ⎦
constructor(a1, a2, a3, b1, b2, b3, c1, c2, c3) {

@@ -9,0 +12,0 @@ this.a1 = a1;

import { Bezier } from 'bezier-js';
import { RenderingStyle, RenderablePathSpec } from '../rendering/AbstractRenderer';
import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
import LineSegment2 from './LineSegment2';

@@ -4,0 +4,0 @@ import Mat33 from './Mat33';

@@ -211,4 +211,4 @@ import { Bezier } from 'bezier-js';

// (or nines) just one or two digits, it's probably a rounding error.
const fixRoundingUpExp = /^([-]?\d*\.?\d*[1-9.])0{4,}\d$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d*9{4,}\d)$/;
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
let text = num.toString();

@@ -234,3 +234,5 @@ if (text.indexOf('.') === -1) {

text = text.replace(fixRoundingUpExp, '$1');
// Remove trailing period (if it exists)
// Remove trailing zeroes
text = text.replace(/([.][^0]*)0+$/, '$1');
// Remove trailing period
return text.replace(/[.]$/, '');

@@ -237,0 +239,0 @@ };

@@ -30,2 +30,3 @@ import LineSegment2 from './LineSegment2';

union(other: Rect2): Rect2;
divideIntoGrid(columns: number, rows: number): Rect2[];
grownToPoint(point: Point2, margin?: number): Rect2;

@@ -32,0 +33,0 @@ grownBy(margin: number): Rect2;

@@ -44,3 +44,3 @@ import LineSegment2 from './LineSegment2';

// Returns the overlap of this and [other], or null, if no such
// / overlap exists
// overlap exists
intersection(other) {

@@ -64,2 +64,29 @@ const topLeft = this.topLeft.zip(other.topLeft, Math.max);

}
// Returns a the subdivision of this into [columns] columns
// and [rows] rows. For example,
// Rect2.unitSquare.divideIntoGrid(2, 2)
// -> [ 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) ]
// The rectangles are ordered in row-major order.
divideIntoGrid(columns, rows) {
const result = [];
if (columns <= 0 || rows <= 0) {
return result;
}
const eachRectWidth = this.w / columns;
const eachRectHeight = this.h / rows;
if (eachRectWidth === 0) {
columns = 1;
}
if (eachRectHeight === 0) {
rows = 1;
}
for (let j = 0; j < rows; j++) {
for (let i = 0; i < columns; i++) {
const x = eachRectWidth * i + this.x;
const y = eachRectHeight * j + this.y;
result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
}
}
return result;
}
// Returns a rectangle containing this and [point].

@@ -66,0 +93,0 @@ // [margin] is the minimum distance between the new point and the edge

import Rect2 from './geometry/Rect2';
import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
export declare const defaultSVGViewRect: Rect2;

@@ -9,2 +9,3 @@ export default class SVGLoader implements ImageLoader {

private onProgress;
private onDetermineExportRect;
private processedCount;

@@ -22,4 +23,4 @@ private totalToProcess;

private getSourceAttrs;
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener): Promise<Rect2>;
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
static fromString(text: string): SVGLoader;
}

@@ -24,2 +24,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

this.onProgress = null;
this.onDetermineExportRect = null;
this.processedCount = 0;

@@ -104,2 +105,3 @@ this.totalToProcess = 0;

updateViewBox(node) {
var _a;
const viewBoxAttr = node.getAttribute('viewBox');

@@ -118,2 +120,3 @@ if (this.rootViewBox || !viewBoxAttr) {

this.rootViewBox = new Rect2(x, y, width, height);
(_a = this.onDetermineExportRect) === null || _a === void 0 ? void 0 : _a.call(this, this.rootViewBox);
}

@@ -160,7 +163,8 @@ updateSVGAttrs(node) {

}
start(onAddComponent, onProgress) {
var _a;
start(onAddComponent, onProgress, onDetermineExportRect = null) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
this.onAddComponent = onAddComponent;
this.onProgress = onProgress;
this.onDetermineExportRect = onDetermineExportRect;
// Estimate the number of tags to process.

@@ -172,8 +176,6 @@ this.totalToProcess = this.source.childElementCount;

const viewBox = this.rootViewBox;
let result = defaultSVGViewRect;
if (viewBox) {
result = Rect2.of(viewBox);
if (!viewBox) {
(_a = this.onDetermineExportRect) === null || _a === void 0 ? void 0 : _a.call(this, defaultSVGViewRect);
}
(_a = this.onFinish) === null || _a === void 0 ? void 0 : _a.call(this);
return result;
(_b = this.onFinish) === null || _b === void 0 ? void 0 : _b.call(this);
});

@@ -180,0 +182,0 @@ }

@@ -1,3 +0,3 @@

import { RenderingMode } from '../Display';
import { RenderingMode } from '../rendering/Display';
import Editor from '../Editor';
export default () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });

@@ -10,3 +10,3 @@ import { ToolType } from '../tools/ToolController';

import { Vec2 } from '../geometry/Vec2';
import SVGRenderer from '../rendering/SVGRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import Viewport from '../Viewport';

@@ -13,0 +13,0 @@ import EventDispatcher from '../EventDispatcher';

@@ -16,3 +16,2 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import { EditorEventType } from '../types';
import Viewport from '../Viewport';
import BaseTool from './BaseTool';

@@ -119,3 +118,3 @@ import { ToolType } from './ToolController';

// Maximum number of strokes to transform without a re-render.
const updateChunkSize = 50;
const updateChunkSize = 100;
class Selection {

@@ -290,6 +289,9 @@ constructor(startPoint, editor) {

}
else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
return true;
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
// As such, test with more lines than just this' edges.
const testLines = [];
for (const subregion of this.region.divideIntoGrid(2, 2)) {
testLines.push(...subregion.getEdges());
}
return false;
return testLines.some(edge => elem.intersects(edge));
});

@@ -398,22 +400,5 @@ // Find the bounding box of all selected elements.

if (hasSelection) {
const visibleRect = this.editor.viewport.visibleRect;
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
const selectionRect = this.selectionBox.region;
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
// Try to move the selection within the center 2/3rds of the viewport.
const targetRect = visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
// Ensure that the selection fits within the target
if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
const viewportContentTransform = visibleRectTransform.inverse();
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
}
// Ensure that the top left is visible
if (!targetRect.containsRect(selectionRect)) {
// target position - current position
const translation = selectionRect.center.minus(targetRect.center);
const visibleRectTransform = Mat33.translation(translation);
const viewportContentTransform = visibleRectTransform.inverse();
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
}
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
}

@@ -420,0 +405,0 @@ }

@@ -92,4 +92,5 @@ import EventDispatcher from './EventDispatcher';

export declare type ComponentAddedListener = (component: AbstractComponent) => void;
export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
export interface ImageLoader {
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener): Promise<Rect2>;
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
}

@@ -96,0 +97,0 @@ export interface StrokeDataPoint {

@@ -0,1 +1,2 @@

import Command from './commands/Command';
import { CommandLocalization } from './commands/localization';

@@ -31,6 +32,9 @@ import Editor from './Editor';

get canvasToScreenTransform(): Mat33;
getResolution(): Vec2;
getScaleFactor(): number;
getSizeOfPixelOnCanvas(): number;
getRotationAngle(): number;
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
roundPoint(point: Point2): Point2;
zoomTo(toMakeVisible: Rect2): Command;
}

@@ -37,0 +41,0 @@ export declare namespace Viewport {

@@ -53,2 +53,5 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {

}
getResolution() {
return this.screenRect.size;
}
// Returns the amount a vector on the canvas is scaled to become a vector on the screen.

@@ -59,2 +62,5 @@ getScaleFactor() {

}
getSizeOfPixelOnCanvas() {
return 1 / this.getScaleFactor();
}
// Returns the angle of the canvas in radians

@@ -81,2 +87,37 @@ getRotationAngle() {

}
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
// centered in the viewport.
// Returns null if no transformation is necessary
zoomTo(toMakeVisible) {
let transform = Mat33.identity;
// Try to move the selection within the center 2/3rds of the viewport.
const recomputeTargetRect = () => {
// transform transforms objects on the canvas. As such, we need to invert it
// to transform the viewport.
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
};
let targetRect = recomputeTargetRect();
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
// Ensure that toMakeVisible is at least 1/8th of the visible region.
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
if (largerThanTarget || muchSmallerThanTarget) {
// If larger than the target, ensure that the longest axis is visible.
// If smaller, shrink the visible rectangle as much as possible
const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
const viewportContentTransform = visibleRectTransform.inverse();
transform = transform.rightMul(viewportContentTransform);
}
targetRect = recomputeTargetRect();
// Ensure that the center of the region is visible
if (!targetRect.containsRect(toMakeVisible)) {
// target position - current position
const translation = toMakeVisible.center.minus(targetRect.center);
const visibleRectTransform = Mat33.translation(translation);
const viewportContentTransform = visibleRectTransform.inverse();
transform = transform.rightMul(viewportContentTransform);
}
return new Viewport.ViewportTransform(transform);
}
}

@@ -83,0 +124,0 @@ // Command that translates/scales the viewport.

{
"name": "js-draw",
"version": "0.0.10",
"version": "0.1.0",
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",

@@ -5,0 +5,0 @@ "main": "dist/src/Editor.js",

@@ -7,3 +7,3 @@ import Command from '../commands/Command';

import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import { ImageComponentLocalization } from './localization';

@@ -14,3 +14,3 @@

protected abstract contentBBox: Rect2;
public zIndex: number;
private zIndex: number;

@@ -25,2 +25,6 @@ // Topmost z-index

public getZIndex(): number {
return this.zIndex;
}
public getBBox(): Rect2 {

@@ -27,0 +31,0 @@ return this.contentBBox;

import { PathCommandType } from '../../geometry/Path';
import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -5,0 +5,0 @@ import Viewport from '../../Viewport';

import { Bezier } from 'bezier-js';
import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
import { Point2, Vec2 } from '../../geometry/Vec2';

@@ -4,0 +4,0 @@ import Rect2 from '../../geometry/Rect2';

import { PathCommandType } from '../../geometry/Path';
import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -5,0 +5,0 @@ import Viewport from '../../Viewport';

import Path from '../../geometry/Path';
import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -5,0 +5,0 @@ import Viewport from '../../Viewport';

import Rect2 from '../../geometry/Rect2';
import AbstractRenderer from '../../rendering/AbstractRenderer';
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
import { StrokeDataPoint } from '../../types';

@@ -4,0 +4,0 @@ import Viewport from '../../Viewport';

@@ -5,3 +5,3 @@ import LineSegment2 from '../geometry/LineSegment2';

import Rect2 from '../geometry/Rect2';
import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/AbstractRenderer';
import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/renderers/AbstractRenderer';
import AbstractComponent from './AbstractComponent';

@@ -8,0 +8,0 @@ import { ImageComponentLocalization } from './localization';

import LineSegment2 from '../geometry/LineSegment2';
import Mat33 from '../geometry/Mat33';
import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import SVGRenderer from '../rendering/SVGRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import AbstractComponent from './AbstractComponent';

@@ -7,0 +7,0 @@ import { ImageComponentLocalization } from './localization';

import LineSegment2 from '../geometry/LineSegment2';
import Mat33 from '../geometry/Mat33';
import Rect2 from '../geometry/Rect2';
import AbstractRenderer from '../rendering/AbstractRenderer';
import SVGRenderer from '../rendering/SVGRenderer';
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import AbstractComponent from './AbstractComponent';

@@ -7,0 +7,0 @@ import { ImageComponentLocalization } from './localization';

@@ -12,5 +12,5 @@

import HTMLToolbar from './toolbar/HTMLToolbar';
import { RenderablePathSpec } from './rendering/AbstractRenderer';
import Display, { RenderingMode } from './Display';
import SVGRenderer from './rendering/SVGRenderer';
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
import Display, { RenderingMode } from './rendering/Display';
import SVGRenderer from './rendering/renderers/SVGRenderer';
import Color4 from './Color4';

@@ -384,3 +384,4 @@ import SVGLoader from './SVGLoader';

this.image.render(renderer, this.viewport);
//this.image.render(renderer, this.viewport);
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
this.rerenderQueued = false;

@@ -468,8 +469,10 @@ }

this.showLoadingWarning(0);
const imageRect = await loader.start((component) => {
this.display.setDraftMode(true);
await loader.start((component) => {
(new EditorImage.AddElementCommand(component)).apply(this);
}, (countProcessed: number, totalToProcess: number) => {
if (countProcessed % 100 === 0) {
if (countProcessed % 500 === 0) {
this.showLoadingWarning(countProcessed / totalToProcess);
this.rerender(false);
this.rerender();
return new Promise(resolve => {

@@ -481,6 +484,10 @@ requestAnimationFrame(() => resolve());

return null;
}, (importExportRect: Rect2) => {
this.setImportExportRect(importExportRect).apply(this);
this.viewport.zoomTo(importExportRect).apply(this);
});
this.hideLoadingWarning();
this.setImportExportRect(imageRect).apply(this);
this.display.setDraftMode(false);
this.queueRerender();
}

@@ -487,0 +494,0 @@

@@ -8,4 +8,4 @@ /* @jest-environment jsdom */

import Color4 from './Color4';
import DummyRenderer from './rendering/DummyRenderer';
import { RenderingStyle } from './rendering/AbstractRenderer';
import DummyRenderer from './rendering/renderers/DummyRenderer';
import { RenderingStyle } from './rendering/renderers/AbstractRenderer';
import createEditor from './testing/createEditor';

@@ -12,0 +12,0 @@

import Editor from './Editor';
import AbstractRenderer from './rendering/AbstractRenderer';
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
import Command from './commands/Command';

@@ -8,3 +8,8 @@ import Viewport from './Viewport';

import { EditorLocalization } from './localization';
import RenderingCache from './rendering/caching/RenderingCache';
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
};
// Handles lookup/storage of elements in the image

@@ -24,3 +29,3 @@ export default class EditorImage {

public findParent(elem: AbstractComponent): ImageNode|null {
const candidates = this.root.getLeavesInRegion(elem.getBBox());
const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
for (const candidate of candidates) {

@@ -34,15 +39,8 @@ if (candidate.getContent() === elem) {

private sortLeaves(leaves: ImageNode[]) {
leaves.sort((a, b) => a.getContent()!.zIndex - b.getContent()!.zIndex);
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
cache.render(screenRenderer, this.root, viewport);
}
public render(renderer: AbstractRenderer, viewport: Viewport, minFraction: number = 0.001) {
// Don't render components that are < 0.1% of the viewport.
const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
this.sortLeaves(leaves);
for (const leaf of leaves) {
// Leaves by definition have content
leaf.getContent()!.render(renderer, viewport.visibleRect);
}
public render(renderer: AbstractRenderer, viewport: Viewport) {
this.root.render(renderer, viewport.visibleRect);
}

@@ -53,3 +51,3 @@

const leaves = this.root.getLeaves();
this.sortLeaves(leaves);
sortLeavesByZIndex(leaves);

@@ -62,4 +60,5 @@ for (const leaf of leaves) {

public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
const leaves = this.root.getLeavesInRegion(region);
this.sortLeaves(leaves);
const leaves = this.root.getLeavesIntersectingRegion(region);
sortLeavesByZIndex(leaves);
return leaves.map(leaf => leaf.getContent()!);

@@ -108,4 +107,5 @@ }

export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
export class ImageNode {

@@ -116,5 +116,6 @@ private content: AbstractComponent|null;

private targetChildCount: number = 30;
private minZIndex: number|null;
private maxZIndex: number|null;
private id: number;
private static idCounter: number = 0;
public constructor(

@@ -127,6 +128,13 @@ private parent: ImageNode|null = null

this.minZIndex = null;
this.maxZIndex = null;
this.id = ImageNode.idCounter++;
}
public getId() {
return this.id;
}
public onContentChange() {
this.id = ImageNode.idCounter++;
}
public getContent(): AbstractComponent|null {

@@ -140,3 +148,3 @@ return this.content;

private getChildrenInRegion(region: Rect2): ImageNode[] {
public getChildrenInRegion(region: Rect2): ImageNode[] {
return this.children.filter(child => {

@@ -147,8 +155,15 @@ return child.getBBox().intersects(region);

public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
if (this.content) {
return [this];
}
return this.getChildrenInRegion(region);
}
// Returns a list of `ImageNode`s with content (and thus no children).
public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
const result: ImageNode[] = [];
// Don't render if too small
if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
if (isTooSmall?.(this.bbox)) {
return [];

@@ -163,3 +178,3 @@ }

for (const child of children) {
result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
}

@@ -186,2 +201,4 @@

public addLeaf(leaf: AbstractComponent): ImageNode {
this.onContentChange();
if (this.content === null && this.children.length === 0) {

@@ -254,8 +271,4 @@ this.content = leaf;

this.bbox = this.content.getBBox();
this.minZIndex = this.content.zIndex;
this.maxZIndex = this.content.zIndex;
} else {
this.bbox = Rect2.empty;
this.minZIndex = null;
this.maxZIndex = null;
let isFirst = true;

@@ -270,11 +283,2 @@

}
this.minZIndex ??= child.minZIndex;
this.maxZIndex ??= child.maxZIndex;
if (child.minZIndex !== null && this.minZIndex !== null) {
this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
}
if (child.maxZIndex !== null && this.maxZIndex !== null) {
this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
}
}

@@ -312,5 +316,2 @@ }

public remove() {
this.minZIndex = null;
this.maxZIndex = null;
if (!this.parent) {

@@ -340,2 +341,13 @@ this.content = null;

}
public render(renderer: AbstractRenderer, visibleRect: Rect2) {
// Don't render components that are < 0.1% of the viewport.
const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
sortLeavesByZIndex(leaves);
for (const leaf of leaves) {
// Leaves by definition have content
leaf.getContent()!.render(renderer, visibleRect);
}
}
}

@@ -10,2 +10,5 @@ import { Point2, Vec2 } from './Vec2';

// ⎡ a1 a2 a3 ⎤
// ⎢ b1 b2 b3 ⎥
// ⎣ c1 c2 c3 ⎦
public constructor(

@@ -12,0 +15,0 @@ public readonly a1: number,

@@ -22,7 +22,7 @@ import Path, { PathCommandType } from './Path';

it('should fix rounding errors', () => {
const path = new Path(Vec2.of(0.100001, 0.199999), [
const path = new Path(Vec2.of(0.10000001, 0.19999999), [
{
kind: PathCommandType.QuadraticBezierTo,
controlPoint: Vec2.of(9999, -10.999999995),
endPoint: Vec2.of(0.000300001, 1.400002),
endPoint: Vec2.of(0.000300001, 1.40000002),
},

@@ -32,2 +32,12 @@ ]);

});
it('should not remove trailing zeroes before decimal points', () => {
const path = new Path(Vec2.of(1000, 2_000_000), [
{
kind: PathCommandType.LineTo,
point: Vec2.of(30.0001, 40.000000001),
},
]);
expect(path.toString()).toBe('M1000,2000000L30.0001,40');
});
});
import { Bezier } from 'bezier-js';
import { RenderingStyle, RenderablePathSpec } from '../rendering/AbstractRenderer';
import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
import LineSegment2 from './LineSegment2';

@@ -291,4 +291,4 @@ import Mat33 from './Mat33';

// (or nines) just one or two digits, it's probably a rounding error.
const fixRoundingUpExp = /^([-]?\d*\.?\d*[1-9.])0{4,}\d$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d*9{4,}\d)$/;
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;

@@ -318,3 +318,7 @@ let text = num.toString();

text = text.replace(fixRoundingUpExp, '$1');
// Remove trailing period (if it exists)
// Remove trailing zeroes
text = text.replace(/([.][^0]*)0+$/, '$1');
// Remove trailing period
return text.replace(/[.]$/, '');

@@ -321,0 +325,0 @@ };

@@ -9,4 +9,4 @@

describe('Rect2 tests', () => {
it('Positive width, height', () => {
describe('Rect2', () => {
it('width, height should always be positive', () => {
expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));

@@ -23,3 +23,3 @@ expect(new Rect2(0, 0, 0, 0).size).objEq(Vec2.zero);

it('Bounding box', () => {
it('bounding boxes should be correctly computed', () => {
expect(Rect2.bboxOf([

@@ -47,3 +47,3 @@ Vec2.zero,

it('"union"ing', () => {
it('"union"s should contain both composite rectangles.', () => {
expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(

@@ -55,3 +55,3 @@ new Rect2(0, 0, 3, 3)

it('contains', () => {
it('should contain points that are within a rectangle', () => {
expect(new Rect2(-1, -1, 2, 2).containsPoint(Vec2.zero)).toBe(true);

@@ -74,3 +74,3 @@ expect(new Rect2(-1, -1, 0, 0).containsPoint(Vec2.zero)).toBe(false);

it('Intersection testing', () => {
it('intersecting rectangles should be identified as intersecting', () => {
expect(new Rect2(-1, -1, 2, 2).intersects(Rect2.empty)).toBe(true);

@@ -80,5 +80,6 @@ expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 1, 1))).toBe(true);

expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(3, 3, 10, 10))).toBe(false);
expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0.2, 0.1, 0, 0))).toBe(true);
});
it('Computing intersections', () => {
it('intersecting rectangles should have their intersections correctly computed', () => {
expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);

@@ -102,3 +103,3 @@ expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(

describe('Grown to include a point', () => {
describe('should correctly expand to include a given point', () => {
it('Growing an empty rectange to include (1, 0)', () => {

@@ -128,2 +129,31 @@ const originalRect = Rect2.empty;

});
describe('divideIntoGrid', () => {
it('division of unit square', () => {
expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject(
[
new Rect2(0, 0, 0.5, 0.5), new Rect2(0.5, 0, 0.5, 0.5),
new Rect2(0, 0.5, 0.5, 0.5), new Rect2(0.5, 0.5, 0.5, 0.5),
]
);
expect(Rect2.unitSquare.divideIntoGrid(0, 0).length).toBe(0);
expect(Rect2.unitSquare.divideIntoGrid(100, 0).length).toBe(0);
expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject(
[
new Rect2(0, 0, 0.25, 1), new Rect2(0.25, 0, 0.25, 1),
new Rect2(0.5, 0, 0.25, 1), new Rect2(0.75, 0, 0.25, 1),
]
);
});
it('division of translated square', () => {
expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject(
[
new Rect2(3, -3, 2, 4), new Rect2(5, -3, 2, 4),
]
);
});
it('division of empty square', () => {
expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
});
});
});

@@ -74,3 +74,3 @@ import LineSegment2 from './LineSegment2';

// Returns the overlap of this and [other], or null, if no such
// / overlap exists
// overlap exists
public intersection(other: Rect2): Rect2|null {

@@ -101,2 +101,33 @@ const topLeft = this.topLeft.zip(other.topLeft, Math.max);

// Returns a the subdivision of this into [columns] columns
// and [rows] rows. For example,
// Rect2.unitSquare.divideIntoGrid(2, 2)
// -> [ 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) ]
// The rectangles are ordered in row-major order.
public divideIntoGrid(columns: number, rows: number): Rect2[] {
const result: Rect2[] = [];
if (columns <= 0 || rows <= 0) {
return result;
}
const eachRectWidth = this.w / columns;
const eachRectHeight = this.h / rows;
if (eachRectWidth === 0) {
columns = 1;
}
if (eachRectHeight === 0) {
rows = 1;
}
for (let j = 0; j < rows; j++) {
for (let i = 0; i < columns; i++) {
const x = eachRectWidth * i + this.x;
const y = eachRectHeight * j + this.y;
result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
}
}
return result;
}
// Returns a rectangle containing this and [point].

@@ -103,0 +134,0 @@ // [margin] is the minimum distance between the new point and the edge

@@ -8,4 +8,4 @@ import Color4 from './Color4';

import Rect2 from './geometry/Rect2';
import { RenderablePathSpec, RenderingStyle } from './rendering/AbstractRenderer';
import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';

@@ -20,2 +20,4 @@ type OnFinishListener = ()=> void;

private onProgress: OnProgressListener|null = null;
private onDetermineExportRect: OnDetermineExportRectListener|null = null;
private processedCount: number = 0;

@@ -131,2 +133,3 @@ private totalToProcess: number = 0;

this.rootViewBox = new Rect2(x, y, width, height);
this.onDetermineExportRect?.(this.rootViewBox);
}

@@ -178,6 +181,8 @@

public async start(
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
): Promise<Rect2> {
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener,
onDetermineExportRect: OnDetermineExportRectListener|null = null
): Promise<void> {
this.onAddComponent = onAddComponent;
this.onProgress = onProgress;
this.onDetermineExportRect = onDetermineExportRect;

@@ -192,10 +197,8 @@ // Estimate the number of tags to process.

const viewBox = this.rootViewBox;
let result = defaultSVGViewRect;
if (viewBox) {
result = Rect2.of(viewBox);
if (!viewBox) {
this.onDetermineExportRect?.(defaultSVGViewRect);
}
this.onFinish?.();
return result;
}

@@ -202,0 +205,0 @@

@@ -1,4 +0,4 @@

import { RenderingMode } from '../Display';
import { RenderingMode } from '../rendering/Display';
import Editor from '../Editor';
export default () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });

@@ -13,3 +13,3 @@ import Editor from '../Editor';

import { Vec2 } from '../geometry/Vec2';
import SVGRenderer from '../rendering/SVGRenderer';
import SVGRenderer from '../rendering/renderers/SVGRenderer';
import Viewport from '../Viewport';

@@ -16,0 +16,0 @@ import EventDispatcher from '../EventDispatcher';

@@ -5,3 +5,3 @@ /* @jest-environment jsdom */

import Stroke from '../components/Stroke';
import { RenderingMode } from '../Display';
import { RenderingMode } from '../rendering/Display';
import Editor from '../Editor';

@@ -8,0 +8,0 @@ import EditorImage from '../EditorImage';

@@ -10,3 +10,2 @@ import Command from '../commands/Command';

import { EditorEventType, PointerEvt } from '../types';
import Viewport from '../Viewport';
import BaseTool from './BaseTool';

@@ -124,3 +123,3 @@ import { ToolType } from './ToolController';

// Maximum number of strokes to transform without a re-render.
const updateChunkSize = 50;
const updateChunkSize = 100;

@@ -346,6 +345,12 @@ class Selection {

return true;
} else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
return true;
}
return false;
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
// As such, test with more lines than just this' edges.
const testLines = [];
for (const subregion of this.region.divideIntoGrid(2, 2)) {
testLines.push(...subregion.getEdges());
}
return testLines.some(edge => elem.intersects(edge));
});

@@ -490,5 +495,2 @@

if (hasSelection) {
const visibleRect = this.editor.viewport.visibleRect;
const selectionRect = this.selectionBox.region;
this.editor.announceForAccessibility(

@@ -498,27 +500,4 @@ this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())

// Try to move the selection within the center 2/3rds of the viewport.
const targetRect = visibleRect.transformedBoundingBox(
Mat33.scaling2D(2 / 3, visibleRect.center)
);
// Ensure that the selection fits within the target
if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
const multiplier = Math.max(
selectionRect.w / targetRect.w, selectionRect.h / targetRect.h
);
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
const viewportContentTransform = visibleRectTransform.inverse();
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
}
// Ensure that the top left is visible
if (!targetRect.containsRect(selectionRect)) {
// target position - current position
const translation = selectionRect.center.minus(targetRect.center);
const visibleRectTransform = Mat33.translation(translation);
const viewportContentTransform = visibleRectTransform.inverse();
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
}
const selectionRect = this.selectionBox.region;
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
}

@@ -525,0 +504,0 @@ }

@@ -137,7 +137,14 @@ // Types related to the image editor

export type ComponentAddedListener = (component: AbstractComponent)=> void;
// Called when a new estimate for the import/export rect has been generated. This can be called multiple times.
// Only the last call to this listener must be accurate.
// The import/export rect is also returned by [start].
export type OnDetermineExportRectListener = (exportRect: Rect2)=> void;
export interface ImageLoader {
// Returns the main region of the loaded image
start(
onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener
): Promise<Rect2>;
onAddComponent: ComponentAddedListener,
onProgressListener: OnProgressListener,
onDetermineExportRect?: OnDetermineExportRectListener,
): Promise<void>;
}

@@ -144,0 +151,0 @@

@@ -121,2 +121,6 @@ import Command from './commands/Command';

public getResolution(): Vec2 {
return this.screenRect.size;
}
// Returns the amount a vector on the canvas is scaled to become a vector on the screen.

@@ -128,2 +132,6 @@ public getScaleFactor(): number {

public getSizeOfPixelOnCanvas(): number {
return 1/this.getScaleFactor();
}
// Returns the angle of the canvas in radians

@@ -163,2 +171,50 @@ public getRotationAngle(): number {

}
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
// centered in the viewport.
// Returns null if no transformation is necessary
public zoomTo(toMakeVisible: Rect2): Command {
let transform = Mat33.identity;
// Try to move the selection within the center 2/3rds of the viewport.
const recomputeTargetRect = () => {
// transform transforms objects on the canvas. As such, we need to invert it
// to transform the viewport.
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
};
let targetRect = recomputeTargetRect();
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
// Ensure that toMakeVisible is at least 1/8th of the visible region.
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
if (largerThanTarget || muchSmallerThanTarget) {
// If larger than the target, ensure that the longest axis is visible.
// If smaller, shrink the visible rectangle as much as possible
const multiplier = (largerThanTarget ? Math.max : Math.min)(
toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
);
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
const viewportContentTransform = visibleRectTransform.inverse();
transform = transform.rightMul(viewportContentTransform);
}
targetRect = recomputeTargetRect();
// Ensure that the center of the region is visible
if (!targetRect.containsRect(toMakeVisible)) {
// target position - current position
const translation = toMakeVisible.center.minus(targetRect.center);
const visibleRectTransform = Mat33.translation(translation);
const viewportContentTransform = visibleRectTransform.inverse();
transform = transform.rightMul(viewportContentTransform);
}
return new Viewport.ViewportTransform(transform);
}
}

@@ -165,0 +221,0 @@

@@ -27,3 +27,4 @@ {

"**/*.test.ts",
"__mocks__/*"
],
}

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc