devexpress-diagram
Version:
DevExpress Diagram Control
596 lines (553 loc) • 32.5 kB
text/typescript
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);
}