Comparing version 0.0.10 to 0.1.0
# 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,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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
732025
204
15252