UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

192 lines (191 loc) 6.06 kB
import { computed, signal } from "@preact/signals-core"; import cloneDeep from "lodash/cloneDeep"; import { ESelectionStrategy } from "../../services/selection/types"; import { AnchorState } from "../anchor/Anchor"; export const IS_BLOCK_TYPE = "Block"; export class BlockState { static fromTBlock(store, block) { return new BlockState(store, block, store.blockSelectionBucket); } /** * Block id */ get id() { return this.$state.value.id; } /** * Block x position */ get x() { return this.$state.value.x; } /** * Block y position */ get y() { return this.$state.value.y; } /** * Block width */ get width() { return this.$state.value.width; } /** * Block height */ get height() { return this.$state.value.height; } /** * Block selected */ get selected() { return this.$selected.value; } constructor(store, block, blockSelectionBucket) { this.store = store; this.blockSelectionBucket = blockSelectionBucket; this.$rawState = signal(undefined); /** * Block state signal * * @returns {ReadonlySignal<T>} Block state */ this.$state = computed(() => ({ ...this.$rawState.value, selected: this.$selected.value, })); /** * Computed signal that reactively determines if this block is selected * by checking if its ID exists in the selection bucket */ this.$selected = computed(() => this.blockSelectionBucket.isSelected(this.$rawState.value.id)); this.$anchorStates = signal([]); /** * Block geometry signal * * Pay attention!! x and y are rounded to integer * * @returns {ReadonlySignal<{x: number, y: number, width: number, height: number}>} Block geometry */ this.$geometry = computed(() => { const state = this.$state.value; return { x: state.x | 0, y: state.y | 0, width: state.width, height: state.height, }; }); /** * Block anchor indexes signal * * @returns {ReadonlySignal<Map<string, number>>} Block anchor indexes */ this.$anchorIndexs = computed(() => { const typeIndex = {}; return new Map(this.$anchorStates.value ?.sort((a, b) => (a.state.index || 0) - (b.state.index || 0)) .map((anchorState) => { if (!typeIndex[anchorState.state.type]) { typeIndex[anchorState.state.type] = 0; } return [anchorState.id, typeIndex[anchorState.state.type]++]; }) || []); }); /** * Block anchors signal * * @returns {ReadonlySignal<TAnchor[]>} Block anchors */ this.$anchors = computed(() => { return this.$anchorStates.value?.map((anchorState) => anchorState.asTAnchor()) || []; }); /** * Block selected anchors signal * * @returns {TAnchor[]} Block selected anchors */ this.$selectedAnchors = computed(() => { return (this.$anchorStates.value?.filter((anchorState) => this.store.anchorSelectionBucket.$selected.value.has(anchorState.id)) || []); }); this.$rawState.value = block; this.$anchorStates.value = block.anchors?.map((anchor) => new AnchorState(this, anchor)) ?? []; } onAnchorSelected(anchorId, selected) { this.store.setAnchorSelection(this.id, anchorId, selected); } setSelection(selected, strategy = ESelectionStrategy.REPLACE) { this.store.updateBlocksSelection([this.id], selected, strategy); } getSelectedAnchor() { return this.$selectedAnchors.value[0]; } getAnchorState(id) { return this.$anchorStates.value.find((state) => state.id === id); } updateXY(x, y, forceUpdate = false) { this.store.updatePosition(this.id, { x, y }); if (forceUpdate) { this.blockView.updatePosition(x, y, true); } } setViewComponent(blockComponent) { this.blockView = blockComponent; } getViewComponent() { return this.blockView; } getConnections() { return this.store.getBlockConnections(this.id); } clearAnchorsSelection() { this.store.anchorSelectionBucket.reset(); } setName(newName) { this.$rawState.value = { ...this.$rawState.value, name: newName, }; } updateAnchors(anchors) { const anchorsMap = new Map(this.$anchorStates.value.map((a) => [a.id, a])); this.$anchorStates.value = anchors.map((anchor) => { if (anchorsMap.has(anchor.id)) { const anchorState = anchorsMap.get(anchor.id); anchorState.update(anchor); return anchorState; } return new AnchorState(this, anchor); }); } /** * Updates block state * * @param block {Partial<TBlock>} Block to update * @returns void */ updateBlock(block) { // Update anchors first to ensure they have correct state when geometry changes if (block.anchors) { this.updateAnchors(block.anchors); } this.$rawState.value = Object.assign({}, this.$rawState.value, block); this.getViewComponent()?.updateHitBox(this.$geometry.value, true); } getAnchorById(anchorId) { return this.$anchorStates.value.find((anchor) => anchor.id === anchorId); } /** * Converts the block state to a TBlock * * @returns {TBlock} TBlock */ asTBlock() { return cloneDeep({ ...this.$rawState.toJSON(), selected: this.$selected.value, }); } }