import { Able, TransformData, params, payload, Entity, EntityData } from '../../common'; import { Rectangle, RectangleAlignType } from '@gedit/math'; import type { EntityManager } from '../../common'; import type { SelectionService } from '@gedit/application-common'; export const SelectPayload = Symbol('SelectPayload'); export interface SelectPayload { selectBounds?: Rectangle // 选中区域 alignBounds?: Rectangle // 画布外围矩形,用于计算选中单个元素的画布对齐 selectionService?: SelectionService, zIndex?: number // 选中对应的zIndex元素 align?: SelectAlignType // 选中的所有元素进行对齐 fromMouse?: boolean additional?: boolean // 是否要追加 hovered?: boolean // 是否为hover } export type SelectControlKey = 'rotate' | 'origin' | 'left' | 'bottom' | 'right' | 'top' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom'; export type SelectDisplayType = 'rectangle' | 'circle' | string; export const SelectAllControlKeys: SelectControlKey[] = ['rotate', 'origin', 'left', 'bottom', 'right', 'top', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']; interface SelectStateData { selected: boolean, // 是否被选中 hovered: boolean, // 是否处在在hover状态 mouseSelect: boolean, // 是否可通过鼠标选择,默认true, 有些场景不需要在画布直接选中节点,则设置为false zIndex: number displayType: SelectDisplayType // 显示的类型, 默认为矩形边框, 也可以自定义 controlKeys: SelectControlKey[] originBtnDisabled?: boolean } export type SelectAlignType = RectangleAlignType; let zIndexId = 0; /** * 选中状态 */ export class SelectState extends EntityData { static type = 'SelectState'; getDefaultData(): SelectStateData { return { selected: false, hovered: false, mouseSelect: true, zIndex: zIndexId++, displayType: 'rectangle', controlKeys: SelectAllControlKeys, }; } get controlKeys(): SelectControlKey[] { return this.data.controlKeys; } get displayType(): SelectDisplayType { return this.data.displayType; } get originBtnDisabled(): boolean | undefined { return this.data.originBtnDisabled; } get selected(): boolean { return this.data.selected; } get hovered(): boolean { return this.data.hovered; } set hovered(hovered) { this.update({ hovered }); } get zIndex(): number { return this.data.zIndex; } get mouseSelect(): boolean { return this.data.mouseSelect; } set mouseSelect(mouseSelect) { this.data.mouseSelect = mouseSelect; } set selected(selected) { this.update({ selected }); } set zIndex(zIndex) { this.update({ zIndex }); } } function getSelectedEntities(entities: Entity[]): Entity[] { return entities.filter(entity => entity.getData(SelectState)!.selected); } function getBound(entity: Entity): Rectangle { return entity.getData(TransformData)!.bounds; } /** * 对齐 */ function alignEntities(entities: Entity[], type: SelectAlignType, pageBounds?: Rectangle): void { const selectedEntities = getSelectedEntities(entities); const selectedBounds: Rectangle[] = selectedEntities.map(e => getBound(e).clone()); // 对齐画布 if (selectedEntities.length === 1 && pageBounds) { selectedBounds.push(pageBounds); } const alignBounds = Rectangle.align(selectedBounds, type); const d = { x: 0, y: 0 }; const sceneChangedBounds = selectedEntities.length === 1 && pageBounds ? alignBounds.pop() || d : d; alignBounds.forEach((bounds, i) => { const entity = selectedEntities[i]; if (!entity) return; const transform = entity.getData(TransformData)!; const originBounds = transform.bounds; const delta = { x: bounds.x - originBounds.x - sceneChangedBounds.x, y: bounds.y - originBounds.y - sceneChangedBounds.y }; transform.update({ position: { x: transform.position.x + delta.x, y: transform.position.y + delta.y } }); }); } function checkSelectionChanged(selectedEntities: Entity[], selection: object | undefined): boolean { if (selection && Array.isArray(selection)) { if (selectedEntities.length !== selection.length) return true; return !!selectedEntities.find(s => !selection.includes(s)); } return true; } export function syncToSelection(selectedEntities: Entity[], selectionService: SelectionService): void { const oldSelection = selectionService.selection; if (checkSelectionChanged(selectedEntities, oldSelection)) { selectionService.selection = selectedEntities; } } /** * 可被选中并对其的组件 */ export class Selectable extends Able { static type = 'Selectable'; @payload(SelectPayload) payload: SelectPayload; /** * 处理全局数据 * @param entities * @param selectPayload */ static globalBefore(entities: Entity[], selectPayload: SelectPayload): SelectPayload | undefined { /** * 处理对齐 */ if (selectPayload.align) { alignEntities(entities, selectPayload.align, selectPayload.alignBounds); return undefined; } return selectPayload; } // static globalAfter(entitites: Entity[], selectPayload: SelectPayload): void { // const service = selectPayload.selectionService; // if (service) { // const oldSelection = service.selection; // const selectedEntities = getSelectedEntities(entitites); // if (checkSelectionChanged(selectedEntities, oldSelection)) { // service.selection = selectedEntities; // } // } // } @params(TransformData, SelectState) handle(transform: TransformData, selectState: SelectState, selectPayload: SelectPayload): void { if (selectPayload.fromMouse && !selectState.mouseSelect) return; // 根据深度判断 if (selectPayload.zIndex !== undefined) { // 追加模式 if (selectPayload.additional) { if (selectState.zIndex === selectPayload.zIndex) { selectState.update({ selected: true }); } } else { selectState.update({ selected: selectState.zIndex === selectPayload.zIndex }); } } else if (selectPayload.selectBounds) { const selected = transform.intersects(selectPayload.selectBounds); selectState.update({ selected }); } } } export namespace Selectable { export function getSelectedBounds(entityManager: EntityManager): Rectangle | undefined { const selectableEntities = entityManager.getEntitiesByAble(Selectable); const bounds = getSelectedEntities(selectableEntities).map(e => getBound(e)); if (bounds.length === 0) return undefined; if (bounds.length === 1) return bounds[0]; return Rectangle.enlarge(bounds); } export function getZIndex(entity: Entity): number | undefined { const state = entity.getData(SelectState); if (!state) return undefined; return state.zIndex; } }