echarts BrushController 源码

  • 2022-10-20
  • 浏览 (520)

echarts BrushController 代码

文件路径:/src/component/helper/BrushController.ts

/*
* 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;

相关信息

echarts 源码目录

相关文章

echarts BrushTargetManager 源码

echarts MapDraw 源码

echarts RoamController 源码

echarts brushHelper 源码

echarts cursorHelper 源码

echarts interactionMutex 源码

echarts listComponent 源码

echarts roamHelper 源码

echarts sliderMove 源码

0  赞