| /* | |
| * 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<{ | |
| [key in keyof BrushControllerEvents]: (params: BrushControllerEvents[key]) => void | undefined | |
| }> { | |
| 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; |