/*
|
* Licensed to the Apache Software Foundation (ASF) under one
|
* or more contributor license agreements. See the NOTICE file
|
* distributed with this work for additional information
|
* regarding copyright ownership. The ASF licenses this file
|
* to you under the Apache License, Version 2.0 (the
|
* "License"); you may not use this file except in compliance
|
* with the License. You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing,
|
* software distributed under the License is distributed on an
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* KIND, either express or implied. See the License for the
|
* specific language governing permissions and limitations
|
* under the License.
|
*/
|
|
|
import {curry, each, map, bind, merge, clone, defaults, assert} from 'zrender/src/core/util';
|
import Eventful from 'zrender/src/core/Eventful';
|
import * as graphic from '../../util/graphic';
|
import * as interactionMutex from './interactionMutex';
|
import DataDiffer from '../../data/DataDiffer';
|
import { Dictionary } from '../../util/types';
|
import { ZRenderType } from 'zrender/src/zrender';
|
import { ElementEvent } from 'zrender/src/Element';
|
import * as matrix from 'zrender/src/core/matrix';
|
import Displayable from 'zrender/src/graphic/Displayable';
|
import { PathStyleProps } from 'zrender/src/graphic/Path';
|
|
|
/**
|
* BrushController not only used in "brush component",
|
* but also used in "tooltip DataZoom", and other possible
|
* futher brush behavior related scenarios.
|
* So `BrushController` should not depends on "brush component model".
|
*/
|
|
|
export type BrushType = 'polygon' | 'rect' | 'lineX' | 'lineY';
|
/**
|
* Only for drawing (after enabledBrush).
|
* 'line', 'rect', 'polygon' or false
|
* If passing false/null/undefined, disable brush.
|
* If passing 'auto', determined by panel.defaultBrushType
|
*/
|
export type BrushTypeUncertain = BrushType | false | 'auto';
|
|
export type BrushMode = 'single' | 'multiple';
|
// MinMax: Range of linear brush.
|
// MinMax[]: Range of multi-dimension like rect/polygon, which is a MinMax
|
// list for each dimension of the coord sys. For example:
|
// cartesian: [[xMin, xMax], [yMin, yMax]]
|
// geo: [[lngMin, lngMin], [latMin, latMax]]
|
export type BrushDimensionMinMax = number[];
|
export type BrushAreaRange = BrushDimensionMinMax | BrushDimensionMinMax[];
|
|
export interface BrushCoverConfig {
|
// Mandatory. determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y')
|
brushType: BrushType;
|
// Can be specified by user to map covers in `updateCovers`
|
// in `dispatchAction({type: 'brush', areas: [{id: ...}, ...]})`
|
id?: string;
|
// Range in global coordinate (pixel).
|
range?: BrushAreaRange;
|
// When create a new area by `updateCovers`, panelId should be specified.
|
// If not null/undefined, means global panel.
|
// Also see `BrushAreaParam['panelId']`.
|
panelId?: string;
|
|
brushMode?: BrushMode;
|
// `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default.
|
brushStyle?: Pick<PathStyleProps, BrushStyleKey>;
|
transformable?: boolean;
|
removeOnClick?: boolean;
|
z?: number;
|
}
|
|
/**
|
* `BrushAreaCreatorOption` input to brushModel via `setBrushOption`,
|
* merge and convert to `BrushCoverCreatorConfig`.
|
*/
|
export interface BrushCoverCreatorConfig extends Pick<
|
BrushCoverConfig,
|
'brushMode' | 'transformable' | 'removeOnClick' | 'brushStyle' | 'z'
|
> {
|
brushType: BrushTypeUncertain;
|
}
|
|
type BrushStyleKey =
|
'fill'
|
| 'stroke'
|
| 'lineWidth'
|
| 'opacity'
|
| 'shadowBlur'
|
| 'shadowOffsetX'
|
| 'shadowOffsetY'
|
| 'shadowColor';
|
|
|
const BRUSH_PANEL_GLOBAL = true as const;
|
|
export interface BrushPanelConfig {
|
// mandatory.
|
panelId: string;
|
// mandatory.
|
clipPath(localPoints: number[][], transform: matrix.MatrixArray): number[][];
|
// mandatory.
|
isTargetByCursor(e: ElementEvent, localCursorPoint: number[], transform: matrix.MatrixArray): boolean;
|
// optional, only used when brushType is 'auto'.
|
defaultBrushType?: BrushType;
|
// optional.
|
getLinearBrushOtherExtent?(xyIndex: number): number[];
|
}
|
// `true` represents global panel;
|
type BrushPanelConfigOrGlobal = BrushPanelConfig | typeof BRUSH_PANEL_GLOBAL;
|
|
|
interface BrushCover extends graphic.Group {
|
__brushOption: BrushCoverConfig;
|
}
|
|
type Point = number[];
|
|
const mathMin = Math.min;
|
const mathMax = Math.max;
|
const mathPow = Math.pow;
|
|
const COVER_Z = 10000;
|
const UNSELECT_THRESHOLD = 6;
|
const MIN_RESIZE_LINE_WIDTH = 6;
|
const MUTEX_RESOURCE_KEY = 'globalPan';
|
|
type DirectionName = 'w' | 'e' | 'n' | 's';
|
type DirectionNameSequence = DirectionName[];
|
|
const DIRECTION_MAP = {
|
w: [0, 0],
|
e: [0, 1],
|
n: [1, 0],
|
s: [1, 1]
|
} as const;
|
const CURSOR_MAP = {
|
w: 'ew',
|
e: 'ew',
|
n: 'ns',
|
s: 'ns',
|
ne: 'nesw',
|
sw: 'nesw',
|
nw: 'nwse',
|
se: 'nwse'
|
} as const;
|
const DEFAULT_BRUSH_OPT = {
|
brushStyle: {
|
lineWidth: 2,
|
stroke: 'rgba(210,219,238,0.3)',
|
fill: '#D2DBEE'
|
},
|
transformable: true,
|
brushMode: 'single',
|
removeOnClick: false
|
};
|
|
let baseUID = 0;
|
|
export interface BrushControllerEvents {
|
brush: {
|
areas: {
|
brushType: BrushType;
|
panelId: string;
|
range: BrushAreaRange;
|
}[];
|
isEnd: boolean;
|
removeOnClick: boolean;
|
}
|
}
|
|
/**
|
* params:
|
* areas: Array.<Array>, coord relates to container group,
|
* If no container specified, to global.
|
* opt {
|
* isEnd: boolean,
|
* removeOnClick: boolean
|
* }
|
*/
|
class BrushController extends Eventful<BrushControllerEvents> {
|
|
readonly group: graphic.Group;
|
|
/**
|
* @internal
|
*/
|
_zr: ZRenderType;
|
|
/**
|
* @internal
|
*/
|
_brushType: BrushTypeUncertain;
|
|
/**
|
* @internal
|
* Only for drawing (after enabledBrush).
|
*/
|
_brushOption: BrushCoverCreatorConfig;
|
|
/**
|
* @internal
|
* Key: panelId
|
*/
|
_panels: Dictionary<BrushPanelConfig>;
|
|
/**
|
* @internal
|
*/
|
_track: number[][] = [];
|
|
/**
|
* @internal
|
*/
|
_dragging: boolean;
|
|
/**
|
* @internal
|
*/
|
_covers: BrushCover[] = [];
|
|
/**
|
* @internal
|
*/
|
_creatingCover: BrushCover;
|
|
/**
|
* @internal
|
*/
|
_creatingPanel: BrushPanelConfigOrGlobal;
|
|
private _enableGlobalPan: boolean;
|
|
private _mounted: boolean;
|
|
/**
|
* @internal
|
*/
|
_transform: matrix.MatrixArray;
|
|
private _uid: string;
|
|
private _handlers: {
|
[eventName: string]: (this: BrushController, e: ElementEvent) => void
|
} = {};
|
|
|
constructor(zr: ZRenderType) {
|
super();
|
|
if (__DEV__) {
|
assert(zr);
|
}
|
|
this._zr = zr;
|
|
this.group = new graphic.Group();
|
|
this._uid = 'brushController_' + baseUID++;
|
|
each(pointerHandlers, function (this: BrushController, handler, eventName) {
|
this._handlers[eventName] = bind(handler, this);
|
}, this);
|
}
|
|
/**
|
* If set to `false`, select disabled.
|
*/
|
enableBrush(brushOption: Partial<BrushCoverCreatorConfig> | false): BrushController {
|
if (__DEV__) {
|
assert(this._mounted);
|
}
|
|
this._brushType && this._doDisableBrush();
|
(brushOption as Partial<BrushCoverCreatorConfig>).brushType && this._doEnableBrush(
|
brushOption as Partial<BrushCoverCreatorConfig>
|
);
|
|
return this;
|
}
|
|
private _doEnableBrush(brushOption: Partial<BrushCoverCreatorConfig>): void {
|
const zr = this._zr;
|
|
// Consider roam, which takes globalPan too.
|
if (!this._enableGlobalPan) {
|
interactionMutex.take(zr, MUTEX_RESOURCE_KEY, this._uid);
|
}
|
|
each(this._handlers, function (handler, eventName) {
|
zr.on(eventName, handler);
|
});
|
|
this._brushType = brushOption.brushType;
|
this._brushOption = merge(
|
clone(DEFAULT_BRUSH_OPT), brushOption, true
|
) as BrushCoverCreatorConfig;
|
}
|
|
private _doDisableBrush(): void {
|
const zr = this._zr;
|
|
interactionMutex.release(zr, MUTEX_RESOURCE_KEY, this._uid);
|
|
each(this._handlers, function (handler, eventName) {
|
zr.off(eventName, handler);
|
});
|
|
this._brushType = this._brushOption = null;
|
}
|
|
/**
|
* @param panelOpts If not pass, it is global brush.
|
*/
|
setPanels(panelOpts?: BrushPanelConfig[]): BrushController {
|
if (panelOpts && panelOpts.length) {
|
const panels = this._panels = {} as Dictionary<BrushPanelConfig>;
|
each(panelOpts, function (panelOpts) {
|
panels[panelOpts.panelId] = clone(panelOpts);
|
});
|
}
|
else {
|
this._panels = null;
|
}
|
return this;
|
}
|
|
mount(opt?: {
|
enableGlobalPan?: boolean;
|
x?: number;
|
y?: number;
|
rotation?: number;
|
scaleX?: number;
|
scaleY?: number
|
}): BrushController {
|
opt = opt || {};
|
|
if (__DEV__) {
|
this._mounted = true; // should be at first.
|
}
|
|
this._enableGlobalPan = opt.enableGlobalPan;
|
|
const thisGroup = this.group;
|
this._zr.add(thisGroup);
|
|
thisGroup.attr({
|
x: opt.x || 0,
|
y: opt.y || 0,
|
rotation: opt.rotation || 0,
|
scaleX: opt.scaleX || 1,
|
scaleY: opt.scaleY || 1
|
});
|
this._transform = thisGroup.getLocalTransform();
|
|
return this;
|
}
|
|
// eachCover(cb, context): void {
|
// each(this._covers, cb, context);
|
// }
|
|
/**
|
* Update covers.
|
* @param coverConfigList
|
* If coverConfigList is null/undefined, all covers removed.
|
*/
|
updateCovers(coverConfigList: BrushCoverConfig[]) {
|
if (__DEV__) {
|
assert(this._mounted);
|
}
|
|
coverConfigList = map(coverConfigList, function (coverConfig) {
|
return merge(clone(DEFAULT_BRUSH_OPT), coverConfig, true);
|
}) as BrushCoverConfig[];
|
|
const tmpIdPrefix = '\0-brush-index-';
|
const oldCovers = this._covers;
|
const newCovers = this._covers = [] as BrushCover[];
|
const controller = this;
|
const creatingCover = this._creatingCover;
|
|
(new DataDiffer(oldCovers, coverConfigList, oldGetKey, getKey))
|
.add(addOrUpdate)
|
.update(addOrUpdate)
|
.remove(remove)
|
.execute();
|
|
return this;
|
|
function getKey(brushOption: BrushCoverConfig, index: number): string {
|
return (brushOption.id != null ? brushOption.id : tmpIdPrefix + index)
|
+ '-' + brushOption.brushType;
|
}
|
|
function oldGetKey(cover: BrushCover, index: number): string {
|
return getKey(cover.__brushOption, index);
|
}
|
|
function addOrUpdate(newIndex: number, oldIndex?: number): void {
|
const newBrushInternal = coverConfigList[newIndex];
|
// Consider setOption in event listener of brushSelect,
|
// where updating cover when creating should be forbiden.
|
if (oldIndex != null && oldCovers[oldIndex] === creatingCover) {
|
newCovers[newIndex] = oldCovers[oldIndex];
|
}
|
else {
|
const cover = newCovers[newIndex] = oldIndex != null
|
? (
|
oldCovers[oldIndex].__brushOption = newBrushInternal,
|
oldCovers[oldIndex]
|
)
|
: endCreating(controller, createCover(controller, newBrushInternal));
|
updateCoverAfterCreation(controller, cover);
|
}
|
}
|
|
function remove(oldIndex: number) {
|
if (oldCovers[oldIndex] !== creatingCover) {
|
controller.group.remove(oldCovers[oldIndex]);
|
}
|
}
|
}
|
|
unmount() {
|
if (__DEV__) {
|
if (!this._mounted) {
|
return;
|
}
|
}
|
|
this.enableBrush(false);
|
|
// container may 'removeAll' outside.
|
clearCovers(this);
|
this._zr.remove(this.group);
|
|
if (__DEV__) {
|
this._mounted = false; // should be at last.
|
}
|
|
return this;
|
}
|
|
dispose() {
|
this.unmount();
|
this.off();
|
}
|
}
|
|
|
function createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover {
|
const cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption);
|
cover.__brushOption = brushOption;
|
updateZ(cover, brushOption);
|
controller.group.add(cover);
|
return cover;
|
}
|
|
function endCreating(controller: BrushController, creatingCover: BrushCover): BrushCover {
|
const coverRenderer = getCoverRenderer(creatingCover);
|
if (coverRenderer.endCreating) {
|
coverRenderer.endCreating(controller, creatingCover);
|
updateZ(creatingCover, creatingCover.__brushOption);
|
}
|
return creatingCover;
|
}
|
|
function updateCoverShape(controller: BrushController, cover: BrushCover): void {
|
const brushOption = cover.__brushOption;
|
getCoverRenderer(cover).updateCoverShape(
|
controller, cover, brushOption.range, brushOption
|
);
|
}
|
|
function updateZ(cover: BrushCover, brushOption: BrushCoverConfig): void {
|
let z = brushOption.z;
|
z == null && (z = COVER_Z);
|
cover.traverse(function (el: Displayable) {
|
el.z = z;
|
el.z2 = z; // Consider in given container.
|
});
|
}
|
|
function updateCoverAfterCreation(controller: BrushController, cover: BrushCover): void {
|
getCoverRenderer(cover).updateCommon(controller, cover);
|
updateCoverShape(controller, cover);
|
}
|
|
function getCoverRenderer(cover: BrushCover): CoverRenderer {
|
return coverRenderers[cover.__brushOption.brushType];
|
}
|
|
// return target panel or `true` (means global panel)
|
function getPanelByPoint(
|
controller: BrushController,
|
e: ElementEvent,
|
localCursorPoint: Point
|
): BrushPanelConfigOrGlobal {
|
const panels = controller._panels;
|
if (!panels) {
|
return BRUSH_PANEL_GLOBAL; // Global panel
|
}
|
let panel;
|
const transform = controller._transform;
|
each(panels, function (pn) {
|
pn.isTargetByCursor(e, localCursorPoint, transform) && (panel = pn);
|
});
|
return panel;
|
}
|
|
// Return a panel or true
|
function getPanelByCover(controller: BrushController, cover: BrushCover): BrushPanelConfigOrGlobal {
|
const panels = controller._panels;
|
if (!panels) {
|
return BRUSH_PANEL_GLOBAL; // Global panel
|
}
|
const panelId = cover.__brushOption.panelId;
|
// User may give cover without coord sys info,
|
// which is then treated as global panel.
|
return panelId != null ? panels[panelId] : BRUSH_PANEL_GLOBAL;
|
}
|
|
function clearCovers(controller: BrushController): boolean {
|
const covers = controller._covers;
|
const originalLength = covers.length;
|
each(covers, function (cover) {
|
controller.group.remove(cover);
|
}, controller);
|
covers.length = 0;
|
|
return !!originalLength;
|
}
|
|
function trigger(
|
controller: BrushController,
|
opt: {isEnd?: boolean, removeOnClick?: boolean}
|
): void {
|
const areas = map(controller._covers, function (cover) {
|
const brushOption = cover.__brushOption;
|
const range = clone(brushOption.range);
|
return {
|
brushType: brushOption.brushType,
|
panelId: brushOption.panelId,
|
range: range
|
};
|
});
|
|
controller.trigger('brush', {
|
areas: areas,
|
isEnd: !!opt.isEnd,
|
removeOnClick: !!opt.removeOnClick
|
});
|
}
|
|
function shouldShowCover(controller: BrushController): boolean {
|
const track = controller._track;
|
|
if (!track.length) {
|
return false;
|
}
|
|
const p2 = track[track.length - 1];
|
const p1 = track[0];
|
const dx = p2[0] - p1[0];
|
const dy = p2[1] - p1[1];
|
const dist = mathPow(dx * dx + dy * dy, 0.5);
|
|
return dist > UNSELECT_THRESHOLD;
|
}
|
|
function getTrackEnds(track: Point[]): Point[] {
|
let tail = track.length - 1;
|
tail < 0 && (tail = 0);
|
return [track[0], track[tail]];
|
}
|
|
interface RectRangeConverter {
|
toRectRange: (range: BrushAreaRange) => BrushDimensionMinMax[];
|
fromRectRange: (areaRange: BrushDimensionMinMax[]) => BrushAreaRange;
|
};
|
function createBaseRectCover(
|
rectRangeConverter: RectRangeConverter,
|
controller: BrushController,
|
brushOption: BrushCoverConfig,
|
edgeNameSequences: DirectionNameSequence[]
|
): BrushCover {
|
const cover = new graphic.Group() as BrushCover;
|
|
cover.add(new graphic.Rect({
|
name: 'main',
|
style: makeStyle(brushOption),
|
silent: true,
|
draggable: true,
|
cursor: 'move',
|
drift: curry(driftRect, rectRangeConverter, controller, cover, ['n', 's', 'w', 'e']),
|
ondragend: curry(trigger, controller, {isEnd: true})
|
}));
|
|
each(
|
edgeNameSequences,
|
function (nameSequence) {
|
cover.add(new graphic.Rect({
|
name: nameSequence.join(''),
|
style: {opacity: 0},
|
draggable: true,
|
silent: true,
|
invisible: true,
|
drift: curry(driftRect, rectRangeConverter, controller, cover, nameSequence),
|
ondragend: curry(trigger, controller, {isEnd: true})
|
}));
|
}
|
);
|
|
return cover;
|
}
|
|
function updateBaseRect(
|
controller: BrushController,
|
cover: BrushCover,
|
localRange: BrushDimensionMinMax[],
|
brushOption: BrushCoverConfig
|
): void {
|
const lineWidth = brushOption.brushStyle.lineWidth || 0;
|
const handleSize = mathMax(lineWidth, MIN_RESIZE_LINE_WIDTH);
|
const x = localRange[0][0];
|
const y = localRange[1][0];
|
const xa = x - lineWidth / 2;
|
const ya = y - lineWidth / 2;
|
const x2 = localRange[0][1];
|
const y2 = localRange[1][1];
|
const x2a = x2 - handleSize + lineWidth / 2;
|
const y2a = y2 - handleSize + lineWidth / 2;
|
const width = x2 - x;
|
const height = y2 - y;
|
const widtha = width + lineWidth;
|
const heighta = height + lineWidth;
|
|
updateRectShape(controller, cover, 'main', x, y, width, height);
|
|
if (brushOption.transformable) {
|
updateRectShape(controller, cover, 'w', xa, ya, handleSize, heighta);
|
updateRectShape(controller, cover, 'e', x2a, ya, handleSize, heighta);
|
updateRectShape(controller, cover, 'n', xa, ya, widtha, handleSize);
|
updateRectShape(controller, cover, 's', xa, y2a, widtha, handleSize);
|
|
updateRectShape(controller, cover, 'nw', xa, ya, handleSize, handleSize);
|
updateRectShape(controller, cover, 'ne', x2a, ya, handleSize, handleSize);
|
updateRectShape(controller, cover, 'sw', xa, y2a, handleSize, handleSize);
|
updateRectShape(controller, cover, 'se', x2a, y2a, handleSize, handleSize);
|
}
|
}
|
|
function updateCommon(controller: BrushController, cover: BrushCover): void {
|
const brushOption = cover.__brushOption;
|
const transformable = brushOption.transformable;
|
|
const mainEl = cover.childAt(0) as Displayable;
|
mainEl.useStyle(makeStyle(brushOption));
|
mainEl.attr({
|
silent: !transformable,
|
cursor: transformable ? 'move' : 'default'
|
});
|
|
each(
|
[['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']],
|
function (nameSequence: DirectionNameSequence) {
|
const el = cover.childOfName(nameSequence.join('')) as Displayable;
|
const globalDir = nameSequence.length === 1
|
? getGlobalDirection1(controller, nameSequence[0])
|
: getGlobalDirection2(controller, nameSequence);
|
|
el && el.attr({
|
silent: !transformable,
|
invisible: !transformable,
|
cursor: transformable ? CURSOR_MAP[globalDir] + '-resize' : null
|
});
|
}
|
);
|
}
|
|
function updateRectShape(
|
controller: BrushController,
|
cover: BrushCover,
|
name: string,
|
x: number, y: number, w: number, h: number
|
): void {
|
const el = cover.childOfName(name) as graphic.Rect;
|
el && el.setShape(pointsToRect(
|
clipByPanel(controller, cover, [[x, y], [x + w, y + h]])
|
));
|
}
|
|
function makeStyle(brushOption: BrushCoverConfig) {
|
return defaults({strokeNoScale: true}, brushOption.brushStyle);
|
}
|
|
function formatRectRange(x: number, y: number, x2: number, y2: number): BrushDimensionMinMax[] {
|
const min = [mathMin(x, x2), mathMin(y, y2)];
|
const max = [mathMax(x, x2), mathMax(y, y2)];
|
|
return [
|
[min[0], max[0]], // x range
|
[min[1], max[1]] // y range
|
];
|
}
|
|
function getTransform(controller: BrushController): matrix.MatrixArray {
|
return graphic.getTransform(controller.group);
|
}
|
|
function getGlobalDirection1(
|
controller: BrushController, localDirName: DirectionName
|
): keyof typeof CURSOR_MAP {
|
const map = {w: 'left', e: 'right', n: 'top', s: 'bottom'} as const;
|
const inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'} as const;
|
const dir = graphic.transformDirection(
|
map[localDirName], getTransform(controller)
|
);
|
return inverseMap[dir];
|
}
|
function getGlobalDirection2(
|
controller: BrushController, localDirNameSeq: DirectionNameSequence
|
): keyof typeof CURSOR_MAP {
|
const globalDir = [
|
getGlobalDirection1(controller, localDirNameSeq[0]),
|
getGlobalDirection1(controller, localDirNameSeq[1])
|
];
|
(globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse();
|
return globalDir.join('') as keyof typeof CURSOR_MAP;
|
}
|
|
function driftRect(
|
rectRangeConverter: RectRangeConverter,
|
controller: BrushController,
|
cover: BrushCover,
|
dirNameSequence: DirectionNameSequence,
|
dx: number,
|
dy: number
|
): void {
|
const brushOption = cover.__brushOption;
|
const rectRange = rectRangeConverter.toRectRange(brushOption.range);
|
const localDelta = toLocalDelta(controller, dx, dy);
|
|
each(dirNameSequence, function (dirName) {
|
const ind = DIRECTION_MAP[dirName];
|
rectRange[ind[0]][ind[1]] += localDelta[ind[0]];
|
});
|
|
brushOption.range = rectRangeConverter.fromRectRange(formatRectRange(
|
rectRange[0][0], rectRange[1][0], rectRange[0][1], rectRange[1][1]
|
));
|
|
updateCoverAfterCreation(controller, cover);
|
trigger(controller, {isEnd: false});
|
}
|
|
function driftPolygon(
|
controller: BrushController,
|
cover: BrushCover,
|
dx: number,
|
dy: number
|
): void {
|
const range = cover.__brushOption.range as BrushDimensionMinMax[];
|
const localDelta = toLocalDelta(controller, dx, dy);
|
|
each(range, function (point) {
|
point[0] += localDelta[0];
|
point[1] += localDelta[1];
|
});
|
|
updateCoverAfterCreation(controller, cover);
|
trigger(controller, {isEnd: false});
|
}
|
|
function toLocalDelta(
|
controller: BrushController, dx: number, dy: number
|
): BrushDimensionMinMax {
|
const thisGroup = controller.group;
|
const localD = thisGroup.transformCoordToLocal(dx, dy);
|
const localZero = thisGroup.transformCoordToLocal(0, 0);
|
|
return [localD[0] - localZero[0], localD[1] - localZero[1]];
|
}
|
|
function clipByPanel(controller: BrushController, cover: BrushCover, data: Point[]): Point[] {
|
const panel = getPanelByCover(controller, cover);
|
|
return (panel && panel !== BRUSH_PANEL_GLOBAL)
|
? panel.clipPath(data, controller._transform)
|
: clone(data);
|
}
|
|
function pointsToRect(points: Point[]): graphic.Rect['shape'] {
|
const xmin = mathMin(points[0][0], points[1][0]);
|
const ymin = mathMin(points[0][1], points[1][1]);
|
const xmax = mathMax(points[0][0], points[1][0]);
|
const ymax = mathMax(points[0][1], points[1][1]);
|
|
return {
|
x: xmin,
|
y: ymin,
|
width: xmax - xmin,
|
height: ymax - ymin
|
};
|
}
|
|
function resetCursor(
|
controller: BrushController, e: ElementEvent, localCursorPoint: Point
|
): void {
|
if (
|
// Check active
|
!controller._brushType
|
// resetCursor should be always called when mouse is in zr area,
|
// but not called when mouse is out of zr area to avoid bad influence
|
// if `mousemove`, `mouseup` are triggered from `document` event.
|
|| isOutsideZrArea(controller, e.offsetX, e.offsetY)
|
) {
|
return;
|
}
|
|
const zr = controller._zr;
|
const covers = controller._covers;
|
const currPanel = getPanelByPoint(controller, e, localCursorPoint);
|
|
// Check whether in covers.
|
if (!controller._dragging) {
|
for (let i = 0; i < covers.length; i++) {
|
const brushOption = covers[i].__brushOption;
|
if (currPanel
|
&& (currPanel === BRUSH_PANEL_GLOBAL || brushOption.panelId === currPanel.panelId)
|
&& coverRenderers[brushOption.brushType].contain(
|
covers[i], localCursorPoint[0], localCursorPoint[1]
|
)
|
) {
|
// Use cursor style set on cover.
|
return;
|
}
|
}
|
}
|
|
currPanel && zr.setCursorStyle('crosshair');
|
}
|
|
function preventDefault(e: ElementEvent): void {
|
const rawE = e.event;
|
rawE.preventDefault && rawE.preventDefault();
|
}
|
|
function mainShapeContain(cover: BrushCover, x: number, y: number): boolean {
|
return (cover.childOfName('main') as Displayable).contain(x, y);
|
}
|
|
function updateCoverByMouse(
|
controller: BrushController,
|
e: ElementEvent,
|
localCursorPoint: Point,
|
isEnd: boolean
|
): {
|
isEnd: boolean,
|
removeOnClick?: boolean
|
} {
|
let creatingCover = controller._creatingCover;
|
const panel = controller._creatingPanel;
|
const thisBrushOption = controller._brushOption;
|
let eventParams;
|
|
controller._track.push(localCursorPoint.slice());
|
|
if (shouldShowCover(controller) || creatingCover) {
|
|
if (panel && !creatingCover) {
|
thisBrushOption.brushMode === 'single' && clearCovers(controller);
|
const brushOption = clone(thisBrushOption) as BrushCoverConfig;
|
brushOption.brushType = determineBrushType(brushOption.brushType, panel as BrushPanelConfig);
|
brushOption.panelId = panel === BRUSH_PANEL_GLOBAL ? null : panel.panelId;
|
creatingCover = controller._creatingCover = createCover(controller, brushOption);
|
controller._covers.push(creatingCover);
|
}
|
|
if (creatingCover) {
|
const coverRenderer = coverRenderers[
|
determineBrushType(controller._brushType, panel as BrushPanelConfig)
|
];
|
const coverBrushOption = creatingCover.__brushOption;
|
|
coverBrushOption.range = coverRenderer.getCreatingRange(
|
clipByPanel(controller, creatingCover, controller._track)
|
);
|
|
if (isEnd) {
|
endCreating(controller, creatingCover);
|
coverRenderer.updateCommon(controller, creatingCover);
|
}
|
|
updateCoverShape(controller, creatingCover);
|
|
eventParams = {isEnd: isEnd};
|
}
|
}
|
else if (
|
isEnd
|
&& thisBrushOption.brushMode === 'single'
|
&& thisBrushOption.removeOnClick
|
) {
|
// Help user to remove covers easily, only by a tiny drag, in 'single' mode.
|
// But a single click do not clear covers, because user may have casual
|
// clicks (for example, click on other component and do not expect covers
|
// disappear).
|
// Only some cover removed, trigger action, but not every click trigger action.
|
if (getPanelByPoint(controller, e, localCursorPoint) && clearCovers(controller)) {
|
eventParams = {isEnd: isEnd, removeOnClick: true};
|
}
|
}
|
|
return eventParams;
|
}
|
|
function determineBrushType(brushType: BrushTypeUncertain, panel: BrushPanelConfig): BrushType {
|
if (brushType === 'auto') {
|
if (__DEV__) {
|
assert(
|
panel && panel.defaultBrushType,
|
'MUST have defaultBrushType when brushType is "atuo"'
|
);
|
}
|
return panel.defaultBrushType;
|
}
|
return brushType as BrushType;
|
}
|
|
const pointerHandlers: Dictionary<(this: BrushController, e: ElementEvent) => void> = {
|
|
mousedown: function (e) {
|
if (this._dragging) {
|
// In case some browser do not support globalOut,
|
// and release mouse out side the browser.
|
handleDragEnd(this, e);
|
}
|
else if (!e.target || !e.target.draggable) {
|
|
preventDefault(e);
|
|
const localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY);
|
|
this._creatingCover = null;
|
const panel = this._creatingPanel = getPanelByPoint(this, e, localCursorPoint);
|
|
if (panel) {
|
this._dragging = true;
|
this._track = [localCursorPoint.slice()];
|
}
|
}
|
},
|
|
mousemove: function (e) {
|
const x = e.offsetX;
|
const y = e.offsetY;
|
|
const localCursorPoint = this.group.transformCoordToLocal(x, y);
|
|
resetCursor(this, e, localCursorPoint);
|
|
if (this._dragging) {
|
preventDefault(e);
|
const eventParams = updateCoverByMouse(this, e, localCursorPoint, false);
|
eventParams && trigger(this, eventParams);
|
}
|
},
|
|
mouseup: function (e) {
|
handleDragEnd(this, e);
|
}
|
};
|
|
|
function handleDragEnd(controller: BrushController, e: ElementEvent) {
|
if (controller._dragging) {
|
preventDefault(e);
|
|
const x = e.offsetX;
|
const y = e.offsetY;
|
|
const localCursorPoint = controller.group.transformCoordToLocal(x, y);
|
const eventParams = updateCoverByMouse(controller, e, localCursorPoint, true);
|
|
controller._dragging = false;
|
controller._track = [];
|
controller._creatingCover = null;
|
|
// trigger event shoule be at final, after procedure will be nested.
|
eventParams && trigger(controller, eventParams);
|
}
|
}
|
|
function isOutsideZrArea(controller: BrushController, x: number, y: number): boolean {
|
const zr = controller._zr;
|
return x < 0 || x > zr.getWidth() || y < 0 || y > zr.getHeight();
|
}
|
|
|
interface CoverRenderer {
|
createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover;
|
getCreatingRange(localTrack: Point[]): BrushAreaRange;
|
updateCoverShape(
|
controller: BrushController, cover: BrushCover, localRange: BrushAreaRange, brushOption: BrushCoverConfig
|
): void;
|
updateCommon(controller: BrushController, cover: BrushCover): void;
|
contain(cover: BrushCover, x: number, y: number): boolean;
|
endCreating?(controller: BrushController, creatingCover: BrushCover): void;
|
}
|
|
/**
|
* key: brushType
|
*/
|
const coverRenderers: Record<BrushType, CoverRenderer> = {
|
|
lineX: getLineRenderer(0),
|
|
lineY: getLineRenderer(1),
|
|
rect: {
|
createCover: function (controller, brushOption) {
|
function returnInput(range: BrushDimensionMinMax[]): BrushDimensionMinMax[] {
|
return range;
|
}
|
return createBaseRectCover(
|
{
|
toRectRange: returnInput,
|
fromRectRange: returnInput
|
},
|
controller,
|
brushOption,
|
[['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']]
|
);
|
},
|
getCreatingRange: function (localTrack) {
|
const ends = getTrackEnds(localTrack);
|
return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]);
|
},
|
updateCoverShape: function (controller, cover, localRange: BrushDimensionMinMax[], brushOption) {
|
updateBaseRect(controller, cover, localRange, brushOption);
|
},
|
updateCommon: updateCommon,
|
contain: mainShapeContain
|
},
|
|
polygon: {
|
createCover: function (controller, brushOption) {
|
const cover = new graphic.Group();
|
|
// Do not use graphic.Polygon because graphic.Polyline do not close the
|
// border of the shape when drawing, which is a better experience for user.
|
cover.add(new graphic.Polyline({
|
name: 'main',
|
style: makeStyle(brushOption),
|
silent: true
|
}));
|
|
return cover as BrushCover;
|
},
|
getCreatingRange: function (localTrack) {
|
return localTrack;
|
},
|
endCreating: function (controller, cover) {
|
cover.remove(cover.childAt(0));
|
// Use graphic.Polygon close the shape.
|
cover.add(new graphic.Polygon({
|
name: 'main',
|
draggable: true,
|
drift: curry(driftPolygon, controller, cover),
|
ondragend: curry(trigger, controller, {isEnd: true})
|
}));
|
},
|
updateCoverShape: function (controller, cover, localRange: BrushDimensionMinMax[], brushOption) {
|
(cover.childAt(0) as graphic.Polygon).setShape({
|
points: clipByPanel(controller, cover, localRange)
|
});
|
},
|
updateCommon: updateCommon,
|
contain: mainShapeContain
|
}
|
};
|
|
function getLineRenderer(xyIndex: 0 | 1) {
|
return {
|
createCover: function (controller: BrushController, brushOption: BrushCoverConfig): BrushCover {
|
return createBaseRectCover(
|
{
|
toRectRange: function (range: BrushDimensionMinMax): BrushDimensionMinMax[] {
|
const rectRange = [range, [0, 100]];
|
xyIndex && rectRange.reverse();
|
return rectRange;
|
},
|
fromRectRange: function (rectRange: BrushDimensionMinMax[]): BrushDimensionMinMax {
|
return rectRange[xyIndex];
|
}
|
},
|
controller,
|
brushOption,
|
([[['w'], ['e']], [['n'], ['s']]] as DirectionNameSequence[][])[xyIndex]
|
);
|
},
|
getCreatingRange: function (localTrack: Point[]): BrushDimensionMinMax {
|
const ends = getTrackEnds(localTrack);
|
const min = mathMin(ends[0][xyIndex], ends[1][xyIndex]);
|
const max = mathMax(ends[0][xyIndex], ends[1][xyIndex]);
|
|
return [min, max];
|
},
|
updateCoverShape: function (
|
controller: BrushController,
|
cover: BrushCover,
|
localRange: BrushDimensionMinMax,
|
brushOption: BrushCoverConfig
|
): void {
|
let otherExtent;
|
// If brushWidth not specified, fit the panel.
|
const panel = getPanelByCover(controller, cover);
|
if (panel !== BRUSH_PANEL_GLOBAL && panel.getLinearBrushOtherExtent) {
|
otherExtent = panel.getLinearBrushOtherExtent(xyIndex);
|
}
|
else {
|
const zr = controller._zr;
|
otherExtent = [0, [zr.getWidth(), zr.getHeight()][1 - xyIndex]];
|
}
|
const rectRange = [localRange, otherExtent];
|
xyIndex && rectRange.reverse();
|
|
updateBaseRect(controller, cover, rectRange, brushOption);
|
},
|
updateCommon: updateCommon,
|
contain: mainShapeContain
|
};
|
}
|
|
export default BrushController;
|