UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

596 lines (553 loc) 32.5 kB
import { CanvasManagerBase } from "./CanvasManagerBase"; import { IModelSizeListener } from "../Model/ModelManipulator"; import { ILayoutPointResolver } from "./CanvasItemsManager"; import { IViewChangesListener, IZoomChangesListener, AutoZoomMode } from "../Settings"; import { EventDispatcher } from "../Utils"; import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle"; import { Offsets } from "@devexpress/utils/lib/geometry/offsets"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { IScrollView, IScrollViewListener } from "./ScrollView"; import { GroupPrimitive } from "./Primitives/GroupPrimitive"; import { ClipPathPrimitive } from "./Primitives/ClipPathPrimitive"; import { RectanglePrimitive } from "./Primitives/RectaglePrimitive"; import { RenderUtils } from "./Utils"; import { IMouseOperationsListener } from "../Events/Event"; import { ShadowFilterPrimitive } from "./Primitives/ShadowFilterPrimitive"; import { EmptyStyle } from "../Model/Style"; import { DOMManipulator } from "./DOMManipulator"; import { OrientationInfo, ICanvasViewManager } from "./ICanvasViewManager"; import { DomUtils } from "@devexpress/utils/lib/utils/dom"; import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter"; export const CANVAS_MIN_PADDING = 8; export const CANVAS_SCROLL_PADDING = 18; export const CROP_OFFSET = 40; const DRAG_SCROLL_CSSCLASS = "dxdi-drag-scroll"; const DRAG_ITEM_CSSCLASS = "dxdi-drag-item"; declare type Transition = { translate: Point, scroll: Point }; export class CanvasViewManager extends CanvasManagerBase implements IModelSizeListener, ICanvasViewManager, ILayoutPointResolver, IViewChangesListener, IZoomChangesListener, IMouseOperationsListener, IScrollViewListener { private modelSize: Size; private containerSize: Size; private paddings: Offsets = new Offsets(0, 0, 0, 0); private scroll: Point = new Point(0, 0); private simpleView: boolean; private crop: Offsets = Offsets.empty(); private lockAutoZoom: boolean = false; autoZoom: AutoZoomMode; autoZoomLocked: boolean; private autoScrollLocker: number = 0; protected scrollView: IScrollView; private fixedZoomLevel: number; readonly pageClipPathId = RenderUtils.generateSvgElementId("page-clip"); readonly pageShadowId = RenderUtils.generateSvgElementId("page-shadow"); canvasElement: SVGGElement; pageElement: SVGGElement; onViewChanged: EventDispatcher<ICanvasViewListener> = new EventDispatcher(); constructor(scrollView: IScrollView, protected svgElement: SVGSVGElement, modelSize: Size, fixedZoomLevel: number, autoZoom: AutoZoomMode, simpleView: boolean, rectangle: Rectangle, dom: DOMManipulator) { super(fixedZoomLevel, dom); scrollView.onScroll.add(this); modelSize = modelSize.clone().applyConverter(UnitConverter.twipsToPixelsF); this.scrollView = scrollView; this.modelSize = modelSize; this.simpleView = simpleView; this.fixedZoomLevel = fixedZoomLevel; this.autoZoom = autoZoom; this.crop = this.rectangleToCrop(rectangle, modelSize); this.updateElements(modelSize.clone().multiply(fixedZoomLevel, fixedZoomLevel), Point.zero(), simpleView); this.getOrCreateElement("shadow", new ShadowFilterPrimitive(this.pageShadowId), this.svgElement); this.containerSize = scrollView.getSize(); } adjust(resetPaddings?: OrientationInfo, saveVerticalScroll?: boolean) { let offset: Offsets; if(!resetPaddings) { resetPaddings = { vertical: false, horizontal: false }; offset = Offsets.empty(); } this.containerSize = this.scrollView.getSize(); this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, this.simpleView, this.crop, resetPaddings, this.containerSize, offset, saveVerticalScroll); this.tryNormalizePaddings(); } notifyModelSizeChanged(size: Size, offset?: Offsets) { size = size.clone().applyConverter(UnitConverter.twipsToPixelsF); const resetPaddings = { horizontal: !offset, vertical: !offset }; this.adjustCore(size, this.fixedZoomLevel, this.autoZoom, this.simpleView, this.crop, resetPaddings, this.containerSize, offset && offset.clone().applyConverter(UnitConverter.twipsToPixelsF)); this.modelSize = size; } notifyModelRectangleChanged(rectangle: Rectangle) { const crop = this.rectangleToCrop(rectangle, this.modelSize); if(!this.crop || !this.crop.equals(crop)) { if(this.simpleView) this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, this.simpleView, crop, { horizontal: false, vertical: false }, this.containerSize, Offsets.empty()); this.crop = crop; } } notifySnapPointPositionChanged(point: Point) { } notifyZoomChanged(fixedZoomLevel: number, autoZoom: AutoZoomMode) { this.adjustCore(this.modelSize, fixedZoomLevel, autoZoom, this.simpleView, this.crop, { horizontal: false, vertical: false }, this.containerSize); this.fixedZoomLevel = fixedZoomLevel; this.autoZoom = autoZoom; } notifyViewChanged(simpleView: boolean) { this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, simpleView, this.crop, { vertical: true, horizontal: true }, this.containerSize); this.simpleView = simpleView; } notifyGridChanged(showGrid: boolean, gridSize: number) { } notifyDragStart(itemKeys: string[]) { this.lockAutoZoom = true; DomUtils.addClassName(this.svgElement, DRAG_ITEM_CSSCLASS); } notifyDragEnd(itemKeys: string[]) { this.lockAutoZoom = false; DomUtils.removeClassName(this.svgElement, DRAG_ITEM_CSSCLASS); this.adjustAfterDragEnd(); } adjustAfterDragEnd() { if(this.autoZoom && !this.autoZoomLocked) this.adjust({ horizontal: true, vertical: this.autoZoom === AutoZoomMode.FitContent }, this.autoZoom === AutoZoomMode.FitToWidth); } notifyShowContextToolbox() { this.autoZoomLocked = true; } notifyHideContextToolbox() { this.autoZoomLocked = false; this.adjustAfterDragEnd(); } notifyDragScrollStart() { DomUtils.addClassName(this.svgElement, DRAG_SCROLL_CSSCLASS); } notifyDragScrollEnd() { DomUtils.removeClassName(this.svgElement, DRAG_SCROLL_CSSCLASS); } notifyScrollChanged(getScroll: () => Point) { this.scroll = getScroll(); } checkFitToCanvas(containerSize?: Size): OrientationInfo { containerSize = containerSize || this.containerSize; const scrollSize = this.scrollView.getScrollBarWidth(); containerSize = containerSize.clone().offset(-CANVAS_MIN_PADDING * 2, -CANVAS_MIN_PADDING * 2).nonNegativeSize(); const modelAbsSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom); const scrollbars = this.checkScrollBars(containerSize, scrollSize, modelAbsSize, Offsets.empty()); containerSize = containerSize.clone().offset(scrollbars.vertical ? -scrollSize : 0, scrollbars.horizontal ? -scrollSize : 0).nonNegativeSize(); return { vertical: containerSize.height >= modelAbsSize.height, horizontal: containerSize.width >= modelAbsSize.width }; } private rectangleToCrop(rectangle: Rectangle, modelSize: Size): Offsets { const absRectangle = rectangle.clone().applyConverter(UnitConverter.twipsToPixelsF); return new Offsets( this.correctCrop(absRectangle.x), this.correctCrop(modelSize.width - absRectangle.right), this.correctCrop(absRectangle.y), this.correctCrop(modelSize.height - absRectangle.bottom) ); } private correctCrop(newVal: number): number { return CROP_OFFSET * Math.floor(newVal / CROP_OFFSET); } private setActualZoom(actualZoom: number) { if(this.actualZoom !== actualZoom) { this.actualZoom = actualZoom; this.raiseActualZoomChanged(); } } getActualAutoZoomLevel(autoZoom: AutoZoomMode): number { if(autoZoom === AutoZoomMode.Disabled) return this.actualZoom; const containerSize = this.containerSize; const scrollbarWidth = this.scrollView.getScrollBarWidth(); const actualModelSizeWithoutZoom = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop); return this.getActualAutoZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, autoZoom); } private getActualZoom(containerSize: Size, scrollbarWidth: number, actualModelSizeWithoutZoom: Size, fixedZoom: number, autoZoom: AutoZoomMode) { return this.lockAutoZoom ? this.actualZoom : autoZoom === AutoZoomMode.Disabled ? fixedZoom : this.getActualAutoZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, autoZoom); } private getActualAutoZoom(containerSize: Size, scrollbarWidth: number, actualModelSizeWithoutZoom: Size, autoZoom: AutoZoomMode): number { if(containerSize.width === 0 || containerSize.height === 0) return 1; if(autoZoom === AutoZoomMode.FitContent) return Math.min( (containerSize.width - CANVAS_MIN_PADDING * 2) / actualModelSizeWithoutZoom.width, (containerSize.height - CANVAS_MIN_PADDING * 2) / actualModelSizeWithoutZoom.height, 1); return Math.min((containerSize.width - CANVAS_MIN_PADDING * 2 - scrollbarWidth) / actualModelSizeWithoutZoom.width, 1); } raiseActualZoomChanged() : void { this.onViewChanged.raise1(l => l.notifyActualZoomChanged(this.actualZoom)); } tryNormalizePaddings(): void { const scrollbarWidth = this.scrollView.getScrollBarWidth(); const actualModelSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom); const translate = new Point(this.paddings.left, this.paddings.top); const currentTail = new Size(this.paddings.right, this.paddings.bottom); const tail = this.getTailSpace(translate, this.scroll, actualModelSize, this.containerSize, scrollbarWidth); if(!tail.equals(currentTail)) this.applyChanges(new Offsets(translate.x, tail.width, translate.y, tail.height), actualModelSize, this.simpleView, this.crop.clone().multiply(this.actualZoom)); } scrollBy(offset: Point): Point { let scroll = this.scroll; const containerSize = this.containerSize; const scrollbarWidth = this.scrollView.getScrollBarWidth(); const actualModelSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom); const scrollbars = this.checkScrollBars(containerSize, scrollbarWidth, actualModelSize, this.paddings); let translate = new Point(this.paddings.left, this.paddings.top); let tail = new Size(this.paddings.right, this.paddings.bottom); ({ scroll, offset } = this.changeScrollByOffset(translate, scroll, tail, actualModelSize, offset, containerSize, scrollbars)); ({ translate, offset } = this.changeTranslateByOffset(translate, tail, offset, scrollbars)); ({ translate, scroll } = this.cropHiddenHead(translate, scroll)); tail = this.getTailSpace(translate, scroll, actualModelSize, containerSize, scrollbarWidth); this.applyChanges(new Offsets(translate.x, tail.width, translate.y, tail.height), actualModelSize, this.simpleView, this.crop.clone().multiply(this.actualZoom), scroll); return offset; } private changeScrollByOffset(curTranslate: Point, curScroll: Point, curTail: Size, modelSize: Size, curOffset: Point, containerSize: Size, scrollbars: OrientationInfo): { scroll: Point, offset: Point } { const scroll = curScroll.clone(); const offset = curOffset.clone(); if(curOffset.x && scrollbars.horizontal) scroll.x -= (offset.x = -this.getScrollDeltaByOffset(curOffset.x, curScroll.x, curTranslate.x + modelSize.width + curTail.width, containerSize.width, scrollbars.vertical)); if(curOffset.y && scrollbars.vertical) scroll.y -= (offset.y = -this.getScrollDeltaByOffset(curOffset.y, curScroll.y, curTranslate.y + modelSize.height + curTail.height, containerSize.height, scrollbars.horizontal)); return { scroll, offset }; } private changeTranslateByOffset(curTranslate: Point, curTail: Size, curOffset: Point, scrollbars: OrientationInfo): { translate: Point, offset: Point } { const translate = curTranslate.clone(); const offset = curOffset.clone(); if(curOffset.x && !scrollbars.horizontal) translate.x += (offset.x = this.getTranslateDeltaByOffset(curOffset.x, translate.x, curTail.width)); if(curOffset.y && !scrollbars.vertical) translate.y += (offset.y = this.getTranslateDeltaByOffset(curOffset.y, translate.y, curTail.height)); return { translate, offset }; } private getScrollDeltaByOffset(offset: number, scroll: number, commonWidth: number, containerWidth: number, hasScrollbar: boolean) { if(offset > 0) return -Math.min(scroll, offset); const maxScroll = commonWidth - (containerWidth - (hasScrollbar ? this.scrollView.getScrollBarWidth() : 0)); return Math.min(maxScroll - scroll, -offset); } private getTranslateDeltaByOffset(offset: number, headPadding: number, tailPadding: number) { if(!offset) return 0; return offset < 0 ? -Math.min(headPadding - CANVAS_MIN_PADDING, -offset) : Math.min(tailPadding - CANVAS_MIN_PADDING, offset); } private getActualModelSizeWithoutZoom(originModelSize: Size, simpleView: boolean, crop: Offsets): Size { return simpleView && crop ? originModelSize.clone().offset(-crop.horizontal, -crop.vertical).nonNegativeSize() : originModelSize; } setScrollTo(modelPoint: Point, offsetPoint?: Point) { const containerSize = this.containerSize; const shift = this.getVisibileAreaAbsShift(); const absPoint = modelPoint .clone().applyConverter(UnitConverter.twipsToPixelsF) .clone().multiply(this.actualZoom, this.actualZoom) .clone().offset(shift.x, shift.y); const scroll = this.scroll; if(!offsetPoint) { if(absPoint.x < 0) scroll.x += absPoint.x - CANVAS_MIN_PADDING; if(absPoint.y < 0) scroll.y += absPoint.y - CANVAS_MIN_PADDING; if(absPoint.x > containerSize.width) scroll.x += (absPoint.x - containerSize.width + CANVAS_MIN_PADDING); if(absPoint.y > containerSize.height) scroll.y += (absPoint.y - containerSize.height + CANVAS_MIN_PADDING); } else { scroll.x += absPoint.x - offsetPoint.x; scroll.y += absPoint.y - offsetPoint.y; } this.setScroll(scroll); } scrollIntoView(rectangle: Rectangle) { rectangle = rectangle .clone() .applyConverter(UnitConverter.twipsToPixelsF) .multiply(this.actualZoom, this.actualZoom) .moveRectangle(this.paddings.left, this.paddings.top); const scroll = this.scroll; const container = this.containerSize; if(rectangle.x >= scroll.x && rectangle.y >= scroll.y && rectangle.right <= scroll.x + container.width && rectangle.bottom <= scroll.y + container.height) return; const newScroll = scroll.clone(); if(rectangle.x < scroll.x) newScroll.x = rectangle.x - CANVAS_SCROLL_PADDING; else if(rectangle.right > scroll.x + container.width) newScroll.x = Math.min(rectangle.x - CANVAS_SCROLL_PADDING, rectangle.right + CANVAS_SCROLL_PADDING - container.width); if(rectangle.y < scroll.y) newScroll.y = rectangle.y - CANVAS_SCROLL_PADDING; else newScroll.y = Math.min(rectangle.y - CANVAS_SCROLL_PADDING, rectangle.bottom + CANVAS_SCROLL_PADDING - container.height); this.setScroll(newScroll); } private setScroll(pt: Point) { const modelAbsSize = this.modelSize.clone().multiply(this.actualZoom, this.actualZoom); pt.x = Math.max(0, Math.min(pt.x, modelAbsSize.width + this.paddings.horizontal - this.containerSize.width)); pt.y = Math.max(0, Math.min(pt.y, modelAbsSize.height + this.paddings.vertical - this.containerSize.height)); this.dom.changeByFunc(null, () => { this.scrollView.setScroll(pt.x, pt.y); }); this.scroll = pt.clone(); } private updateElements(modelAbsSize: Size, translate: Point, simpleView: boolean) { this.updatePageElement(modelAbsSize, translate, simpleView); this.updateCanvasElement(translate); } private updateCanvasElement(translate: Point) { this.canvasElement = this.getOrCreateElement("dxdi-main", new GroupPrimitive([], "dxdi-main", null, null, el => { el.setAttribute("transform", `translate(${Math.round(translate.x)}, ${Math.round(translate.y)})`); }), this.svgElement); } private updatePageElement(modelAbsSize: Size, translate: Point, simpleView: boolean) { if(simpleView) this.updatePageElementCore("", 0, 0, modelAbsSize.width, modelAbsSize.height); else { const x = translate.x; const y = translate.y; const modelAbsWidth = modelAbsSize.width; const modelAbsHeight = modelAbsSize.height; this.createPageShadow(x, y, modelAbsWidth, modelAbsHeight); this.updatePageElementCore(this.pageClipPathId, Math.round(x), Math.round(y), modelAbsWidth, modelAbsHeight); } } private createPageShadow(left: number, top: number, width: number, height: number) : void { this.getOrCreateElement("pageShadowRect", new RectanglePrimitive( left.toString(), top.toString(), width.toString(), height.toString(), new EmptyStyle({ "filter": RenderUtils.getUrlPathById(this.pageShadowId) }), "dxdi-page-shadow"), this.svgElement, <SVGElement> this.svgElement.firstChild); } private updatePageElementCore(groupClipPathId: string, translateX: number, translateY: number, modelAbsWidth: number, modelAbsHeight: number): void { this.pageElement = this.getOrCreateElement("page", new GroupPrimitive([], "dxdi-page", null, groupClipPathId, el => { el.setAttribute("transform", `translate(${translateX}, ${translateY})`); }), this.svgElement); this.getOrCreateElement("pageClip", this.createPageClipPathPrimitive(modelAbsWidth, modelAbsHeight), this.svgElement); } private createPageClipPathPrimitive(modelAbsWidth: number, modelAbsHeight: number): ClipPathPrimitive { return new ClipPathPrimitive(this.pageClipPathId, [new RectanglePrimitive(0, 0, modelAbsWidth.toString(), modelAbsHeight.toString())]); } adjustCore(newModelSize: Size, fixedZoomLevel: number, autoZoom: AutoZoomMode, simpleView: boolean, crop: Offsets, resetPaddings: OrientationInfo, containerSize: Size, offset?: Offsets, saveVerticalScroll?: boolean) { const actualModelSizeWithoutZoom = this.getActualModelSizeWithoutZoom(newModelSize, simpleView, crop); if(!this.lockAutoZoom && (autoZoom || !offset || !this.modelSize)) { const scrollbarWidth = this.scrollView.getScrollBarWidth(); const actualZoom = this.getActualZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, fixedZoomLevel, autoZoom); if(autoZoom && actualZoom === this.actualZoom && (!resetPaddings.horizontal || (!resetPaddings.vertical && !saveVerticalScroll))) this.resizeView(actualModelSizeWithoutZoom, actualZoom, containerSize, simpleView, crop, offset || Offsets.empty()); else { this.resetView(actualModelSizeWithoutZoom, actualZoom, containerSize, simpleView, crop, resetPaddings); this.setActualZoom(actualZoom); } } else this.resizeView(actualModelSizeWithoutZoom, this.actualZoom, containerSize, simpleView, crop, offset); } private resetView(actualModelSizeWithoutZoom: Size, actualZoom: number, containerSize: Size, simpleView: boolean, cropWithoutZoom: Offsets, toReset: OrientationInfo) { const actualModelSize = actualModelSizeWithoutZoom.clone().multiply(actualZoom, actualZoom); const paddings = Offsets.fromNumber(CANVAS_MIN_PADDING); toReset = toReset || { horizontal: true, vertical: true }; if(!toReset.horizontal && this.paddings) { paddings.left = this.paddings.left; paddings.right = this.paddings.right; } if(!toReset.vertical && this.paddings) { paddings.top = this.paddings.top; paddings.bottom = this.paddings.bottom; } const scrollbars = this.checkScrollBars(containerSize, this.scrollView.getScrollBarWidth(), actualModelSize, paddings); const scrollBarWidth = this.scrollView.getScrollBarWidth(); const scroll = (toReset.horizontal || toReset.vertical) ? this.scroll : undefined; if(toReset.horizontal) { const paddingsH = Math.max((containerSize.width - (scrollbars.vertical ? scrollBarWidth : 0) - actualModelSize.width) / 2, CANVAS_MIN_PADDING); paddings.left = paddingsH; paddings.right = paddingsH; scroll.x = 0; } if(toReset.vertical) { const paddingsV = Math.max((containerSize.height - (scrollbars.horizontal ? scrollBarWidth : 0) - actualModelSize.height) / 2, CANVAS_MIN_PADDING); paddings.top = paddingsV; paddings.bottom = paddingsV; scroll.y = 0; } this.applyChanges(paddings, actualModelSize, simpleView, cropWithoutZoom.clone().multiply(actualZoom), scroll); } private resizeView(actualModelSizeWithoutZoom: Size, actualZoom: number, containerSize: Size, simpleView: boolean, cropWithoutZoom: Offsets, offset: Offsets) { const oldZoom = this.actualZoom; const oldCrop = this.simpleView && this.crop ? this.crop.clone().multiply(oldZoom) : Offsets.empty(); const actualModelSize = actualModelSizeWithoutZoom.clone().multiply(actualZoom, actualZoom); offset = offset.clone().multiply(actualZoom); const newCrop = simpleView && cropWithoutZoom ? cropWithoutZoom.clone().multiply(actualZoom) : Offsets.empty(); let translate: Point = new Point(this.paddings.left, this.paddings.top); let scroll = this.scroll; ({ translate, scroll } = this.applyOffset(translate, scroll, oldCrop, newCrop, offset)); ({ translate, scroll } = this.cropHiddenHead(translate, scroll)); const tailSpace = this.getTailSpace(translate, scroll, actualModelSize, containerSize, this.scrollView.getScrollBarWidth()); if(!simpleView) { const maxTailSpaceWidth = containerSize.width - CANVAS_SCROLL_PADDING; const maxTailSpaceHeight = containerSize.height - CANVAS_SCROLL_PADDING; if(offset.left < 0) if(translate.x > maxTailSpaceWidth) { translate.x = maxTailSpaceWidth; scroll.x = 0; } if(offset.right < 0) if(tailSpace.width > maxTailSpaceWidth) { tailSpace.width = maxTailSpaceWidth; if(scroll.x > actualModelSize.width) scroll.x = actualModelSize.width; } if(offset.top < 0) if(translate.y > maxTailSpaceHeight) { translate.y = maxTailSpaceHeight; scroll.y = 0; } if(offset.bottom < 0) if(tailSpace.height > maxTailSpaceHeight) { tailSpace.height = maxTailSpaceHeight; if(scroll.y > actualModelSize.height) scroll.y = actualModelSize.height; } } const newPaddings = new Offsets(translate.x, tailSpace.width, translate.y, tailSpace.height); this.applyChanges(newPaddings, actualModelSize, simpleView, newCrop, scroll); } applyChanges(paddings: Offsets, actualModelSize: Size, simpleView: boolean, crop: Offsets, scroll?: Point) { let translate = new Point(paddings.left, paddings.top); if(simpleView && crop) translate = translate.clone().offset(-crop.left, -crop.top); this.updateElements(actualModelSize, translate, simpleView); this.setSvgSize(actualModelSize.width + paddings.horizontal, actualModelSize.height + paddings.vertical); this.onViewChanged.raise1(l => l.notifyViewAdjusted(new Point(translate.x, translate.y))); if(scroll) { this.lockAutoScroll(); scroll && this.dom.changeByFunc(this.scrollView, s => { s.setScroll(scroll.x, scroll.y); this.unlockAutoScroll(); }); this.scroll = scroll; } this.paddings = paddings; } isAutoScrollLocked() { return this.autoScrollLocker !== 0; } private lockAutoScroll() { this.autoScrollLocker++; } private unlockAutoScroll() { this.autoScrollLocker--; } private applyOffset(curTranslate: Point, curScroll: Point, oldCrop: Offsets, newCrop: Offsets, modelOffset: Offsets): Transition { const translate = curTranslate.clone(); const scroll = curScroll.clone(); const offset = this.getActualOffset(oldCrop, newCrop, modelOffset); if(offset.left) { translate.x = Math.max(CANVAS_MIN_PADDING, translate.x - offset.left); scroll.x += offset.left - (curTranslate.x - translate.x); } if(offset.top) { translate.y = Math.max(CANVAS_MIN_PADDING, translate.y - offset.top); scroll.y += offset.top - (curTranslate.y - translate.y); } return { translate, scroll }; } private cropHiddenHead(curTranslate: Point, curScroll: Point): Transition { const scroll = curScroll.clone(); const translate = curTranslate.clone(); if(scroll.x && translate.x > CANVAS_MIN_PADDING) { const delta = translate.x - Math.max(CANVAS_MIN_PADDING, translate.x - scroll.x); translate.x -= delta; scroll.x -= delta; } if(scroll.y && translate.y > CANVAS_MIN_PADDING) { const delta = translate.y - Math.max(CANVAS_MIN_PADDING, translate.y - scroll.y); translate.y -= delta; scroll.y -= delta; } return { translate, scroll }; } private getTailSpace(curTranslate: Point, curScroll: Point, newModelAbsSize: Size, containerSize: Size, scrollbarWidth: number): Size { const translate = curTranslate.clone(); const scroll = curScroll.clone(); let right = Math.max(containerSize.width + scroll.x - (translate.x + newModelAbsSize.width), CANVAS_MIN_PADDING); let bottom = Math.max(containerSize.height + scroll.y - (translate.y + newModelAbsSize.height), CANVAS_MIN_PADDING); const scrollbars = this.checkScrollBars(containerSize, scrollbarWidth, newModelAbsSize, new Offsets(translate.x, right, translate.y, bottom)); if(scrollbars.vertical) right = Math.max(CANVAS_MIN_PADDING, right - scrollbarWidth); if(scrollbars.horizontal) bottom = Math.max(CANVAS_MIN_PADDING, bottom - scrollbarWidth); return new Size(right, bottom); } private getActualOffset(oldCrop: Offsets, newCrop: Offsets, docOffset: Offsets): Offsets { return new Offsets( -(newCrop.left - oldCrop.left) + docOffset.left, -(newCrop.right - oldCrop.right) + docOffset.right, -(newCrop.top - oldCrop.top) + docOffset.top, -(newCrop.bottom - oldCrop.bottom) + docOffset.bottom ); } private checkScrollBars(containerSize: Size, scrollBarWidth: number, modelAbsSize: Size, paddings: Offsets): OrientationInfo { let hasHorizontalScroll = containerSize.width < modelAbsSize.width + paddings.horizontal; let hasVerticalScroll = containerSize.height < modelAbsSize.height + paddings.vertical; if(hasHorizontalScroll && !hasVerticalScroll) hasVerticalScroll = containerSize.height - scrollBarWidth < modelAbsSize.height + paddings.vertical; if(hasVerticalScroll && !hasHorizontalScroll) hasHorizontalScroll = containerSize.width - scrollBarWidth < modelAbsSize.width + paddings.horizontal; return { horizontal: hasHorizontalScroll, vertical: hasVerticalScroll }; } private lastWidth: number; private lastHeight: number; private setSvgSize(width: number, height: number) { if(width !== this.lastWidth || height !== this.lastHeight) { this.dom.changeByFunc(this.svgElement, e => RenderUtils.updateSvgElementSize(e, width, height)); this.lastWidth = width; this.lastHeight = height; } } private getVisibileAreaAbsShift(excludeScroll?: boolean): Point { const scroll = this.scroll; const paddings = this.paddings.clone(); const simpleView = this.simpleView; const cropLeft = simpleView && this.crop ? this.crop.left * this.actualZoom : 0; const cropTop = simpleView && this.crop ? this.crop.top * this.actualZoom : 0; return new Point( paddings.left - cropLeft - (excludeScroll ? 0 : scroll.x), paddings.top - cropTop - (excludeScroll ? 0 : scroll.y) ); } getModelPoint(absolutePoint: Point, checkScrollArea?: boolean): Point { const shift = this.getVisibileAreaAbsShift(); const modelPoint = absolutePoint .clone().offset(-shift.x, -shift.y) .multiply(1 / this.actualZoom, 1 / this.actualZoom); if(checkScrollArea) { const scrollSize = this.containerSize; if(absolutePoint.x < 0 || absolutePoint.y < 0 || absolutePoint.x > scrollSize.width || absolutePoint.y > scrollSize.height) return null; if(modelPoint.x < 0 || modelPoint.y < 0) return null; if(modelPoint.x > this.modelSize.width || modelPoint.y > this.modelSize.height) return null; } return modelPoint.clone().applyConverter(UnitConverter.pixelsToTwips); } getAbsolutePoint(modelPoint: Point, excludeScroll?: boolean, checkScrollArea?: boolean): Point { const shift = this.getVisibileAreaAbsShift(excludeScroll); const absPoint = modelPoint .clone().multiply(this.actualZoom, this.actualZoom) .clone().applyConverter(UnitConverter.twipsToPixelsF) .clone().offset(shift.x, shift.y); if(checkScrollArea) { if(absPoint.x < 0 || absPoint.y < 0) return null; const scrollSize = this.containerSize; if(absPoint.x > scrollSize.width || absPoint.y > scrollSize.height) return null; } return absPoint; } } export interface ICanvasViewListener { notifyViewAdjusted(canvasOffset: Point); notifyActualZoomChanged(actualZoom: number); }