/* * 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' // 自定义 import { ToolboxDataZoomFeatureModel } from '../toolbox/feature/DataZoom' /** * BrushController is not only used in "brush component", * but is also used in "tooltip DataZoom", and other possible * further brush behavior related scenarios. * So `BrushController` should not depend 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 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 let isContolDirection: any = null let startSelection: any export interface BrushControllerEvents { brush: { areas: { brushType: BrushType panelId: string range: BrushAreaRange }[] isEnd: boolean removeOnClick: boolean } } /** * params: * areas: 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 /** * @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 /** * 自定义参数,控制横纵坐标 */ _isLineXY: Boolean = false // x/y 轴转换的最小距离 _distance: Number = 10 private _uid: string private _handlers: { [eventName: string]: (this: BrushController, e: ElementEvent) => void } = {} constructor(zr: ZRenderType, featureModel?: ToolboxDataZoomFeatureModel) { super() if (__DEV__) { assert(zr) } this._zr = zr this.group = new graphic.Group() this._uid = 'brushController_' + baseUID++ const { option } = featureModel || {} this._isLineXY = option?.isLineXY || false this._distance = option?.distance || 10 this._distance = this._distance <= 10 ? 10 : this._distance each( pointerHandlers, function (this: BrushController, handler, eventName) { this._handlers[eventName] = bind(handler, this) }, this, ) } /** * If set to `false`, select disabled. */ enableBrush( brushOption: Partial | false, ): BrushController { if (__DEV__) { assert(this._mounted) } this._brushType && this._doDisableBrush() ;(brushOption as Partial).brushType && this._doEnableBrush(brushOption as Partial) return this } private _doEnableBrush(brushOption: Partial): 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) 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 forbidden. 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 { // cover.__brushOption.brushType 不能为rect 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 const prarms = { 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 }), } 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 if (x !== undefined || y !== undefined) { 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 let eventParams controller._track.push(localCursorPoint.slice()) const dx = Math.abs(localCursorPoint[0] - startSelection[0]) const dy = Math.abs(localCursorPoint[1] - startSelection[1]) const isLineXY = controller._isLineXY || false const distance = controller._distance if (isLineXY) { if (isContolDirection === null && controller._track.length >= 2) { if (dy > dx) { controller._brushType = 'lineY' controller._brushOption.brushType = 'lineY' isContolDirection = 'y' } else { controller._brushType = 'lineX' controller._brushOption.brushType = 'lineX' isContolDirection = 'x' } } } const thisBrushOption = controller._brushOption if (shouldShowCover(controller) || creatingCover) { if (panel && !creatingCover) { thisBrushOption.brushMode === 'single' && clearCovers(controller) const brushOption = clone(thisBrushOption) as BrushCoverConfig // console.log('2===',brushOption.brushType) 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) } // 执行封装 const next = () => { 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 } } if ( creatingCover && isLineXY && creatingCover.__brushOption.brushType !== 'rect' ) { if (isContolDirection === 'x') { if (Math.abs(dx) <= distance) { controller._brushType = 'lineY' controller._brushOption.brushType = 'lineY' creatingCover.__brushOption.brushType = 'lineY' isContolDirection = 'y' } } else if (isContolDirection === 'y') { if (Math.abs(dy) <= distance) { controller._brushType = 'lineX' controller._brushOption.brushType = 'lineX' creatingCover.__brushOption.brushType = 'lineX' isContolDirection = 'x' } } next() return eventParams } next() } 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, )) startSelection = 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) startSelection = null isContolDirection = null }, } 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 should 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 = { 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