From e4393b5e8eb07de1c5df9b068d8ea29f4c306a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Wed, 8 Oct 2025 10:27:44 +0200 Subject: [PATCH 1/7] chore: initial commit From 6ef125edb68be34236d3004f96eb3b39a72f40e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 10:42:20 +0200 Subject: [PATCH 2/7] chore: wip --- code/packages/sdk/src/index.ts | 2 + code/packages/sdk/src/managers/cloning.ts | 44 +- .../packages/sdk/src/nodes/frame/constants.ts | 15 +- code/packages/sdk/src/nodes/frame/frame.ts | 39 +- code/packages/sdk/src/nodes/frame/types.ts | 5 +- code/packages/sdk/src/nodes/node copy.ts | 854 ++++++++++++++++++ .../nodes-distance-snapping.ts | 1 + .../nodes-edge-snapping.ts | 1 + .../nodes-selection/nodes-selection.ts | 14 +- .../plugins/stage-panning copy/constants.ts | 10 + .../stage-panning copy/stage-panning.ts | 464 ++++++++++ .../src/plugins/stage-panning copy/types.ts | 12 + .../src/plugins/stage-panning/constants.ts | 5 + .../plugins/stage-panning/stage-panning.ts | 141 ++- .../sdk/src/plugins/stage-panning/types.ts | 12 + code/packages/sdk/src/weave.ts | 4 + 16 files changed, 1578 insertions(+), 45 deletions(-) create mode 100644 code/packages/sdk/src/nodes/node copy.ts create mode 100644 code/packages/sdk/src/plugins/stage-panning copy/constants.ts create mode 100644 code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts create mode 100644 code/packages/sdk/src/plugins/stage-panning copy/types.ts create mode 100644 code/packages/sdk/src/plugins/stage-panning/types.ts diff --git a/code/packages/sdk/src/index.ts b/code/packages/sdk/src/index.ts index 0a7039092..dd486d100 100644 --- a/code/packages/sdk/src/index.ts +++ b/code/packages/sdk/src/index.ts @@ -123,6 +123,8 @@ export { WeaveStageGridPlugin } from './plugins/stage-grid/stage-grid'; export * from './plugins/stage-grid/constants'; export * from './plugins/stage-grid/types'; export { WeaveStagePanningPlugin } from './plugins/stage-panning/stage-panning'; +export * from './plugins/stage-panning/constants'; +export * from './plugins/stage-panning/types'; export { WeaveStageMinimapPlugin } from './plugins/stage-minimap/stage-minimap'; export { WeaveStageResizePlugin } from './plugins/stage-resize/stage-resize'; export { WeaveStageZoomPlugin } from './plugins/stage-zoom/stage-zoom'; diff --git a/code/packages/sdk/src/managers/cloning.ts b/code/packages/sdk/src/managers/cloning.ts index 57773a588..97e15b17e 100644 --- a/code/packages/sdk/src/managers/cloning.ts +++ b/code/packages/sdk/src/managers/cloning.ts @@ -8,7 +8,10 @@ import { v4 as uuidv4 } from 'uuid'; import { Weave } from '@/weave'; import { type Vector2d } from 'konva/lib/types'; import { type Logger } from 'pino'; -import { type WeaveStateElement } from '@inditextech/weave-types'; +import { + type WeaveElementInstance, + type WeaveStateElement, +} from '@inditextech/weave-types'; import type { WeaveNode } from '@/nodes/node'; export class WeaveCloningManager { @@ -272,4 +275,43 @@ export class WeaveCloningManager { newGroup.destroy(); } + + private recursivelyUpdateKeys(nodes: WeaveStateElement[]) { + for (const child of nodes) { + const newNodeId = uuidv4(); + child.key = newNodeId; + child.props.id = newNodeId; + if (child.props.children) { + this.recursivelyUpdateKeys(child.props.children); + } + } + } + + cloneNode(targetNode: Konva.Node): Konva.Node | undefined { + const nodeHandler = this.instance.getNodeHandler( + targetNode.getAttrs().nodeType + ); + + if (!nodeHandler) { + return undefined; + } + + const parent: Konva.Container = targetNode.getParent() as Konva.Container; + + const serializedNode = nodeHandler.serialize( + targetNode as WeaveElementInstance + ); + + this.recursivelyUpdateKeys(serializedNode.props.children ?? []); + + const newNodeId = uuidv4(); + serializedNode.key = newNodeId; + serializedNode.props.id = newNodeId; + + const realParent = this.instance.getInstanceRecursive(parent); + + this.instance.addNode(serializedNode, realParent?.getAttrs().id); + + return this.instance.getStage().findOne(`#${newNodeId}`); + } } diff --git a/code/packages/sdk/src/nodes/frame/constants.ts b/code/packages/sdk/src/nodes/frame/constants.ts index b13347b9e..b15d502af 100644 --- a/code/packages/sdk/src/nodes/frame/constants.ts +++ b/code/packages/sdk/src/nodes/frame/constants.ts @@ -8,17 +8,13 @@ export const WEAVE_FRAME_NODE_DEFAULT_CONFIG = { fontFamily: 'Arial', fontStyle: 'bold', fontSize: 20, - fontColor: '#000000ff', + fontColor: '#000000', titleMargin: 20, - borderColor: '#000000ff', + borderColor: '#000000', borderWidth: 1, - onTargetLeave: { - borderColor: '#000000ff', - fill: '#ffffffff', - }, onTargetEnter: { - borderColor: '#ff6863ff', - fill: '#ffffffff', + borderColor: '#FF6863FF', + fill: '#FFFFFFFF', }, transform: { rotateEnabled: false, @@ -29,8 +25,11 @@ export const WEAVE_FRAME_NODE_DEFAULT_CONFIG = { }, }; +export const WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR = '#FFFFFFFF'; + export const WEAVE_FRAME_NODE_DEFAULT_PROPS = { title: 'Frame XXX', frameWidth: 1920, frameHeight: 1080, + frameBackground: WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, }; diff --git a/code/packages/sdk/src/nodes/frame/frame.ts b/code/packages/sdk/src/nodes/frame/frame.ts index 5bca682af..fa1455d7d 100644 --- a/code/packages/sdk/src/nodes/frame/frame.ts +++ b/code/packages/sdk/src/nodes/frame/frame.ts @@ -12,6 +12,7 @@ import { WEAVE_NODE_CUSTOM_EVENTS, } from '@inditextech/weave-types'; import { + WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, WEAVE_FRAME_NODE_DEFAULT_CONFIG, WEAVE_FRAME_NODE_DEFAULT_PROPS, WEAVE_FRAME_NODE_TYPE, @@ -48,6 +49,9 @@ export class WeaveFrameNode extends WeaveNode { ...(props.title && { title: props.title }), ...(props.frameWidth && { frameWidth: props.frameWidth }), ...(props.frameHeight && { frameHeight: props.frameHeight }), + ...(props.frameBackground && { + frameBackground: props.frameBackground, + }), children: [], }, }; @@ -73,10 +77,6 @@ export class WeaveFrameNode extends WeaveNode { borderColor: onTargetEnterBorderColor, fill: onTargetEnterFill, }, - onTargetLeave: { - borderColor: onTargetLeaveBorderColor, - fill: onTargetLeaveFill, - }, } = this.config; const frameParams = { @@ -119,13 +119,14 @@ export class WeaveFrameNode extends WeaveNode { nodeId: id, x: 0, y: 0, + onTargetEnter: false, width: props.frameWidth, stroke: borderColor, strokeWidth: borderWidth, strokeScaleEnabled: true, shadowForStrokeEnabled: false, height: props.frameHeight, - fill: frameParams.frameBackground ?? '#ffffffff', + fill: frameParams.frameBackground ?? WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, listening: false, draggable: false, }); @@ -138,7 +139,7 @@ export class WeaveFrameNode extends WeaveNode { id: `${id}-title`, x: 0, width: props.frameWidth, - fontSize: fontSize / stage.scaleX(), + fontSize: fontSize, fontFamily, fontStyle, verticalAlign: 'middle', @@ -208,6 +209,7 @@ export class WeaveFrameNode extends WeaveNode { ctx.rect(0, -textHeight, props.frameWidth, textHeight); ctx.fillStrokeShape(shape); }, + strokeWidth: 0, fill: 'transparent', id: `${id}-selection-area`, listening: true, @@ -226,6 +228,7 @@ export class WeaveFrameNode extends WeaveNode { ctx.rect(0, 0, props.frameWidth, props.frameHeight); ctx.fillStrokeShape(shape); }, + strokeWidth: 0, fill: 'transparent', nodeId: id, id: `${id}-container-area`, @@ -264,15 +267,6 @@ export class WeaveFrameNode extends WeaveNode { this.setupDefaultNodeEvents(frame); this.instance.addEventListener('onZoomChange', () => { - const stage = this.instance.getStage(); - text.fontSize(fontSize / stage.scaleX()); - text.width(props.frameWidth); - const textMeasures = text.measureSize(text.getAttrs().text ?? ''); - const textHeight = - textMeasures.height + (2 * titleMargin) / stage.scaleX(); - text.y(-textHeight); - text.height(textHeight); - selectionArea.hitFunc(function (ctx, shape) { ctx.beginPath(); ctx.rect(0, -textHeight, props.frameWidth, textHeight); @@ -284,14 +278,17 @@ export class WeaveFrameNode extends WeaveNode { frame.on(WEAVE_NODE_CUSTOM_EVENTS.onTargetLeave, () => { background.setAttrs({ - stroke: onTargetLeaveBorderColor, + onTargetEnter: false, + stroke: borderColor, strokeWidth: borderWidth, - fill: onTargetLeaveFill, + fill: + frameParams.frameBackground ?? WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, }); }); frame.on(WEAVE_NODE_CUSTOM_EVENTS.onTargetEnter, () => { background.setAttrs({ + onTargetEnter: true, stroke: onTargetEnterBorderColor, strokeWidth: borderWidth, fill: onTargetEnterFill, @@ -331,9 +328,9 @@ export class WeaveFrameNode extends WeaveNode { `#${newProps.id}-selection-area` ); - if (background) { + if (background && !newProps.onTargetEnter) { background.setAttrs({ - fill: newProps.frameBackground ?? '#ffffffff', + fill: newProps.frameBackground ?? WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, }); } @@ -341,8 +338,7 @@ export class WeaveFrameNode extends WeaveNode { title.text(newProps.title); const textMeasures = title.measureSize(title.getAttrs().text ?? ''); - const textHeight = - textMeasures.height + (2 * titleMargin) / stage.scaleX(); + const textHeight = textMeasures.height + 2 * titleMargin; title.y(-textHeight); title.height(textHeight); @@ -391,6 +387,7 @@ export class WeaveFrameNode extends WeaveNode { const cleanedAttrs = { ...realAttrs }; delete cleanedAttrs.draggable; + delete cleanedAttrs.onTargetEnter; return { key: realAttrs?.id ?? '', diff --git a/code/packages/sdk/src/nodes/frame/types.ts b/code/packages/sdk/src/nodes/frame/types.ts index e6b1500b1..c3b089fee 100644 --- a/code/packages/sdk/src/nodes/frame/types.ts +++ b/code/packages/sdk/src/nodes/frame/types.ts @@ -15,10 +15,6 @@ export type WeaveFrameProperties = { titleMargin: number; borderWidth: number; borderColor: string; - onTargetLeave: { - borderColor: string; - fill: string; - }; onTargetEnter: { borderColor: string; fill: string; @@ -30,6 +26,7 @@ export type WeaveFrameAttributes = WeaveElementAttributes & { title: string; frameWidth: number; frameHeight: number; + frameBackground: string; }; export type WeaveFrameNodeParams = { diff --git a/code/packages/sdk/src/nodes/node copy.ts b/code/packages/sdk/src/nodes/node copy.ts new file mode 100644 index 000000000..662626e00 --- /dev/null +++ b/code/packages/sdk/src/nodes/node copy.ts @@ -0,0 +1,854 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { Weave } from '@/weave'; +import merge from 'lodash/merge'; +import { + type WeaveElementAttributes, + type WeaveElementInstance, + type WeaveStateElement, + type WeaveNodeBase, + WEAVE_NODE_CUSTOM_EVENTS, + type WeaveNodeConfiguration, +} from '@inditextech/weave-types'; +import { type Logger } from 'pino'; +import { WeaveNodesSelectionPlugin } from '@/plugins/nodes-selection/nodes-selection'; +import Konva from 'konva'; +import { WeaveCopyPasteNodesPlugin } from '@/plugins/copy-paste-nodes/copy-paste-nodes'; +import type { WeaveNodesSelectionPluginOnNodesChangeEvent } from '@/plugins/nodes-selection/types'; +import { + clearContainerTargets, + containerOverCursor, + hasFrames, + moveNodeToContainer, +} from '@/utils'; +import type { WeaveNodesEdgeSnappingPlugin } from '@/plugins/nodes-edge-snapping/nodes-edge-snapping'; +import { throttle } from 'lodash'; +import type { KonvaEventObject } from 'konva/lib/Node'; +import { WEAVE_STAGE_DEFAULT_MODE } from './stage/constants'; +import { MOVE_TOOL_ACTION_NAME } from '@/actions/move-tool/constants'; +import { SELECTION_TOOL_ACTION_NAME } from '@/actions/selection-tool/constants'; +import { WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY } from '@/plugins/nodes-edge-snapping/constants'; +import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '@/plugins/nodes-distance-snapping/constants'; +import type { WeaveNodesDistanceSnappingPlugin } from '@/plugins/nodes-distance-snapping/nodes-distance-snapping'; +import type { Vector2d } from 'konva/lib/types'; +import type { WeaveStageGridPlugin } from '@/plugins/stage-grid/stage-grid'; +import type { WeaveStagePanningPlugin } from '@/plugins/stage-panning/stage-panning'; + +let clones: Konva.Node[] = []; + +export const augmentKonvaStageClass = (): void => { + Konva.Stage.prototype.isMouseWheelPressed = function () { + return false; + }; +}; + +export const augmentKonvaNodeClass = ( + config?: WeaveNodeConfiguration +): void => { + const { transform } = config ?? {}; + + Konva.Node.prototype.getTransformerProperties = function () { + return { + ...transform, + }; + }; + Konva.Node.prototype.getExportClientRect = function (config) { + return this.getClientRect(config); + }; + Konva.Node.prototype.getRealClientRect = function (config) { + return this.getClientRect(config); + }; + Konva.Node.prototype.movedToContainer = function () {}; + Konva.Node.prototype.updatePosition = function () {}; + Konva.Node.prototype.triggerCrop = function () {}; + Konva.Node.prototype.closeCrop = function () {}; + Konva.Node.prototype.resetCrop = function () {}; + Konva.Node.prototype.dblClick = function () {}; +}; + +export abstract class WeaveNode implements WeaveNodeBase { + protected instance!: Weave; + protected nodeType!: string; + protected didMove!: boolean; + private logger!: Logger; + protected previousPointer!: string | null; + + register(instance: Weave): WeaveNode { + this.instance = instance; + this.logger = this.instance.getChildLogger(this.getNodeType()); + this.instance + .getChildLogger('node') + .debug(`Node with type [${this.getNodeType()}] registered`); + + return this; + } + + getNodeType(): string { + return this.nodeType; + } + + getLogger(): Logger { + return this.logger; + } + + getSelectionPlugin(): WeaveNodesSelectionPlugin | undefined { + const selectionPlugin = + this.instance.getPlugin('nodesSelection'); + + return selectionPlugin; + } + + isSelecting(): boolean { + return this.instance.getActiveAction() === SELECTION_TOOL_ACTION_NAME; + } + + isPasting(): boolean { + const copyPastePlugin = + this.instance.getPlugin('copyPasteNodes'); + + if (copyPastePlugin) { + return copyPastePlugin.isPasting(); + } + + return false; + } + + setupDefaultNodeAugmentation(node: Konva.Node): void { + const defaultTransformerProperties = this.defaultGetTransformerProperties(); + + node.getTransformerProperties = function () { + return defaultTransformerProperties; + }; + node.allowedAnchors = function () { + return [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', + ]; + }; + node.movedToContainer = function () {}; + node.updatePosition = function () {}; + node.resetCrop = function () {}; + node.handleMouseover = function () {}; + node.handleMouseout = function () {}; + } + + isNodeSelected(ele: Konva.Node): boolean { + const selectionPlugin = + this.instance.getPlugin('nodesSelection'); + + if ( + selectionPlugin + ?.getSelectedNodes() + .map((node) => node.getAttrs().id) + .includes(ele.getAttrs().id) + ) { + return true; + } + + return false; + } + + scaleReset(node: Konva.Node): void { + const scale = node.scale(); + + node.width(Math.max(5, node.width() * scale.x)); + node.height(Math.max(5, node.height() * scale.y)); + + // reset scale to 1 + node.scale({ x: 1, y: 1 }); + } + + protected setHoverState(node: Konva.Node): void { + const selectionPlugin = this.getSelectionPlugin(); + + if (!selectionPlugin) { + return; + } + + if (selectionPlugin.isAreaSelecting()) { + this.hideHoverState(); + return; + } + + selectionPlugin.getHoverTransformer().nodes([node]); + } + + protected hideHoverState(): void { + const selectionPlugin = this.getSelectionPlugin(); + + if (!selectionPlugin) { + return; + } + + selectionPlugin.getHoverTransformer().nodes([]); + } + + private getRealSelectedNode(nodeTarget: Konva.Node) { + const stage = this.instance.getStage(); + + let realNodeTarget: Konva.Node = nodeTarget; + + if (nodeTarget.getParent() instanceof Konva.Transformer) { + const mousePos = stage.getPointerPosition(); + const intersections = stage.getAllIntersections(mousePos); + const nodesIntersected = intersections.filter( + (ele) => ele.getAttrs().nodeType + ); + + if (nodesIntersected.length > 0) { + realNodeTarget = this.instance.getInstanceRecursive( + nodesIntersected[nodesIntersected.length - 1] + ); + } + } + + return realNodeTarget; + } + + setupDefaultNodeEvents(node: Konva.Node): void { + this.instance.addEventListener( + 'onNodesChange', + () => { + if ( + !this.isLocked(node as WeaveElementInstance) && + this.isSelecting() && + this.isNodeSelected(node) + ) { + node.draggable(true); + return; + } + node.draggable(false); + } + ); + + const isLocked = node.getAttrs().locked ?? false; + + if (isLocked) { + node.off('transformstart'); + node.off('transform'); + node.off('transformend'); + node.off('dragstart'); + node.off('dragmove'); + node.off('dragend'); + node.off('pointerenter'); + node.off('pointerleave'); + } else { + let transforming = false; + + node.on('transformstart', (e) => { + transforming = true; + + this.instance.emitEvent('onTransform', e.target); + }); + + const handleTransform = (e: KonvaEventObject) => { + const node = e.target; + + const nodesSelectionPlugin = + this.instance.getPlugin('nodesSelection'); + + const nodesEdgeSnappingPlugin = this.getNodesEdgeSnappingPlugin(); + + if ( + nodesSelectionPlugin && + this.isSelecting() && + this.isNodeSelected(node) + ) { + nodesSelectionPlugin.getTransformer().forceUpdate(); + } + + if ( + nodesEdgeSnappingPlugin && + transforming && + this.isSelecting() && + this.isNodeSelected(node) + ) { + nodesEdgeSnappingPlugin.evaluateGuidelines(e); + } + }; + + node.on('transform', throttle(handleTransform, 100)); + + node.on('transformend', (e) => { + const node = e.target; + + this.instance.emitEvent('onTransform', null); + + transforming = false; + + const nodesSelectionPlugin = + this.instance.getPlugin('nodesSelection'); + + const nodesSnappingPlugin = this.getNodesEdgeSnappingPlugin(); + + if (nodesSnappingPlugin) { + nodesSnappingPlugin.cleanupGuidelines(); + } + + if (nodesSelectionPlugin) { + nodesSelectionPlugin.getTransformer().forceUpdate(); + } + + this.scaleReset(node); + + const nodeHandler = this.instance.getNodeHandler( + node.getAttrs().nodeType + ); + if (nodeHandler) { + this.instance.updateNode( + nodeHandler.serialize(node as WeaveElementInstance) + ); + } + }); + + const stage = this.instance.getStage(); + // const cloned: Record = {}; + let clone: Konva.Node | undefined = undefined; + + node.on('dragstart', (e) => { + const nodeTarget = e.target; + + this.didMove = false; + + if (e.evt?.buttons === 0) { + nodeTarget.stopDrag(); + return; + } + + const isErasing = this.instance.getActiveAction() === 'eraseTool'; + + if (isErasing) { + nodeTarget.stopDrag(); + return; + } + + this.instance.emitEvent('onDrag', nodeTarget); + + if (stage.isMouseWheelPressed()) { + e.cancelBubble = true; + nodeTarget.stopDrag(); + } + + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + + if (realNodeTarget.getAttrs().isCloned) { + return; + } + + if (e.evt?.altKey) { + nodeTarget.stopDrag(e.evt); + + e.cancelBubble = true; + + clone = this.instance.getCloningManager().cloneNode(realNodeTarget); + + if (clone && !clones.find((c) => c === clone)) { + clone.setAttrs({ isCloned: true }); + clones.push(clone); + } + + stage.setPointersPositions(e.evt); + + const nodesSelectionPlugin = this.getNodesSelectionPlugin(); + nodesSelectionPlugin?.setSelectedNodes([]); + + setTimeout(() => { + nodesSelectionPlugin?.setSelectedNodes(clones); + clone?.startDrag(e.evt); + }, 0); + } + }); + + const handleDragMove = (e: KonvaEventObject) => { + const nodeTarget = e.target; + + e.cancelBubble = true; + + if (e.evt?.buttons === 0) { + nodeTarget.stopDrag(); + return; + } + + this.didMove = true; + + const stage = this.instance.getStage(); + + const isErasing = this.instance.getActiveAction() === 'eraseTool'; + + if (isErasing) { + nodeTarget.stopDrag(); + return; + } + + if (stage.isMouseWheelPressed()) { + e.cancelBubble = true; + nodeTarget.stopDrag(); + return; + } + + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + + if (this.isSelecting() && !realNodeTarget.getAttrs().isCloned) { + clearContainerTargets(this.instance); + + const layerToMove = containerOverCursor(this.instance, [ + realNodeTarget, + ]); + + if ( + layerToMove && + !hasFrames(realNodeTarget) && + realNodeTarget.isDragging() + ) { + layerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetEnter, { + bubbles: true, + }); + } + } + }; + + node.on('dragmove', throttle(handleDragMove, 100)); + + node.on('dragend', (e) => { + const nodeTarget = e.target; + + if (clone) { + clone = undefined; + } + + if (!this.didMove) { + return; + } + + const isErasing = this.instance.getActiveAction() === 'eraseTool'; + + if (isErasing) { + nodeTarget.stopDrag(); + return; + } + + this.instance.emitEvent('onDrag', null); + + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + + if (this.isSelecting()) { + clearContainerTargets(this.instance); + + const nodesEdgeSnappingPlugin = this.getNodesEdgeSnappingPlugin(); + + const nodesDistanceSnappingPlugin = + this.getNodesDistanceSnappingPlugin(); + + if (nodesEdgeSnappingPlugin) { + nodesEdgeSnappingPlugin.cleanupGuidelines(); + } + + if (nodesDistanceSnappingPlugin) { + nodesDistanceSnappingPlugin.cleanupGuidelines(); + } + + const layerToMove = containerOverCursor(this.instance, [ + realNodeTarget, + ]); + + let containerToMove: Konva.Layer | Konva.Node | undefined = + this.instance.getMainLayer(); + + if (layerToMove) { + containerToMove = layerToMove; + } + + let moved = false; + if (realNodeTarget.getAttrs().isCloned) { + const parent: Konva.Container = + realNodeTarget.getParent() as Konva.Container; + const realParent = this.instance.getInstanceRecursive(parent); + containerToMove = realParent; + } + + if ( + containerToMove && + !hasFrames(realNodeTarget) && + !realNodeTarget.getAttrs().isCloned + ) { + moved = moveNodeToContainer( + this.instance, + realNodeTarget, + containerToMove + ); + } + + if (realNodeTarget.getAttrs().isCloned) { + clones = clones.filter((c) => c !== realNodeTarget); + } + + realNodeTarget?.setAttrs({ isCloned: undefined }); + + if (containerToMove) { + containerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetLeave, { + bubbles: true, + }); + } + + if (!moved) { + this.instance.updateNode( + this.serialize(realNodeTarget as WeaveElementInstance) + ); + } + } + }); + + node.handleMouseover = () => { + const stage = this.instance.getStage(); + const activeAction = this.instance.getActiveAction(); + + const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled(); + + const realNode = this.instance.getInstanceRecursive(node); + + const isTargetable = realNode.getAttrs().isTargetable !== false; + const isLocked = realNode.getAttrs().locked ?? false; + + if ([MOVE_TOOL_ACTION_NAME].includes(activeAction ?? '')) { + return; + } + + // Node is locked + if ( + isNodeSelectionEnabled && + this.isSelecting() && + !this.isNodeSelected(realNode) && + !this.isPasting() && + isLocked + ) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + } + + // Node is not locked and not selected + if ( + isNodeSelectionEnabled && + this.isSelecting() && + !this.isNodeSelected(realNode) && + !this.isPasting() && + isTargetable && + !isLocked && + stage.mode() === WEAVE_STAGE_DEFAULT_MODE + ) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'pointer'; + } + + // Node is not locked and selected + if ( + isNodeSelectionEnabled && + this.isSelecting() && + this.isNodeSelected(realNode) && + !this.isPasting() && + isTargetable && + !isLocked && + stage.mode() === WEAVE_STAGE_DEFAULT_MODE + ) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'grab'; + } + + // We're on pasting mode + if (this.isPasting()) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'crosshair'; + } + }; + + node.on('pointerover', (e) => { + const stage = this.instance.getStage(); + const activeAction = this.instance.getActiveAction(); + + const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled(); + + const realNode = this.instance.getInstanceRecursive(node); + + const isTargetable = e.target.getAttrs().isTargetable !== false; + const isLocked = realNode.getAttrs().locked ?? false; + + if ([MOVE_TOOL_ACTION_NAME].includes(activeAction ?? '')) { + return; + } + + // Node is locked + if ( + isNodeSelectionEnabled && + this.isSelecting() && + !this.isNodeSelected(realNode) && + !this.isPasting() && + isLocked + ) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + e.cancelBubble = true; + } + + // Node is not locked + if ( + isNodeSelectionEnabled && + this.isSelecting() && + !this.isNodeSelected(realNode) && + !this.isPasting() && + isTargetable && + !isLocked && + stage.mode() === WEAVE_STAGE_DEFAULT_MODE + ) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'pointer'; + this.setHoverState(realNode); + e.cancelBubble = true; + } + + if (!isTargetable) { + this.hideHoverState(); + e.cancelBubble = true; + } + + // We're on pasting mode + if (this.isPasting()) { + const stage = this.instance.getStage(); + stage.container().style.cursor = 'crosshair'; + e.cancelBubble = true; + } + }); + } + } + + create(key: string, props: WeaveElementAttributes): WeaveStateElement { + return { + key, + type: this.nodeType, + props: { + ...props, + id: key, + nodeType: this.nodeType, + children: [], + }, + }; + } + + abstract onRender(props: WeaveElementAttributes): WeaveElementInstance; + + abstract onUpdate( + instance: WeaveElementInstance, + nextProps: WeaveElementAttributes + ): void; + + onDestroy(nodeInstance: WeaveElementInstance): void { + nodeInstance.destroy(); + } + + serialize(instance: WeaveElementInstance): WeaveStateElement { + const attrs = instance.getAttrs(); + + const cleanedAttrs = { ...attrs }; + delete cleanedAttrs.draggable; + + return { + key: attrs.id ?? '', + type: attrs.nodeType, + props: { + ...cleanedAttrs, + id: attrs.id ?? '', + nodeType: attrs.nodeType, + children: [], + }, + }; + } + + show(instance: Konva.Node): void { + if (instance.getAttrs().nodeType !== this.getNodeType()) { + return; + } + + instance.setAttrs({ + visible: true, + }); + + this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); + + this.setupDefaultNodeEvents(instance); + + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + } + + hide(instance: Konva.Node): void { + if (instance.getAttrs().nodeType !== this.getNodeType()) { + return; + } + + instance.setAttrs({ + visible: false, + }); + + const selectionPlugin = this.getSelectionPlugin(); + if (selectionPlugin) { + const ids = [instance.getAttrs().id]; + + if (instance.getAttrs().nodeType === 'frame') { + ids.push(`${instance.getAttrs().id}-selector-area`); + } + + const selectedNodes = selectionPlugin.getSelectedNodes(); + const newSelectedNodes = selectedNodes.filter( + (node) => !ids.includes(node.getAttrs().id) + ); + selectionPlugin.setSelectedNodes(newSelectedNodes); + selectionPlugin.getTransformer().forceUpdate(); + } + + this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); + + this.setupDefaultNodeEvents(instance); + + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + } + + isVisible(instance: Konva.Node): boolean { + if (typeof instance.getAttrs().visible === 'undefined') { + return true; + } + return instance.getAttrs().visible ?? false; + } + + lock(instance: Konva.Node): void { + if (instance.getAttrs().nodeType !== this.getNodeType()) { + return; + } + + instance.setAttrs({ + locked: true, + }); + + this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); + + const selectionPlugin = this.getSelectionPlugin(); + if (selectionPlugin) { + const selectedNodes = selectionPlugin.getSelectedNodes(); + const newSelectedNodes = selectedNodes.filter( + (node) => node.getAttrs().id !== instance.getAttrs().id + ); + selectionPlugin.setSelectedNodes(newSelectedNodes); + selectionPlugin.getTransformer().forceUpdate(); + } + + this.setupDefaultNodeEvents(instance); + + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + } + + unlock(instance: Konva.Node): void { + if (instance.getAttrs().nodeType !== this.getNodeType()) { + return; + } + + let realInstance = instance; + if (instance.getAttrs().nodeId) { + realInstance = this.instance + .getStage() + .findOne(`#${instance.getAttrs().nodeId}`) as Konva.Node; + } + + if (!realInstance) { + return; + } + + realInstance.setAttrs({ + locked: false, + }); + + this.instance.updateNode( + this.serialize(realInstance as WeaveElementInstance) + ); + + this.setupDefaultNodeEvents(realInstance); + + const stage = this.instance.getStage(); + stage.container().style.cursor = 'default'; + } + + isLocked(instance: Konva.Node): boolean { + let realInstance = instance; + if (instance.getAttrs().nodeId === false) { + realInstance = this.instance.getInstanceRecursive(instance); + } + + return realInstance.getAttrs().locked ?? false; + } + + protected defaultGetTransformerProperties( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodeTransformConfig?: any + ) { + const selectionPlugin = + this.instance.getPlugin('nodesSelection'); + let transformProperties = {}; + if (selectionPlugin) { + transformProperties = { + ...transformProperties, + ...selectionPlugin.getSelectorConfig(), + }; + } + + return merge(transformProperties, nodeTransformConfig ?? {}); + } + + private getNodesSelectionPlugin() { + const nodesSelectionPlugin = + this.instance.getPlugin('nodesSelection'); + + return nodesSelectionPlugin; + } + + getNodesEdgeSnappingPlugin() { + const snappingPlugin = + this.instance.getPlugin( + WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY + ); + return snappingPlugin; + } + + getNodesDistanceSnappingPlugin() { + const snappingPlugin = + this.instance.getPlugin( + WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY + ); + return snappingPlugin; + } + + getStagePanningPlugin() { + const panningPlugin = + this.instance.getPlugin('stagePanning'); + return panningPlugin; + } + + getStageGridPlugin() { + const gridPlugin = + this.instance.getPlugin('stageGrid'); + return gridPlugin; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + realOffset(instance: WeaveStateElement): Vector2d { + return { + x: 0, + y: 0, + }; + } +} diff --git a/code/packages/sdk/src/plugins/nodes-distance-snapping/nodes-distance-snapping.ts b/code/packages/sdk/src/plugins/nodes-distance-snapping/nodes-distance-snapping.ts index 35c4fe9d3..58d808c0b 100644 --- a/code/packages/sdk/src/plugins/nodes-distance-snapping/nodes-distance-snapping.ts +++ b/code/packages/sdk/src/plugins/nodes-distance-snapping/nodes-distance-snapping.ts @@ -1110,6 +1110,7 @@ export class WeaveNodesDistanceSnappingPlugin extends WeavePlugin { } disable(): void { + this.cleanupGuidelines(); this.enabled = false; } } diff --git a/code/packages/sdk/src/plugins/nodes-edge-snapping/nodes-edge-snapping.ts b/code/packages/sdk/src/plugins/nodes-edge-snapping/nodes-edge-snapping.ts index 0731c9d0a..f4a2ba4fb 100644 --- a/code/packages/sdk/src/plugins/nodes-edge-snapping/nodes-edge-snapping.ts +++ b/code/packages/sdk/src/plugins/nodes-edge-snapping/nodes-edge-snapping.ts @@ -505,6 +505,7 @@ export class WeaveNodesEdgeSnappingPlugin extends WeavePlugin { } disable(): void { + this.cleanupGuidelines(); this.enabled = false; } } diff --git a/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts b/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts index 9b845e601..ed4f29008 100644 --- a/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts +++ b/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts @@ -917,9 +917,9 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { } if (contextMenuPlugin && contextMenuPlugin.isContextMenuVisible()) { - this.selecting = false; + // this.selecting = false; this.stopPanLoop(); - return; + // return; } if (this.isSpaceKeyPressed) { @@ -1001,10 +1001,10 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { return; } - if (contextMenuPlugin && contextMenuPlugin.isContextMenuVisible()) { - this.selecting = false; + if (contextMenuPlugin?.isContextMenuVisible()) { + // this.selecting = false; this.stopPanLoop(); - return; + // return; } if (!this.selectionRectangle.visible()) { @@ -1204,9 +1204,9 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { const contextMenuPlugin = this.getContextMenuPlugin(); if (contextMenuPlugin?.isContextMenuVisible()) { - this.selecting = false; + // this.selecting = false; this.stopPanLoop(); - return; + // return; } this.hideHoverState(); diff --git a/code/packages/sdk/src/plugins/stage-panning copy/constants.ts b/code/packages/sdk/src/plugins/stage-panning copy/constants.ts new file mode 100644 index 000000000..3f3ae2bbe --- /dev/null +++ b/code/packages/sdk/src/plugins/stage-panning copy/constants.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export const WEAVE_STAGE_PANNING_KEY = 'stagePanning'; + +export const WEAVE_STAGE_PANNING_DEFAULT_CONFIG = { + edgePanOffset: 50, + edgePanSpeed: 10, +}; diff --git a/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts new file mode 100644 index 000000000..7cd24d243 --- /dev/null +++ b/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts @@ -0,0 +1,464 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { WeavePlugin } from '@/plugins/plugin'; +import { + WEAVE_STAGE_PANNING_DEFAULT_CONFIG, + WEAVE_STAGE_PANNING_KEY, +} from './constants'; +import type { KonvaEventObject } from 'konva/lib/Node'; +import type { Stage } from 'konva/lib/Stage'; +import type { Vector2d } from 'konva/lib/types'; +import type { WeaveStageZoomPlugin } from '../stage-zoom/stage-zoom'; +import type { WeaveContextMenuPlugin } from '../context-menu/context-menu'; +import { MOVE_TOOL_ACTION_NAME } from '@/actions/move-tool/constants'; +import { + getTopmostShadowHost, + // getTopmostShadowHost, + isInShadowDOM, +} from '@/utils'; +import type { WeaveNodesEdgeSnappingPlugin } from '../nodes-edge-snapping/nodes-edge-snapping'; +import type { WeaveNodesDistanceSnappingPlugin } from '../nodes-distance-snapping/nodes-distance-snapping'; +import type { WeaveNodesSelectionPlugin } from '../nodes-selection/nodes-selection'; +import { WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY } from '../nodes-edge-snapping/constants'; +import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '../nodes-distance-snapping/constants'; +import { WEAVE_NODES_SELECTION_KEY } from '../nodes-selection/constants'; +import { WEAVE_CONTEXT_MENU_PLUGIN_KEY } from '../context-menu/constants'; +import type { WeaveStageGridPlugin } from '../stage-grid/stage-grid'; +import type Konva from 'konva'; +import merge from 'lodash/merge'; +import type { + WeaveStagePanningPluginConfig, + WeaveStagePanningPluginParams, +} from './types'; + +export class WeaveStagePanningPlugin extends WeavePlugin { + private readonly config!: WeaveStagePanningPluginConfig; + private moveToolActive: boolean; + private isMouseLeftButtonPressed: boolean; + private isMouseMiddleButtonPressed: boolean; + private isCtrlOrMetaPressed: boolean; + private isDragging: boolean; + private enableMove: boolean; + private isSpaceKeyPressed: boolean; + private pointers: Map; + private panning: boolean = false; + protected previousPointer!: string | null; + protected currentPointer: Konva.Vector2d | null = null; + getLayerName = undefined; + initLayer = undefined; + onRender: undefined; + + constructor(params?: WeaveStagePanningPluginParams) { + super(); + + this.config = merge( + WEAVE_STAGE_PANNING_DEFAULT_CONFIG, + params?.config ?? {} + ); + + this.pointers = new Map(); + this.panning = false; + this.isDragging = false; + this.enableMove = false; + this.enabled = true; + this.moveToolActive = false; + this.isMouseLeftButtonPressed = false; + this.isMouseMiddleButtonPressed = false; + this.isCtrlOrMetaPressed = false; + this.isSpaceKeyPressed = false; + this.previousPointer = null; + } + + getName(): string { + return WEAVE_STAGE_PANNING_KEY; + } + + onInit(): void { + this.initEvents(); + } + + private setCursor() { + const stage = this.instance.getStage(); + if (stage.container().style.cursor !== 'grabbing') { + this.previousPointer = stage.container().style.cursor; + stage.container().style.cursor = 'grabbing'; + } + } + + private disableMove() { + const stage = this.instance.getStage(); + if (stage.container().style.cursor === 'grabbing') { + stage.container().style.cursor = this.previousPointer ?? 'default'; + this.previousPointer = null; + } + } + + private initEvents() { + const stage = this.instance.getStage(); + + window.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + this.isCtrlOrMetaPressed = true; + } + if (e.code === 'Space') { + this.getContextMenuPlugin()?.disable(); + this.getNodesSelectionPlugin()?.disable(); + this.getNodesEdgeSnappingPlugin()?.disable(); + this.getNodesDistanceSnappingPlugin()?.disable(); + + this.isSpaceKeyPressed = true; + this.setCursor(); + } + }); + + window.addEventListener('keyup', (e) => { + if (e.key === 'Meta' || e.key === 'Control') { + this.isCtrlOrMetaPressed = false; + } + if (e.code === 'Space') { + this.getContextMenuPlugin()?.enable(); + this.getNodesSelectionPlugin()?.enable(); + this.getNodesEdgeSnappingPlugin()?.enable(); + this.getNodesDistanceSnappingPlugin()?.enable(); + + this.isSpaceKeyPressed = false; + this.disableMove(); + } + }); + + let lastPos: Vector2d | null = null; + + stage.on('pointerdown', (e) => { + this.pointers.set(e.evt.pointerId, { + x: e.evt.clientX, + y: e.evt.clientY, + }); + + if (this.pointers.size > 1) { + return; + } + + const activeAction = this.instance.getActiveAction(); + + this.enableMove = false; + + if (activeAction === MOVE_TOOL_ACTION_NAME) { + this.moveToolActive = true; + } + + if (e.evt.pointerType === 'mouse' && e.evt.buttons === 1) { + this.isMouseLeftButtonPressed = true; + } + + if (e.evt.pointerType === 'mouse' && e.evt.buttons === 4) { + this.isMouseMiddleButtonPressed = true; + } + + const isTouchOrPen = ['touch', 'pen'].includes(e.evt.pointerType); + + if ( + this.enabled && + (this.isSpaceKeyPressed || + (this.moveToolActive && + (this.isMouseLeftButtonPressed || isTouchOrPen)) || + this.isMouseMiddleButtonPressed) + ) { + this.enableMove = true; + } + + if (this.enableMove) { + this.isDragging = true; + lastPos = stage.getPointerPosition(); + this.setCursor(); + } + }); + + stage.on('pointercancel', (e) => { + this.pointers.delete(e.evt.pointerId); + + lastPos = null; + }); + + const handleMouseMove = (e: KonvaEventObject) => { + const pos = stage.getPointerPosition(); + if (pos) this.currentPointer = pos; + + if (['touch', 'pen'].includes(e.evt.pointerType) && e.evt.buttons !== 1) { + return; + } + + this.pointers.set(e.evt.pointerId, { + x: e.evt.clientX, + y: e.evt.clientY, + }); + + if (this.pointers.size > 1) { + return; + } + + if (this.isSpaceKeyPressed) { + stage.container().style.cursor = 'grabbing'; + } + + if (!this.isDragging) return; + + this.getContextMenuPlugin()?.cancelLongPressTimer(); + + if (pos && lastPos) { + const dx = pos.x - lastPos.x; + const dy = pos.y - lastPos.y; + + stage.x(stage.x() + dx); + stage.y(stage.y() + dy); + } + + lastPos = pos; + + this.instance.emitEvent('onStageMove'); + }; + + stage.on('pointermove', handleMouseMove); + + stage.on('pointerup', (e) => { + this.pointers.delete(e.evt.pointerId); + + this.isMouseLeftButtonPressed = false; + this.isMouseMiddleButtonPressed = false; + this.moveToolActive = false; + this.isDragging = false; + this.enableMove = false; + + this.panning = false; + }); + + // Pan with wheel mouse pressed + const handleWheel = (e: WheelEvent) => { + const performPanning = !this.isCtrlOrMetaPressed && !e.ctrlKey; + + const mouseX = e.clientX; + const mouseY = e.clientY; + + let elementUnderMouse = document.elementFromPoint(mouseX, mouseY); + if (isInShadowDOM(stage.container())) { + const shadowHost = getTopmostShadowHost(stage.container()); + if (shadowHost) { + elementUnderMouse = shadowHost.elementFromPoint(mouseX, mouseY); + } + } + + if ( + !this.enabled || + this.isCtrlOrMetaPressed || + e.buttons === 4 || + !performPanning || + this.instance.getClosestParentWithWeaveId(elementUnderMouse) !== + stage.container() + ) { + return; + } + + this.getContextMenuPlugin()?.cancelLongPressTimer(); + + stage.x(stage.x() - e.deltaX); + stage.y(stage.y() - e.deltaY); + + this.instance.emitEvent('onStageMove'); + }; + + window.addEventListener('wheel', handleWheel, { passive: true }); + + let stageScrollInterval: NodeJS.Timeout | undefined = undefined; + const targetScrollIntervals: Record = + {}; + + stage.on('dragstart', (e) => { + const duration = 1000 / 60; + + if ( + typeof targetScrollIntervals[e.target.getAttrs().id ?? ''] !== + 'undefined' + ) { + return; + } + + targetScrollIntervals[e.target.getAttrs().id ?? ''] = setInterval(() => { + const pos = stage.getPointerPosition(); + const offset = this.config.edgePanOffset; + const speed = this.config.edgePanSpeed; + + if (!pos) return; + + const isNearLeft = pos.x < offset; + if (isNearLeft) { + e.target.x(e.target.x() - speed / stage.scaleX()); + } + + const isNearRight = pos.x > stage.width() - offset; + if (isNearRight) { + e.target.x(e.target.x() + speed / stage.scaleX()); + } + + const isNearTop = pos.y < offset; + if (isNearTop) { + e.target.y(e.target.y() - speed / stage.scaleX()); + } + + const isNearBottom = pos.y > stage.height() - offset; + if (isNearBottom) { + e.target.y(e.target.y() + speed / stage.scaleX()); + } + + this.getStageGridPlugin()?.renderGrid(); + }, duration); + + if (typeof stageScrollInterval !== 'undefined') { + return; + } + + stageScrollInterval = setInterval(() => { + const pos = stage.getPointerPosition(); + const offset = this.config.edgePanOffset; + const speed = this.config.edgePanSpeed; + + if (!pos) return; + + let isOnBorder = false; + + const isNearLeft = pos.x < offset; + if (isNearLeft) { + stage.x(stage.x() + speed); + isOnBorder = true; + } + + const isNearRight = pos.x > stage.width() - offset; + if (isNearRight) { + stage.x(stage.x() - speed); + isOnBorder = true; + } + + const isNearTop = pos.y < offset; + if (isNearTop) { + stage.y(stage.y() + speed); + isOnBorder = true; + } + + const isNearBottom = pos.y > stage.height() - offset; + if (isNearBottom) { + stage.y(stage.y() - speed); + isOnBorder = true; + } + + if (isOnBorder) { + this.getNodesEdgeSnappingPlugin()?.disable(); + this.getNodesDistanceSnappingPlugin()?.disable(); + } + if (!isOnBorder) { + this.getNodesEdgeSnappingPlugin()?.enable(); + this.getNodesDistanceSnappingPlugin()?.enable(); + } + + this.getStageGridPlugin()?.renderGrid(); + }, duration); + }); + + stage.on('dragend', () => { + const intervals = Object.keys(targetScrollIntervals); + for (const key of intervals) { + clearInterval(targetScrollIntervals[key]); + targetScrollIntervals[key] = undefined; + } + + clearInterval(stageScrollInterval); + stageScrollInterval = undefined; + }); + + stage.container().style.touchAction = 'none'; + stage.container().style.userSelect = 'none'; + stage.container().style.setProperty('-webkit-user-drag', 'none'); + + stage.getContent().addEventListener( + 'touchmove', + function (e) { + e.preventDefault(); + }, + { passive: false } + ); + } + + isPanning(): boolean { + return this.panning; + } + + getDistance(p1: Vector2d, p2: Vector2d): number { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + return Math.hypot(dx, dy); + } + + getTouchCenter(): { x: number; y: number } | null { + const points = Array.from(this.pointers.values()); + if (points.length !== 2) return null; + + const [p1, p2] = points; + return { + x: (p1.x + p2.x) / 2, + y: (p1.y + p2.y) / 2, + }; + } + + getZoomPlugin() { + const zoomPlugin = + this.instance.getPlugin('stageZoom'); + return zoomPlugin; + } + + getContextMenuPlugin() { + const contextMenuPlugin = this.instance.getPlugin( + WEAVE_CONTEXT_MENU_PLUGIN_KEY + ); + return contextMenuPlugin; + } + + getNodesSelectionPlugin() { + const selectionPlugin = this.instance.getPlugin( + WEAVE_NODES_SELECTION_KEY + ); + return selectionPlugin; + } + + getNodesEdgeSnappingPlugin() { + const snappingPlugin = + this.instance.getPlugin( + WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY + ); + return snappingPlugin; + } + + getNodesDistanceSnappingPlugin() { + const snappingPlugin = + this.instance.getPlugin( + WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY + ); + return snappingPlugin; + } + + getStageGridPlugin() { + const gridPlugin = + this.instance.getPlugin('stageGrid'); + return gridPlugin; + } + + getCurrentPointer() { + return this.currentPointer; + } + + enable(): void { + this.enabled = true; + } + + disable(): void { + this.enabled = false; + } +} diff --git a/code/packages/sdk/src/plugins/stage-panning copy/types.ts b/code/packages/sdk/src/plugins/stage-panning copy/types.ts new file mode 100644 index 000000000..d0f0076df --- /dev/null +++ b/code/packages/sdk/src/plugins/stage-panning copy/types.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export type WeaveStagePanningPluginParams = { + config?: Partial; +}; + +export type WeaveStagePanningPluginConfig = { + edgePanOffset: number; + edgePanSpeed: number; +}; diff --git a/code/packages/sdk/src/plugins/stage-panning/constants.ts b/code/packages/sdk/src/plugins/stage-panning/constants.ts index 37c14f89d..3f3ae2bbe 100644 --- a/code/packages/sdk/src/plugins/stage-panning/constants.ts +++ b/code/packages/sdk/src/plugins/stage-panning/constants.ts @@ -3,3 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 export const WEAVE_STAGE_PANNING_KEY = 'stagePanning'; + +export const WEAVE_STAGE_PANNING_DEFAULT_CONFIG = { + edgePanOffset: 50, + edgePanSpeed: 10, +}; diff --git a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts index c3c1f1356..7cd24d243 100644 --- a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts +++ b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts @@ -3,7 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 import { WeavePlugin } from '@/plugins/plugin'; -import { WEAVE_STAGE_PANNING_KEY } from './constants'; +import { + WEAVE_STAGE_PANNING_DEFAULT_CONFIG, + WEAVE_STAGE_PANNING_KEY, +} from './constants'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Stage } from 'konva/lib/Stage'; import type { Vector2d } from 'konva/lib/types'; @@ -22,8 +25,16 @@ import { WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY } from '../nodes-edge-snapping/con import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '../nodes-distance-snapping/constants'; import { WEAVE_NODES_SELECTION_KEY } from '../nodes-selection/constants'; import { WEAVE_CONTEXT_MENU_PLUGIN_KEY } from '../context-menu/constants'; +import type { WeaveStageGridPlugin } from '../stage-grid/stage-grid'; +import type Konva from 'konva'; +import merge from 'lodash/merge'; +import type { + WeaveStagePanningPluginConfig, + WeaveStagePanningPluginParams, +} from './types'; export class WeaveStagePanningPlugin extends WeavePlugin { + private readonly config!: WeaveStagePanningPluginConfig; private moveToolActive: boolean; private isMouseLeftButtonPressed: boolean; private isMouseMiddleButtonPressed: boolean; @@ -34,13 +45,19 @@ export class WeaveStagePanningPlugin extends WeavePlugin { private pointers: Map; private panning: boolean = false; protected previousPointer!: string | null; + protected currentPointer: Konva.Vector2d | null = null; getLayerName = undefined; initLayer = undefined; onRender: undefined; - constructor() { + constructor(params?: WeaveStagePanningPluginParams) { super(); + this.config = merge( + WEAVE_STAGE_PANNING_DEFAULT_CONFIG, + params?.config ?? {} + ); + this.pointers = new Map(); this.panning = false; this.isDragging = false; @@ -165,6 +182,9 @@ export class WeaveStagePanningPlugin extends WeavePlugin { }); const handleMouseMove = (e: KonvaEventObject) => { + const pos = stage.getPointerPosition(); + if (pos) this.currentPointer = pos; + if (['touch', 'pen'].includes(e.evt.pointerType) && e.evt.buttons !== 1) { return; } @@ -186,8 +206,6 @@ export class WeaveStagePanningPlugin extends WeavePlugin { this.getContextMenuPlugin()?.cancelLongPressTimer(); - const pos = stage.getPointerPosition(); - if (pos && lastPos) { const dx = pos.x - lastPos.x; const dy = pos.y - lastPos.y; @@ -251,6 +269,111 @@ export class WeaveStagePanningPlugin extends WeavePlugin { window.addEventListener('wheel', handleWheel, { passive: true }); + let stageScrollInterval: NodeJS.Timeout | undefined = undefined; + const targetScrollIntervals: Record = + {}; + + stage.on('dragstart', (e) => { + const duration = 1000 / 60; + + if ( + typeof targetScrollIntervals[e.target.getAttrs().id ?? ''] !== + 'undefined' + ) { + return; + } + + targetScrollIntervals[e.target.getAttrs().id ?? ''] = setInterval(() => { + const pos = stage.getPointerPosition(); + const offset = this.config.edgePanOffset; + const speed = this.config.edgePanSpeed; + + if (!pos) return; + + const isNearLeft = pos.x < offset; + if (isNearLeft) { + e.target.x(e.target.x() - speed / stage.scaleX()); + } + + const isNearRight = pos.x > stage.width() - offset; + if (isNearRight) { + e.target.x(e.target.x() + speed / stage.scaleX()); + } + + const isNearTop = pos.y < offset; + if (isNearTop) { + e.target.y(e.target.y() - speed / stage.scaleX()); + } + + const isNearBottom = pos.y > stage.height() - offset; + if (isNearBottom) { + e.target.y(e.target.y() + speed / stage.scaleX()); + } + + this.getStageGridPlugin()?.renderGrid(); + }, duration); + + if (typeof stageScrollInterval !== 'undefined') { + return; + } + + stageScrollInterval = setInterval(() => { + const pos = stage.getPointerPosition(); + const offset = this.config.edgePanOffset; + const speed = this.config.edgePanSpeed; + + if (!pos) return; + + let isOnBorder = false; + + const isNearLeft = pos.x < offset; + if (isNearLeft) { + stage.x(stage.x() + speed); + isOnBorder = true; + } + + const isNearRight = pos.x > stage.width() - offset; + if (isNearRight) { + stage.x(stage.x() - speed); + isOnBorder = true; + } + + const isNearTop = pos.y < offset; + if (isNearTop) { + stage.y(stage.y() + speed); + isOnBorder = true; + } + + const isNearBottom = pos.y > stage.height() - offset; + if (isNearBottom) { + stage.y(stage.y() - speed); + isOnBorder = true; + } + + if (isOnBorder) { + this.getNodesEdgeSnappingPlugin()?.disable(); + this.getNodesDistanceSnappingPlugin()?.disable(); + } + if (!isOnBorder) { + this.getNodesEdgeSnappingPlugin()?.enable(); + this.getNodesDistanceSnappingPlugin()?.enable(); + } + + this.getStageGridPlugin()?.renderGrid(); + }, duration); + }); + + stage.on('dragend', () => { + const intervals = Object.keys(targetScrollIntervals); + for (const key of intervals) { + clearInterval(targetScrollIntervals[key]); + targetScrollIntervals[key] = undefined; + } + + clearInterval(stageScrollInterval); + stageScrollInterval = undefined; + }); + stage.container().style.touchAction = 'none'; stage.container().style.userSelect = 'none'; stage.container().style.setProperty('-webkit-user-drag', 'none'); @@ -321,6 +444,16 @@ export class WeaveStagePanningPlugin extends WeavePlugin { return snappingPlugin; } + getStageGridPlugin() { + const gridPlugin = + this.instance.getPlugin('stageGrid'); + return gridPlugin; + } + + getCurrentPointer() { + return this.currentPointer; + } + enable(): void { this.enabled = true; } diff --git a/code/packages/sdk/src/plugins/stage-panning/types.ts b/code/packages/sdk/src/plugins/stage-panning/types.ts new file mode 100644 index 000000000..d0f0076df --- /dev/null +++ b/code/packages/sdk/src/plugins/stage-panning/types.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export type WeaveStagePanningPluginParams = { + config?: Partial; +}; + +export type WeaveStagePanningPluginConfig = { + edgePanOffset: number; + edgePanSpeed: number; +}; diff --git a/code/packages/sdk/src/weave.ts b/code/packages/sdk/src/weave.ts index 720fac662..93fc3f9b3 100644 --- a/code/packages/sdk/src/weave.ts +++ b/code/packages/sdk/src/weave.ts @@ -708,6 +708,10 @@ export class Weave { // CLONING MANAGEMENT METHODS PROXIES + getCloningManager(): WeaveCloningManager { + return this.cloningManager; + } + nodesToGroupSerialized(instancesToClone: Konva.Node[]): WeaveSerializedGroup { return this.cloningManager.nodesToGroupSerialized(instancesToClone); } From e4483fc6a18ea3e428607c98cf71f5e3b2d5d1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 13:26:34 +0200 Subject: [PATCH 3/7] chore: finish click + alt + drag cloning --- code/packages/sdk/src/managers/cloning.ts | 21 + code/packages/sdk/src/nodes/frame/frame.ts | 1 + code/packages/sdk/src/nodes/node copy.ts | 854 ------------------ code/packages/sdk/src/nodes/node.ts | 129 ++- code/packages/sdk/src/nodes/video/video.ts | 6 + .../nodes-selection/nodes-selection.ts | 28 +- .../plugins/stage-panning copy/constants.ts | 10 - .../stage-panning copy/stage-panning.ts | 464 ---------- .../src/plugins/stage-panning copy/types.ts | 12 - .../plugins/stage-panning/stage-panning.ts | 101 ++- code/packages/sdk/src/weave.ts | 6 +- 11 files changed, 221 insertions(+), 1411 deletions(-) delete mode 100644 code/packages/sdk/src/nodes/node copy.ts delete mode 100644 code/packages/sdk/src/plugins/stage-panning copy/constants.ts delete mode 100644 code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts delete mode 100644 code/packages/sdk/src/plugins/stage-panning copy/types.ts diff --git a/code/packages/sdk/src/managers/cloning.ts b/code/packages/sdk/src/managers/cloning.ts index 97e15b17e..72f0363e0 100644 --- a/code/packages/sdk/src/managers/cloning.ts +++ b/code/packages/sdk/src/managers/cloning.ts @@ -17,6 +17,7 @@ import type { WeaveNode } from '@/nodes/node'; export class WeaveCloningManager { private instance: Weave; private logger: Logger; + private clones: Konva.Node[] = []; constructor(instance: Weave) { this.instance = instance; @@ -314,4 +315,24 @@ export class WeaveCloningManager { return this.instance.getStage().findOne(`#${newNodeId}`); } + + addClone(node: Konva.Node) { + this.clones.push(node); + } + + removeClone(node: Konva.Node) { + this.clones = this.clones.filter((c) => c !== node); + } + + getClones() { + return this.clones; + } + + isClone(node: Konva.Node) { + return this.clones.find((c) => c === node); + } + + cleanupClones() { + this.clones = []; + } } diff --git a/code/packages/sdk/src/nodes/frame/frame.ts b/code/packages/sdk/src/nodes/frame/frame.ts index fa1455d7d..bf7389978 100644 --- a/code/packages/sdk/src/nodes/frame/frame.ts +++ b/code/packages/sdk/src/nodes/frame/frame.ts @@ -211,6 +211,7 @@ export class WeaveFrameNode extends WeaveNode { }, strokeWidth: 0, fill: 'transparent', + nodeId: id, id: `${id}-selection-area`, listening: true, draggable: true, diff --git a/code/packages/sdk/src/nodes/node copy.ts b/code/packages/sdk/src/nodes/node copy.ts deleted file mode 100644 index 662626e00..000000000 --- a/code/packages/sdk/src/nodes/node copy.ts +++ /dev/null @@ -1,854 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { Weave } from '@/weave'; -import merge from 'lodash/merge'; -import { - type WeaveElementAttributes, - type WeaveElementInstance, - type WeaveStateElement, - type WeaveNodeBase, - WEAVE_NODE_CUSTOM_EVENTS, - type WeaveNodeConfiguration, -} from '@inditextech/weave-types'; -import { type Logger } from 'pino'; -import { WeaveNodesSelectionPlugin } from '@/plugins/nodes-selection/nodes-selection'; -import Konva from 'konva'; -import { WeaveCopyPasteNodesPlugin } from '@/plugins/copy-paste-nodes/copy-paste-nodes'; -import type { WeaveNodesSelectionPluginOnNodesChangeEvent } from '@/plugins/nodes-selection/types'; -import { - clearContainerTargets, - containerOverCursor, - hasFrames, - moveNodeToContainer, -} from '@/utils'; -import type { WeaveNodesEdgeSnappingPlugin } from '@/plugins/nodes-edge-snapping/nodes-edge-snapping'; -import { throttle } from 'lodash'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { WEAVE_STAGE_DEFAULT_MODE } from './stage/constants'; -import { MOVE_TOOL_ACTION_NAME } from '@/actions/move-tool/constants'; -import { SELECTION_TOOL_ACTION_NAME } from '@/actions/selection-tool/constants'; -import { WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY } from '@/plugins/nodes-edge-snapping/constants'; -import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '@/plugins/nodes-distance-snapping/constants'; -import type { WeaveNodesDistanceSnappingPlugin } from '@/plugins/nodes-distance-snapping/nodes-distance-snapping'; -import type { Vector2d } from 'konva/lib/types'; -import type { WeaveStageGridPlugin } from '@/plugins/stage-grid/stage-grid'; -import type { WeaveStagePanningPlugin } from '@/plugins/stage-panning/stage-panning'; - -let clones: Konva.Node[] = []; - -export const augmentKonvaStageClass = (): void => { - Konva.Stage.prototype.isMouseWheelPressed = function () { - return false; - }; -}; - -export const augmentKonvaNodeClass = ( - config?: WeaveNodeConfiguration -): void => { - const { transform } = config ?? {}; - - Konva.Node.prototype.getTransformerProperties = function () { - return { - ...transform, - }; - }; - Konva.Node.prototype.getExportClientRect = function (config) { - return this.getClientRect(config); - }; - Konva.Node.prototype.getRealClientRect = function (config) { - return this.getClientRect(config); - }; - Konva.Node.prototype.movedToContainer = function () {}; - Konva.Node.prototype.updatePosition = function () {}; - Konva.Node.prototype.triggerCrop = function () {}; - Konva.Node.prototype.closeCrop = function () {}; - Konva.Node.prototype.resetCrop = function () {}; - Konva.Node.prototype.dblClick = function () {}; -}; - -export abstract class WeaveNode implements WeaveNodeBase { - protected instance!: Weave; - protected nodeType!: string; - protected didMove!: boolean; - private logger!: Logger; - protected previousPointer!: string | null; - - register(instance: Weave): WeaveNode { - this.instance = instance; - this.logger = this.instance.getChildLogger(this.getNodeType()); - this.instance - .getChildLogger('node') - .debug(`Node with type [${this.getNodeType()}] registered`); - - return this; - } - - getNodeType(): string { - return this.nodeType; - } - - getLogger(): Logger { - return this.logger; - } - - getSelectionPlugin(): WeaveNodesSelectionPlugin | undefined { - const selectionPlugin = - this.instance.getPlugin('nodesSelection'); - - return selectionPlugin; - } - - isSelecting(): boolean { - return this.instance.getActiveAction() === SELECTION_TOOL_ACTION_NAME; - } - - isPasting(): boolean { - const copyPastePlugin = - this.instance.getPlugin('copyPasteNodes'); - - if (copyPastePlugin) { - return copyPastePlugin.isPasting(); - } - - return false; - } - - setupDefaultNodeAugmentation(node: Konva.Node): void { - const defaultTransformerProperties = this.defaultGetTransformerProperties(); - - node.getTransformerProperties = function () { - return defaultTransformerProperties; - }; - node.allowedAnchors = function () { - return [ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right', - ]; - }; - node.movedToContainer = function () {}; - node.updatePosition = function () {}; - node.resetCrop = function () {}; - node.handleMouseover = function () {}; - node.handleMouseout = function () {}; - } - - isNodeSelected(ele: Konva.Node): boolean { - const selectionPlugin = - this.instance.getPlugin('nodesSelection'); - - if ( - selectionPlugin - ?.getSelectedNodes() - .map((node) => node.getAttrs().id) - .includes(ele.getAttrs().id) - ) { - return true; - } - - return false; - } - - scaleReset(node: Konva.Node): void { - const scale = node.scale(); - - node.width(Math.max(5, node.width() * scale.x)); - node.height(Math.max(5, node.height() * scale.y)); - - // reset scale to 1 - node.scale({ x: 1, y: 1 }); - } - - protected setHoverState(node: Konva.Node): void { - const selectionPlugin = this.getSelectionPlugin(); - - if (!selectionPlugin) { - return; - } - - if (selectionPlugin.isAreaSelecting()) { - this.hideHoverState(); - return; - } - - selectionPlugin.getHoverTransformer().nodes([node]); - } - - protected hideHoverState(): void { - const selectionPlugin = this.getSelectionPlugin(); - - if (!selectionPlugin) { - return; - } - - selectionPlugin.getHoverTransformer().nodes([]); - } - - private getRealSelectedNode(nodeTarget: Konva.Node) { - const stage = this.instance.getStage(); - - let realNodeTarget: Konva.Node = nodeTarget; - - if (nodeTarget.getParent() instanceof Konva.Transformer) { - const mousePos = stage.getPointerPosition(); - const intersections = stage.getAllIntersections(mousePos); - const nodesIntersected = intersections.filter( - (ele) => ele.getAttrs().nodeType - ); - - if (nodesIntersected.length > 0) { - realNodeTarget = this.instance.getInstanceRecursive( - nodesIntersected[nodesIntersected.length - 1] - ); - } - } - - return realNodeTarget; - } - - setupDefaultNodeEvents(node: Konva.Node): void { - this.instance.addEventListener( - 'onNodesChange', - () => { - if ( - !this.isLocked(node as WeaveElementInstance) && - this.isSelecting() && - this.isNodeSelected(node) - ) { - node.draggable(true); - return; - } - node.draggable(false); - } - ); - - const isLocked = node.getAttrs().locked ?? false; - - if (isLocked) { - node.off('transformstart'); - node.off('transform'); - node.off('transformend'); - node.off('dragstart'); - node.off('dragmove'); - node.off('dragend'); - node.off('pointerenter'); - node.off('pointerleave'); - } else { - let transforming = false; - - node.on('transformstart', (e) => { - transforming = true; - - this.instance.emitEvent('onTransform', e.target); - }); - - const handleTransform = (e: KonvaEventObject) => { - const node = e.target; - - const nodesSelectionPlugin = - this.instance.getPlugin('nodesSelection'); - - const nodesEdgeSnappingPlugin = this.getNodesEdgeSnappingPlugin(); - - if ( - nodesSelectionPlugin && - this.isSelecting() && - this.isNodeSelected(node) - ) { - nodesSelectionPlugin.getTransformer().forceUpdate(); - } - - if ( - nodesEdgeSnappingPlugin && - transforming && - this.isSelecting() && - this.isNodeSelected(node) - ) { - nodesEdgeSnappingPlugin.evaluateGuidelines(e); - } - }; - - node.on('transform', throttle(handleTransform, 100)); - - node.on('transformend', (e) => { - const node = e.target; - - this.instance.emitEvent('onTransform', null); - - transforming = false; - - const nodesSelectionPlugin = - this.instance.getPlugin('nodesSelection'); - - const nodesSnappingPlugin = this.getNodesEdgeSnappingPlugin(); - - if (nodesSnappingPlugin) { - nodesSnappingPlugin.cleanupGuidelines(); - } - - if (nodesSelectionPlugin) { - nodesSelectionPlugin.getTransformer().forceUpdate(); - } - - this.scaleReset(node); - - const nodeHandler = this.instance.getNodeHandler( - node.getAttrs().nodeType - ); - if (nodeHandler) { - this.instance.updateNode( - nodeHandler.serialize(node as WeaveElementInstance) - ); - } - }); - - const stage = this.instance.getStage(); - // const cloned: Record = {}; - let clone: Konva.Node | undefined = undefined; - - node.on('dragstart', (e) => { - const nodeTarget = e.target; - - this.didMove = false; - - if (e.evt?.buttons === 0) { - nodeTarget.stopDrag(); - return; - } - - const isErasing = this.instance.getActiveAction() === 'eraseTool'; - - if (isErasing) { - nodeTarget.stopDrag(); - return; - } - - this.instance.emitEvent('onDrag', nodeTarget); - - if (stage.isMouseWheelPressed()) { - e.cancelBubble = true; - nodeTarget.stopDrag(); - } - - const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); - - if (realNodeTarget.getAttrs().isCloned) { - return; - } - - if (e.evt?.altKey) { - nodeTarget.stopDrag(e.evt); - - e.cancelBubble = true; - - clone = this.instance.getCloningManager().cloneNode(realNodeTarget); - - if (clone && !clones.find((c) => c === clone)) { - clone.setAttrs({ isCloned: true }); - clones.push(clone); - } - - stage.setPointersPositions(e.evt); - - const nodesSelectionPlugin = this.getNodesSelectionPlugin(); - nodesSelectionPlugin?.setSelectedNodes([]); - - setTimeout(() => { - nodesSelectionPlugin?.setSelectedNodes(clones); - clone?.startDrag(e.evt); - }, 0); - } - }); - - const handleDragMove = (e: KonvaEventObject) => { - const nodeTarget = e.target; - - e.cancelBubble = true; - - if (e.evt?.buttons === 0) { - nodeTarget.stopDrag(); - return; - } - - this.didMove = true; - - const stage = this.instance.getStage(); - - const isErasing = this.instance.getActiveAction() === 'eraseTool'; - - if (isErasing) { - nodeTarget.stopDrag(); - return; - } - - if (stage.isMouseWheelPressed()) { - e.cancelBubble = true; - nodeTarget.stopDrag(); - return; - } - - const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); - - if (this.isSelecting() && !realNodeTarget.getAttrs().isCloned) { - clearContainerTargets(this.instance); - - const layerToMove = containerOverCursor(this.instance, [ - realNodeTarget, - ]); - - if ( - layerToMove && - !hasFrames(realNodeTarget) && - realNodeTarget.isDragging() - ) { - layerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetEnter, { - bubbles: true, - }); - } - } - }; - - node.on('dragmove', throttle(handleDragMove, 100)); - - node.on('dragend', (e) => { - const nodeTarget = e.target; - - if (clone) { - clone = undefined; - } - - if (!this.didMove) { - return; - } - - const isErasing = this.instance.getActiveAction() === 'eraseTool'; - - if (isErasing) { - nodeTarget.stopDrag(); - return; - } - - this.instance.emitEvent('onDrag', null); - - const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); - - if (this.isSelecting()) { - clearContainerTargets(this.instance); - - const nodesEdgeSnappingPlugin = this.getNodesEdgeSnappingPlugin(); - - const nodesDistanceSnappingPlugin = - this.getNodesDistanceSnappingPlugin(); - - if (nodesEdgeSnappingPlugin) { - nodesEdgeSnappingPlugin.cleanupGuidelines(); - } - - if (nodesDistanceSnappingPlugin) { - nodesDistanceSnappingPlugin.cleanupGuidelines(); - } - - const layerToMove = containerOverCursor(this.instance, [ - realNodeTarget, - ]); - - let containerToMove: Konva.Layer | Konva.Node | undefined = - this.instance.getMainLayer(); - - if (layerToMove) { - containerToMove = layerToMove; - } - - let moved = false; - if (realNodeTarget.getAttrs().isCloned) { - const parent: Konva.Container = - realNodeTarget.getParent() as Konva.Container; - const realParent = this.instance.getInstanceRecursive(parent); - containerToMove = realParent; - } - - if ( - containerToMove && - !hasFrames(realNodeTarget) && - !realNodeTarget.getAttrs().isCloned - ) { - moved = moveNodeToContainer( - this.instance, - realNodeTarget, - containerToMove - ); - } - - if (realNodeTarget.getAttrs().isCloned) { - clones = clones.filter((c) => c !== realNodeTarget); - } - - realNodeTarget?.setAttrs({ isCloned: undefined }); - - if (containerToMove) { - containerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetLeave, { - bubbles: true, - }); - } - - if (!moved) { - this.instance.updateNode( - this.serialize(realNodeTarget as WeaveElementInstance) - ); - } - } - }); - - node.handleMouseover = () => { - const stage = this.instance.getStage(); - const activeAction = this.instance.getActiveAction(); - - const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled(); - - const realNode = this.instance.getInstanceRecursive(node); - - const isTargetable = realNode.getAttrs().isTargetable !== false; - const isLocked = realNode.getAttrs().locked ?? false; - - if ([MOVE_TOOL_ACTION_NAME].includes(activeAction ?? '')) { - return; - } - - // Node is locked - if ( - isNodeSelectionEnabled && - this.isSelecting() && - !this.isNodeSelected(realNode) && - !this.isPasting() && - isLocked - ) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - } - - // Node is not locked and not selected - if ( - isNodeSelectionEnabled && - this.isSelecting() && - !this.isNodeSelected(realNode) && - !this.isPasting() && - isTargetable && - !isLocked && - stage.mode() === WEAVE_STAGE_DEFAULT_MODE - ) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'pointer'; - } - - // Node is not locked and selected - if ( - isNodeSelectionEnabled && - this.isSelecting() && - this.isNodeSelected(realNode) && - !this.isPasting() && - isTargetable && - !isLocked && - stage.mode() === WEAVE_STAGE_DEFAULT_MODE - ) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'grab'; - } - - // We're on pasting mode - if (this.isPasting()) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'crosshair'; - } - }; - - node.on('pointerover', (e) => { - const stage = this.instance.getStage(); - const activeAction = this.instance.getActiveAction(); - - const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled(); - - const realNode = this.instance.getInstanceRecursive(node); - - const isTargetable = e.target.getAttrs().isTargetable !== false; - const isLocked = realNode.getAttrs().locked ?? false; - - if ([MOVE_TOOL_ACTION_NAME].includes(activeAction ?? '')) { - return; - } - - // Node is locked - if ( - isNodeSelectionEnabled && - this.isSelecting() && - !this.isNodeSelected(realNode) && - !this.isPasting() && - isLocked - ) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - e.cancelBubble = true; - } - - // Node is not locked - if ( - isNodeSelectionEnabled && - this.isSelecting() && - !this.isNodeSelected(realNode) && - !this.isPasting() && - isTargetable && - !isLocked && - stage.mode() === WEAVE_STAGE_DEFAULT_MODE - ) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'pointer'; - this.setHoverState(realNode); - e.cancelBubble = true; - } - - if (!isTargetable) { - this.hideHoverState(); - e.cancelBubble = true; - } - - // We're on pasting mode - if (this.isPasting()) { - const stage = this.instance.getStage(); - stage.container().style.cursor = 'crosshair'; - e.cancelBubble = true; - } - }); - } - } - - create(key: string, props: WeaveElementAttributes): WeaveStateElement { - return { - key, - type: this.nodeType, - props: { - ...props, - id: key, - nodeType: this.nodeType, - children: [], - }, - }; - } - - abstract onRender(props: WeaveElementAttributes): WeaveElementInstance; - - abstract onUpdate( - instance: WeaveElementInstance, - nextProps: WeaveElementAttributes - ): void; - - onDestroy(nodeInstance: WeaveElementInstance): void { - nodeInstance.destroy(); - } - - serialize(instance: WeaveElementInstance): WeaveStateElement { - const attrs = instance.getAttrs(); - - const cleanedAttrs = { ...attrs }; - delete cleanedAttrs.draggable; - - return { - key: attrs.id ?? '', - type: attrs.nodeType, - props: { - ...cleanedAttrs, - id: attrs.id ?? '', - nodeType: attrs.nodeType, - children: [], - }, - }; - } - - show(instance: Konva.Node): void { - if (instance.getAttrs().nodeType !== this.getNodeType()) { - return; - } - - instance.setAttrs({ - visible: true, - }); - - this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); - - this.setupDefaultNodeEvents(instance); - - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - } - - hide(instance: Konva.Node): void { - if (instance.getAttrs().nodeType !== this.getNodeType()) { - return; - } - - instance.setAttrs({ - visible: false, - }); - - const selectionPlugin = this.getSelectionPlugin(); - if (selectionPlugin) { - const ids = [instance.getAttrs().id]; - - if (instance.getAttrs().nodeType === 'frame') { - ids.push(`${instance.getAttrs().id}-selector-area`); - } - - const selectedNodes = selectionPlugin.getSelectedNodes(); - const newSelectedNodes = selectedNodes.filter( - (node) => !ids.includes(node.getAttrs().id) - ); - selectionPlugin.setSelectedNodes(newSelectedNodes); - selectionPlugin.getTransformer().forceUpdate(); - } - - this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); - - this.setupDefaultNodeEvents(instance); - - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - } - - isVisible(instance: Konva.Node): boolean { - if (typeof instance.getAttrs().visible === 'undefined') { - return true; - } - return instance.getAttrs().visible ?? false; - } - - lock(instance: Konva.Node): void { - if (instance.getAttrs().nodeType !== this.getNodeType()) { - return; - } - - instance.setAttrs({ - locked: true, - }); - - this.instance.updateNode(this.serialize(instance as WeaveElementInstance)); - - const selectionPlugin = this.getSelectionPlugin(); - if (selectionPlugin) { - const selectedNodes = selectionPlugin.getSelectedNodes(); - const newSelectedNodes = selectedNodes.filter( - (node) => node.getAttrs().id !== instance.getAttrs().id - ); - selectionPlugin.setSelectedNodes(newSelectedNodes); - selectionPlugin.getTransformer().forceUpdate(); - } - - this.setupDefaultNodeEvents(instance); - - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - } - - unlock(instance: Konva.Node): void { - if (instance.getAttrs().nodeType !== this.getNodeType()) { - return; - } - - let realInstance = instance; - if (instance.getAttrs().nodeId) { - realInstance = this.instance - .getStage() - .findOne(`#${instance.getAttrs().nodeId}`) as Konva.Node; - } - - if (!realInstance) { - return; - } - - realInstance.setAttrs({ - locked: false, - }); - - this.instance.updateNode( - this.serialize(realInstance as WeaveElementInstance) - ); - - this.setupDefaultNodeEvents(realInstance); - - const stage = this.instance.getStage(); - stage.container().style.cursor = 'default'; - } - - isLocked(instance: Konva.Node): boolean { - let realInstance = instance; - if (instance.getAttrs().nodeId === false) { - realInstance = this.instance.getInstanceRecursive(instance); - } - - return realInstance.getAttrs().locked ?? false; - } - - protected defaultGetTransformerProperties( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - nodeTransformConfig?: any - ) { - const selectionPlugin = - this.instance.getPlugin('nodesSelection'); - let transformProperties = {}; - if (selectionPlugin) { - transformProperties = { - ...transformProperties, - ...selectionPlugin.getSelectorConfig(), - }; - } - - return merge(transformProperties, nodeTransformConfig ?? {}); - } - - private getNodesSelectionPlugin() { - const nodesSelectionPlugin = - this.instance.getPlugin('nodesSelection'); - - return nodesSelectionPlugin; - } - - getNodesEdgeSnappingPlugin() { - const snappingPlugin = - this.instance.getPlugin( - WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY - ); - return snappingPlugin; - } - - getNodesDistanceSnappingPlugin() { - const snappingPlugin = - this.instance.getPlugin( - WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY - ); - return snappingPlugin; - } - - getStagePanningPlugin() { - const panningPlugin = - this.instance.getPlugin('stagePanning'); - return panningPlugin; - } - - getStageGridPlugin() { - const gridPlugin = - this.instance.getPlugin('stageGrid'); - return gridPlugin; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - realOffset(instance: WeaveStateElement): Vector2d { - return { - x: 0, - y: 0, - }; - } -} diff --git a/code/packages/sdk/src/nodes/node.ts b/code/packages/sdk/src/nodes/node.ts index 5710a2cbf..e3f412ef5 100644 --- a/code/packages/sdk/src/nodes/node.ts +++ b/code/packages/sdk/src/nodes/node.ts @@ -283,11 +283,15 @@ export abstract class WeaveNode implements WeaveNodeBase { } }); + let clone: Konva.Node | undefined = undefined; + node.on('dragstart', (e) => { + const nodeTarget = e.target; + this.didMove = false; if (e.evt?.buttons === 0) { - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } @@ -296,21 +300,56 @@ export abstract class WeaveNode implements WeaveNodeBase { const isErasing = this.instance.getActiveAction() === 'eraseTool'; if (isErasing) { - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } - this.instance.emitEvent('onDrag', e.target); + this.instance.emitEvent('onDrag', nodeTarget); if (stage.isMouseWheelPressed()) { e.cancelBubble = true; - e.target.stopDrag(); + nodeTarget.stopDrag(); + } + + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + + if (realNodeTarget.getAttrs().isCloned) { + return; + } + + if (e.evt?.altKey) { + nodeTarget.stopDrag(e.evt); + + e.cancelBubble = true; + + clone = this.instance.getCloningManager().cloneNode(realNodeTarget); + + if (clone && !this.instance.getCloningManager().isClone(clone)) { + clone.setAttrs({ isCloned: true }); + this.instance.getCloningManager().addClone(clone); + } + + stage.setPointersPositions(e.evt); + + const nodesSelectionPlugin = this.getNodesSelectionPlugin(); + nodesSelectionPlugin?.setSelectedNodes([]); + + setTimeout(() => { + nodesSelectionPlugin?.setSelectedNodes( + this.instance.getCloningManager().getClones() + ); + clone?.startDrag(e.evt); + }, 0); } }); const handleDragMove = (e: KonvaEventObject) => { + const nodeTarget = e.target; + + e.cancelBubble = true; + if (e.evt?.buttons === 0) { - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } @@ -321,26 +360,34 @@ export abstract class WeaveNode implements WeaveNodeBase { const isErasing = this.instance.getActiveAction() === 'eraseTool'; if (isErasing) { - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } if (stage.isMouseWheelPressed()) { e.cancelBubble = true; - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + if ( this.isSelecting() && - this.isNodeSelected(node) && + // this.isNodeSelected(node) && this.getSelectionPlugin()?.getSelectedNodes().length === 1 ) { clearContainerTargets(this.instance); - const layerToMove = containerOverCursor(this.instance, [node]); + const layerToMove = containerOverCursor(this.instance, [ + realNodeTarget, + ]); - if (layerToMove && !hasFrames(node) && node.isDragging()) { + if ( + layerToMove && + !hasFrames(realNodeTarget) && + realNodeTarget.isDragging() + ) { layerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetEnter, { bubbles: true, }); @@ -351,6 +398,14 @@ export abstract class WeaveNode implements WeaveNodeBase { node.on('dragmove', throttle(handleDragMove, 100)); node.on('dragend', (e) => { + const nodeTarget = e.target; + + e.cancelBubble = true; + + if (clone) { + clone = undefined; + } + if (!this.didMove) { return; } @@ -358,15 +413,16 @@ export abstract class WeaveNode implements WeaveNodeBase { const isErasing = this.instance.getActiveAction() === 'eraseTool'; if (isErasing) { - e.target.stopDrag(); + nodeTarget.stopDrag(); return; } this.instance.emitEvent('onDrag', null); + const realNodeTarget: Konva.Node = this.getRealSelectedNode(nodeTarget); + if ( this.isSelecting() && - this.isNodeSelected(node) && this.getSelectionPlugin()?.getSelectedNodes().length === 1 ) { clearContainerTargets(this.instance); @@ -384,7 +440,9 @@ export abstract class WeaveNode implements WeaveNodeBase { nodesDistanceSnappingPlugin.cleanupGuidelines(); } - const layerToMove = containerOverCursor(this.instance, [node]); + const layerToMove = containerOverCursor(this.instance, [ + realNodeTarget, + ]); let containerToMove: Konva.Layer | Konva.Node | undefined = this.instance.getMainLayer(); @@ -394,14 +452,24 @@ export abstract class WeaveNode implements WeaveNodeBase { } let moved = false; - if (containerToMove && !hasFrames(node)) { + if ( + containerToMove && + !hasFrames(node) && + !realNodeTarget.getAttrs().isCloned + ) { moved = moveNodeToContainer( this.instance, - e.target, + realNodeTarget, containerToMove ); } + if (realNodeTarget.getAttrs().isCloned) { + this.instance.getCloningManager().removeClone(realNodeTarget); + } + + realNodeTarget?.setAttrs({ isCloned: undefined }); + if (containerToMove) { containerToMove.fire(WEAVE_NODE_CUSTOM_EVENTS.onTargetLeave, { bubbles: true, @@ -410,7 +478,7 @@ export abstract class WeaveNode implements WeaveNodeBase { if (!moved) { this.instance.updateNode( - this.serialize(node as WeaveElementInstance) + this.serialize(realNodeTarget as WeaveElementInstance) ); } } @@ -719,6 +787,13 @@ export abstract class WeaveNode implements WeaveNodeBase { return merge(transformProperties, nodeTransformConfig ?? {}); } + private getNodesSelectionPlugin() { + const nodesSelectionPlugin = + this.instance.getPlugin('nodesSelection'); + + return nodesSelectionPlugin; + } + getNodesEdgeSnappingPlugin() { const snappingPlugin = this.instance.getPlugin( @@ -742,4 +817,26 @@ export abstract class WeaveNode implements WeaveNodeBase { y: 0, }; } + + private getRealSelectedNode(nodeTarget: Konva.Node) { + const stage = this.instance.getStage(); + + let realNodeTarget: Konva.Node = nodeTarget; + + if (nodeTarget.getParent() instanceof Konva.Transformer) { + const mousePos = stage.getPointerPosition(); + const intersections = stage.getAllIntersections(mousePos); + const nodesIntersected = intersections.filter( + (ele) => ele.getAttrs().nodeType + ); + + if (nodesIntersected.length > 0) { + realNodeTarget = this.instance.getInstanceRecursive( + nodesIntersected[nodesIntersected.length - 1] + ); + } + } + + return realNodeTarget; + } } diff --git a/code/packages/sdk/src/nodes/video/video.ts b/code/packages/sdk/src/nodes/video/video.ts index 4c11146ab..dadb3402c 100644 --- a/code/packages/sdk/src/nodes/video/video.ts +++ b/code/packages/sdk/src/nodes/video/video.ts @@ -292,6 +292,7 @@ export class WeaveVideoNode extends WeaveNode { fill: this.config.style.background.color, stroke: this.config.style.background.strokeColor, strokeWidth: this.config.style.background.strokeWidth, + nodeId: id, }); videoGroup.add(bg); @@ -306,6 +307,7 @@ export class WeaveVideoNode extends WeaveNode { draggable: false, image: undefined, name: undefined, + nodeId: id, }); video.hide(); @@ -322,6 +324,7 @@ export class WeaveVideoNode extends WeaveNode { strokeWidth: 0, fill: this.config.style.track.color, name: undefined, + nodeId: id, }); this.instance.addEventListener( @@ -348,6 +351,7 @@ export class WeaveVideoNode extends WeaveNode { draggable: false, image: undefined, name: undefined, + nodeId: id, }); videoPlaceholder.show(); @@ -373,6 +377,7 @@ export class WeaveVideoNode extends WeaveNode { strokeWidth: this.config.style.iconBackground.strokeWidth, stroke: this.config.style.iconBackground.strokeColor, fill: this.config.style.iconBackground.color, + nodeId: id, }); videoIconGroup.add(videoIconBg); @@ -386,6 +391,7 @@ export class WeaveVideoNode extends WeaveNode { height: this.config.style.icon.height, fill: 'transparent', image: this.videoIconImage, + nodeId: id, }); videoIconGroup.add(videoIcon); diff --git a/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts b/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts index 76c4a6e39..c4d73fa64 100644 --- a/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts +++ b/code/packages/sdk/src/plugins/nodes-selection/nodes-selection.ts @@ -50,6 +50,8 @@ import type { WeaveNodesDistanceSnappingPlugin } from '../nodes-distance-snappin import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '../nodes-distance-snapping/constants'; import { WEAVE_STAGE_GRID_PLUGIN_KEY } from '../stage-grid/constants'; import type { WeaveStageGridPlugin } from '../stage-grid/stage-grid'; +import type { WeaveStagePanningPlugin } from '../stage-panning/stage-panning'; +import { WEAVE_STAGE_PANNING_KEY } from '../stage-panning/constants'; export class WeaveNodesSelectionPlugin extends WeavePlugin { private tr!: Konva.Transformer; @@ -313,6 +315,12 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { node.updatePosition(node.getAbsolutePosition()); } + if (e.evt?.altKey) { + tr.stopDrag(e.evt); + + e.cancelBubble = true; + } + tr.forceUpdate(); }); @@ -321,6 +329,8 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { ) => { const actualPos = { x: e.target.x(), y: e.target.y() }; + e.cancelBubble = true; + if (initialPos) { const moved = this.checkMovedDrag(initialPos, actualPos); if (moved) { @@ -338,8 +348,6 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { this.didMove = true; - e.cancelBubble = true; - const selectedNodes = tr.nodes(); let selectionContainsFrames = false; for (let i = 0; i < selectedNodes.length; i++) { @@ -372,12 +380,17 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { e.cancelBubble = true; + this.instance.getCloningManager().cleanupClones(); + + this.getStagePanningPlugin()?.cleanupEdgeMoveIntervals(); + const selectedNodes = tr.nodes(); let selectionContainsFrames = false; for (let i = 0; i < selectedNodes.length; i++) { const node = selectedNodes[i]; selectionContainsFrames = selectionContainsFrames || hasFrames(node); node.updatePosition(node.getAbsolutePosition()); + node.setAttrs({ isCloned: undefined }); } if (this.isSelecting() && tr.nodes().length > 1) { @@ -779,7 +792,9 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { (e.code === 'Backspace' || e.code === 'Delete') && Object.keys(window.weaveTextEditing).length === 0 ) { - this.removeSelectedNodes(); + Promise.resolve().then(() => { + this.removeSelectedNodes(); + }); return; } }); @@ -1450,6 +1465,13 @@ export class WeaveNodesSelectionPlugin extends WeavePlugin { return snappingPlugin; } + getStagePanningPlugin() { + const stagePanning = this.instance.getPlugin( + WEAVE_STAGE_PANNING_KEY + ); + return stagePanning; + } + getSelectorConfig(): TransformerConfig { return this.config.selection; } diff --git a/code/packages/sdk/src/plugins/stage-panning copy/constants.ts b/code/packages/sdk/src/plugins/stage-panning copy/constants.ts deleted file mode 100644 index 3f3ae2bbe..000000000 --- a/code/packages/sdk/src/plugins/stage-panning copy/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -export const WEAVE_STAGE_PANNING_KEY = 'stagePanning'; - -export const WEAVE_STAGE_PANNING_DEFAULT_CONFIG = { - edgePanOffset: 50, - edgePanSpeed: 10, -}; diff --git a/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts deleted file mode 100644 index 7cd24d243..000000000 --- a/code/packages/sdk/src/plugins/stage-panning copy/stage-panning.ts +++ /dev/null @@ -1,464 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { WeavePlugin } from '@/plugins/plugin'; -import { - WEAVE_STAGE_PANNING_DEFAULT_CONFIG, - WEAVE_STAGE_PANNING_KEY, -} from './constants'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Stage } from 'konva/lib/Stage'; -import type { Vector2d } from 'konva/lib/types'; -import type { WeaveStageZoomPlugin } from '../stage-zoom/stage-zoom'; -import type { WeaveContextMenuPlugin } from '../context-menu/context-menu'; -import { MOVE_TOOL_ACTION_NAME } from '@/actions/move-tool/constants'; -import { - getTopmostShadowHost, - // getTopmostShadowHost, - isInShadowDOM, -} from '@/utils'; -import type { WeaveNodesEdgeSnappingPlugin } from '../nodes-edge-snapping/nodes-edge-snapping'; -import type { WeaveNodesDistanceSnappingPlugin } from '../nodes-distance-snapping/nodes-distance-snapping'; -import type { WeaveNodesSelectionPlugin } from '../nodes-selection/nodes-selection'; -import { WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY } from '../nodes-edge-snapping/constants'; -import { WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY } from '../nodes-distance-snapping/constants'; -import { WEAVE_NODES_SELECTION_KEY } from '../nodes-selection/constants'; -import { WEAVE_CONTEXT_MENU_PLUGIN_KEY } from '../context-menu/constants'; -import type { WeaveStageGridPlugin } from '../stage-grid/stage-grid'; -import type Konva from 'konva'; -import merge from 'lodash/merge'; -import type { - WeaveStagePanningPluginConfig, - WeaveStagePanningPluginParams, -} from './types'; - -export class WeaveStagePanningPlugin extends WeavePlugin { - private readonly config!: WeaveStagePanningPluginConfig; - private moveToolActive: boolean; - private isMouseLeftButtonPressed: boolean; - private isMouseMiddleButtonPressed: boolean; - private isCtrlOrMetaPressed: boolean; - private isDragging: boolean; - private enableMove: boolean; - private isSpaceKeyPressed: boolean; - private pointers: Map; - private panning: boolean = false; - protected previousPointer!: string | null; - protected currentPointer: Konva.Vector2d | null = null; - getLayerName = undefined; - initLayer = undefined; - onRender: undefined; - - constructor(params?: WeaveStagePanningPluginParams) { - super(); - - this.config = merge( - WEAVE_STAGE_PANNING_DEFAULT_CONFIG, - params?.config ?? {} - ); - - this.pointers = new Map(); - this.panning = false; - this.isDragging = false; - this.enableMove = false; - this.enabled = true; - this.moveToolActive = false; - this.isMouseLeftButtonPressed = false; - this.isMouseMiddleButtonPressed = false; - this.isCtrlOrMetaPressed = false; - this.isSpaceKeyPressed = false; - this.previousPointer = null; - } - - getName(): string { - return WEAVE_STAGE_PANNING_KEY; - } - - onInit(): void { - this.initEvents(); - } - - private setCursor() { - const stage = this.instance.getStage(); - if (stage.container().style.cursor !== 'grabbing') { - this.previousPointer = stage.container().style.cursor; - stage.container().style.cursor = 'grabbing'; - } - } - - private disableMove() { - const stage = this.instance.getStage(); - if (stage.container().style.cursor === 'grabbing') { - stage.container().style.cursor = this.previousPointer ?? 'default'; - this.previousPointer = null; - } - } - - private initEvents() { - const stage = this.instance.getStage(); - - window.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - this.isCtrlOrMetaPressed = true; - } - if (e.code === 'Space') { - this.getContextMenuPlugin()?.disable(); - this.getNodesSelectionPlugin()?.disable(); - this.getNodesEdgeSnappingPlugin()?.disable(); - this.getNodesDistanceSnappingPlugin()?.disable(); - - this.isSpaceKeyPressed = true; - this.setCursor(); - } - }); - - window.addEventListener('keyup', (e) => { - if (e.key === 'Meta' || e.key === 'Control') { - this.isCtrlOrMetaPressed = false; - } - if (e.code === 'Space') { - this.getContextMenuPlugin()?.enable(); - this.getNodesSelectionPlugin()?.enable(); - this.getNodesEdgeSnappingPlugin()?.enable(); - this.getNodesDistanceSnappingPlugin()?.enable(); - - this.isSpaceKeyPressed = false; - this.disableMove(); - } - }); - - let lastPos: Vector2d | null = null; - - stage.on('pointerdown', (e) => { - this.pointers.set(e.evt.pointerId, { - x: e.evt.clientX, - y: e.evt.clientY, - }); - - if (this.pointers.size > 1) { - return; - } - - const activeAction = this.instance.getActiveAction(); - - this.enableMove = false; - - if (activeAction === MOVE_TOOL_ACTION_NAME) { - this.moveToolActive = true; - } - - if (e.evt.pointerType === 'mouse' && e.evt.buttons === 1) { - this.isMouseLeftButtonPressed = true; - } - - if (e.evt.pointerType === 'mouse' && e.evt.buttons === 4) { - this.isMouseMiddleButtonPressed = true; - } - - const isTouchOrPen = ['touch', 'pen'].includes(e.evt.pointerType); - - if ( - this.enabled && - (this.isSpaceKeyPressed || - (this.moveToolActive && - (this.isMouseLeftButtonPressed || isTouchOrPen)) || - this.isMouseMiddleButtonPressed) - ) { - this.enableMove = true; - } - - if (this.enableMove) { - this.isDragging = true; - lastPos = stage.getPointerPosition(); - this.setCursor(); - } - }); - - stage.on('pointercancel', (e) => { - this.pointers.delete(e.evt.pointerId); - - lastPos = null; - }); - - const handleMouseMove = (e: KonvaEventObject) => { - const pos = stage.getPointerPosition(); - if (pos) this.currentPointer = pos; - - if (['touch', 'pen'].includes(e.evt.pointerType) && e.evt.buttons !== 1) { - return; - } - - this.pointers.set(e.evt.pointerId, { - x: e.evt.clientX, - y: e.evt.clientY, - }); - - if (this.pointers.size > 1) { - return; - } - - if (this.isSpaceKeyPressed) { - stage.container().style.cursor = 'grabbing'; - } - - if (!this.isDragging) return; - - this.getContextMenuPlugin()?.cancelLongPressTimer(); - - if (pos && lastPos) { - const dx = pos.x - lastPos.x; - const dy = pos.y - lastPos.y; - - stage.x(stage.x() + dx); - stage.y(stage.y() + dy); - } - - lastPos = pos; - - this.instance.emitEvent('onStageMove'); - }; - - stage.on('pointermove', handleMouseMove); - - stage.on('pointerup', (e) => { - this.pointers.delete(e.evt.pointerId); - - this.isMouseLeftButtonPressed = false; - this.isMouseMiddleButtonPressed = false; - this.moveToolActive = false; - this.isDragging = false; - this.enableMove = false; - - this.panning = false; - }); - - // Pan with wheel mouse pressed - const handleWheel = (e: WheelEvent) => { - const performPanning = !this.isCtrlOrMetaPressed && !e.ctrlKey; - - const mouseX = e.clientX; - const mouseY = e.clientY; - - let elementUnderMouse = document.elementFromPoint(mouseX, mouseY); - if (isInShadowDOM(stage.container())) { - const shadowHost = getTopmostShadowHost(stage.container()); - if (shadowHost) { - elementUnderMouse = shadowHost.elementFromPoint(mouseX, mouseY); - } - } - - if ( - !this.enabled || - this.isCtrlOrMetaPressed || - e.buttons === 4 || - !performPanning || - this.instance.getClosestParentWithWeaveId(elementUnderMouse) !== - stage.container() - ) { - return; - } - - this.getContextMenuPlugin()?.cancelLongPressTimer(); - - stage.x(stage.x() - e.deltaX); - stage.y(stage.y() - e.deltaY); - - this.instance.emitEvent('onStageMove'); - }; - - window.addEventListener('wheel', handleWheel, { passive: true }); - - let stageScrollInterval: NodeJS.Timeout | undefined = undefined; - const targetScrollIntervals: Record = - {}; - - stage.on('dragstart', (e) => { - const duration = 1000 / 60; - - if ( - typeof targetScrollIntervals[e.target.getAttrs().id ?? ''] !== - 'undefined' - ) { - return; - } - - targetScrollIntervals[e.target.getAttrs().id ?? ''] = setInterval(() => { - const pos = stage.getPointerPosition(); - const offset = this.config.edgePanOffset; - const speed = this.config.edgePanSpeed; - - if (!pos) return; - - const isNearLeft = pos.x < offset; - if (isNearLeft) { - e.target.x(e.target.x() - speed / stage.scaleX()); - } - - const isNearRight = pos.x > stage.width() - offset; - if (isNearRight) { - e.target.x(e.target.x() + speed / stage.scaleX()); - } - - const isNearTop = pos.y < offset; - if (isNearTop) { - e.target.y(e.target.y() - speed / stage.scaleX()); - } - - const isNearBottom = pos.y > stage.height() - offset; - if (isNearBottom) { - e.target.y(e.target.y() + speed / stage.scaleX()); - } - - this.getStageGridPlugin()?.renderGrid(); - }, duration); - - if (typeof stageScrollInterval !== 'undefined') { - return; - } - - stageScrollInterval = setInterval(() => { - const pos = stage.getPointerPosition(); - const offset = this.config.edgePanOffset; - const speed = this.config.edgePanSpeed; - - if (!pos) return; - - let isOnBorder = false; - - const isNearLeft = pos.x < offset; - if (isNearLeft) { - stage.x(stage.x() + speed); - isOnBorder = true; - } - - const isNearRight = pos.x > stage.width() - offset; - if (isNearRight) { - stage.x(stage.x() - speed); - isOnBorder = true; - } - - const isNearTop = pos.y < offset; - if (isNearTop) { - stage.y(stage.y() + speed); - isOnBorder = true; - } - - const isNearBottom = pos.y > stage.height() - offset; - if (isNearBottom) { - stage.y(stage.y() - speed); - isOnBorder = true; - } - - if (isOnBorder) { - this.getNodesEdgeSnappingPlugin()?.disable(); - this.getNodesDistanceSnappingPlugin()?.disable(); - } - if (!isOnBorder) { - this.getNodesEdgeSnappingPlugin()?.enable(); - this.getNodesDistanceSnappingPlugin()?.enable(); - } - - this.getStageGridPlugin()?.renderGrid(); - }, duration); - }); - - stage.on('dragend', () => { - const intervals = Object.keys(targetScrollIntervals); - for (const key of intervals) { - clearInterval(targetScrollIntervals[key]); - targetScrollIntervals[key] = undefined; - } - - clearInterval(stageScrollInterval); - stageScrollInterval = undefined; - }); - - stage.container().style.touchAction = 'none'; - stage.container().style.userSelect = 'none'; - stage.container().style.setProperty('-webkit-user-drag', 'none'); - - stage.getContent().addEventListener( - 'touchmove', - function (e) { - e.preventDefault(); - }, - { passive: false } - ); - } - - isPanning(): boolean { - return this.panning; - } - - getDistance(p1: Vector2d, p2: Vector2d): number { - const dx = p2.x - p1.x; - const dy = p2.y - p1.y; - return Math.hypot(dx, dy); - } - - getTouchCenter(): { x: number; y: number } | null { - const points = Array.from(this.pointers.values()); - if (points.length !== 2) return null; - - const [p1, p2] = points; - return { - x: (p1.x + p2.x) / 2, - y: (p1.y + p2.y) / 2, - }; - } - - getZoomPlugin() { - const zoomPlugin = - this.instance.getPlugin('stageZoom'); - return zoomPlugin; - } - - getContextMenuPlugin() { - const contextMenuPlugin = this.instance.getPlugin( - WEAVE_CONTEXT_MENU_PLUGIN_KEY - ); - return contextMenuPlugin; - } - - getNodesSelectionPlugin() { - const selectionPlugin = this.instance.getPlugin( - WEAVE_NODES_SELECTION_KEY - ); - return selectionPlugin; - } - - getNodesEdgeSnappingPlugin() { - const snappingPlugin = - this.instance.getPlugin( - WEAVE_NODES_EDGE_SNAPPING_PLUGIN_KEY - ); - return snappingPlugin; - } - - getNodesDistanceSnappingPlugin() { - const snappingPlugin = - this.instance.getPlugin( - WEAVE_NODES_DISTANCE_SNAPPING_PLUGIN_KEY - ); - return snappingPlugin; - } - - getStageGridPlugin() { - const gridPlugin = - this.instance.getPlugin('stageGrid'); - return gridPlugin; - } - - getCurrentPointer() { - return this.currentPointer; - } - - enable(): void { - this.enabled = true; - } - - disable(): void { - this.enabled = false; - } -} diff --git a/code/packages/sdk/src/plugins/stage-panning copy/types.ts b/code/packages/sdk/src/plugins/stage-panning copy/types.ts deleted file mode 100644 index d0f0076df..000000000 --- a/code/packages/sdk/src/plugins/stage-panning copy/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -export type WeaveStagePanningPluginParams = { - config?: Partial; -}; - -export type WeaveStagePanningPluginConfig = { - edgePanOffset: number; - edgePanSpeed: number; -}; diff --git a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts index 7cd24d243..a6d3775a6 100644 --- a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts +++ b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts @@ -13,11 +13,7 @@ import type { Vector2d } from 'konva/lib/types'; import type { WeaveStageZoomPlugin } from '../stage-zoom/stage-zoom'; import type { WeaveContextMenuPlugin } from '../context-menu/context-menu'; import { MOVE_TOOL_ACTION_NAME } from '@/actions/move-tool/constants'; -import { - getTopmostShadowHost, - // getTopmostShadowHost, - isInShadowDOM, -} from '@/utils'; +import { getTopmostShadowHost, isInShadowDOM } from '@/utils'; import type { WeaveNodesEdgeSnappingPlugin } from '../nodes-edge-snapping/nodes-edge-snapping'; import type { WeaveNodesDistanceSnappingPlugin } from '../nodes-distance-snapping/nodes-distance-snapping'; import type { WeaveNodesSelectionPlugin } from '../nodes-selection/nodes-selection'; @@ -46,6 +42,10 @@ export class WeaveStagePanningPlugin extends WeavePlugin { private panning: boolean = false; protected previousPointer!: string | null; protected currentPointer: Konva.Vector2d | null = null; + protected stageScrollInterval: NodeJS.Timeout | undefined = undefined; + protected targetScrollIntervals: Record = + {}; + getLayerName = undefined; initLayer = undefined; onRender: undefined; @@ -269,55 +269,54 @@ export class WeaveStagePanningPlugin extends WeavePlugin { window.addEventListener('wheel', handleWheel, { passive: true }); - let stageScrollInterval: NodeJS.Timeout | undefined = undefined; - const targetScrollIntervals: Record = - {}; - stage.on('dragstart', (e) => { const duration = 1000 / 60; if ( - typeof targetScrollIntervals[e.target.getAttrs().id ?? ''] !== + typeof this.targetScrollIntervals[e.target.getAttrs().id ?? ''] !== 'undefined' ) { return; } - targetScrollIntervals[e.target.getAttrs().id ?? ''] = setInterval(() => { - const pos = stage.getPointerPosition(); - const offset = this.config.edgePanOffset; - const speed = this.config.edgePanSpeed; - - if (!pos) return; - - const isNearLeft = pos.x < offset; - if (isNearLeft) { - e.target.x(e.target.x() - speed / stage.scaleX()); - } - - const isNearRight = pos.x > stage.width() - offset; - if (isNearRight) { - e.target.x(e.target.x() + speed / stage.scaleX()); - } - - const isNearTop = pos.y < offset; - if (isNearTop) { - e.target.y(e.target.y() - speed / stage.scaleX()); - } - - const isNearBottom = pos.y > stage.height() - offset; - if (isNearBottom) { - e.target.y(e.target.y() + speed / stage.scaleX()); - } - - this.getStageGridPlugin()?.renderGrid(); - }, duration); + this.targetScrollIntervals[e.target.getAttrs().id ?? ''] = setInterval( + () => { + const pos = stage.getPointerPosition(); + const offset = this.config.edgePanOffset; + const speed = this.config.edgePanSpeed; + + if (!pos) return; + + const isNearLeft = pos.x < offset; + if (isNearLeft) { + e.target.x(e.target.x() - speed / stage.scaleX()); + } + + const isNearRight = pos.x > stage.width() - offset; + if (isNearRight) { + e.target.x(e.target.x() + speed / stage.scaleX()); + } + + const isNearTop = pos.y < offset; + if (isNearTop) { + e.target.y(e.target.y() - speed / stage.scaleX()); + } + + const isNearBottom = pos.y > stage.height() - offset; + if (isNearBottom) { + e.target.y(e.target.y() + speed / stage.scaleX()); + } + + this.getStageGridPlugin()?.renderGrid(); + }, + duration + ); - if (typeof stageScrollInterval !== 'undefined') { + if (typeof this.stageScrollInterval !== 'undefined') { return; } - stageScrollInterval = setInterval(() => { + this.stageScrollInterval = setInterval(() => { const pos = stage.getPointerPosition(); const offset = this.config.edgePanOffset; const speed = this.config.edgePanSpeed; @@ -364,14 +363,7 @@ export class WeaveStagePanningPlugin extends WeavePlugin { }); stage.on('dragend', () => { - const intervals = Object.keys(targetScrollIntervals); - for (const key of intervals) { - clearInterval(targetScrollIntervals[key]); - targetScrollIntervals[key] = undefined; - } - - clearInterval(stageScrollInterval); - stageScrollInterval = undefined; + this.cleanupEdgeMoveIntervals(); }); stage.container().style.touchAction = 'none'; @@ -454,6 +446,17 @@ export class WeaveStagePanningPlugin extends WeavePlugin { return this.currentPointer; } + cleanupEdgeMoveIntervals() { + const intervals = Object.keys(this.targetScrollIntervals); + for (const key of intervals) { + clearInterval(this.targetScrollIntervals[key]); + this.targetScrollIntervals[key] = undefined; + } + + clearInterval(this.stageScrollInterval); + this.stageScrollInterval = undefined; + } + enable(): void { this.enabled = true; } diff --git a/code/packages/sdk/src/weave.ts b/code/packages/sdk/src/weave.ts index 93fc3f9b3..330a80c1e 100644 --- a/code/packages/sdk/src/weave.ts +++ b/code/packages/sdk/src/weave.ts @@ -580,7 +580,7 @@ export class Weave { const stage = this.getStage(); let nodeContainer = node.getParent()?.getAttrs().id ?? ''; - if (typeof node.getParent()?.getAttrs().nodeId !== 'undefined') { + if (typeof node?.getParent()?.getAttrs().nodeId !== 'undefined') { const realContainer = stage.findOne( `#${node.getParent()?.getAttrs().nodeId}` ); @@ -594,9 +594,9 @@ export class Weave { getNodeContainer(node: WeaveElementInstance | Konva.Node): Konva.Node | null { const stage = this.getStage(); - let nodeContainer: Konva.Node | null = node.getParent(); + let nodeContainer: Konva.Node | null = node?.getParent(); - if (typeof node.getParent()?.getAttrs().nodeId !== 'undefined') { + if (typeof node?.getParent()?.getAttrs().nodeId !== 'undefined') { const realContainer = stage.findOne( `#${node.getParent()?.getAttrs().nodeId}` ); From e58ab0f30c794a089531d0c653cda8ef93f3d2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 13:28:35 +0200 Subject: [PATCH 4/7] chore: update changelog and docs --- code/CHANGELOG.md | 144 +----------------- docs/content/docs/main/changelog/index.mdx | 1 + .../docs/main/changelog/prerelease/0.74.0.mdx | 12 ++ .../docs/main/changelog/prerelease/meta.json | 1 + 4 files changed, 18 insertions(+), 140 deletions(-) create mode 100644 docs/content/docs/main/changelog/prerelease/0.74.0.mdx diff --git a/code/CHANGELOG.md b/code/CHANGELOG.md index 7aa64828a..5b61c80de 100644 --- a/code/CHANGELOG.md +++ b/code/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- [#693](https://github.com/InditexTech/weavejs/issues/693) Clone elements on ALT + Click & Drag + ## [0.73.1] - 2025-10-09 ### Fixed @@ -1104,283 +1108,143 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#18](https://github.com/InditexTech/weavejs/issues/18) Fix awareness not working on store-azure-web-pubsub [Unreleased]: https://github.com/InditexTech/weavejs/compare/0.73.1...HEAD - [0.73.1]: https://github.com/InditexTech/weavejs/compare/0.73.0...0.73.1 - [0.73.0]: https://github.com/InditexTech/weavejs/compare/0.72.1...0.73.0 - [0.72.1]: https://github.com/InditexTech/weavejs/compare/0.72.0...0.72.1 - [0.72.0]: https://github.com/InditexTech/weavejs/compare/0.71.0...0.72.0 - [0.71.0]: https://github.com/InditexTech/weavejs/compare/0.70.0...0.71.0 - [0.70.0]: https://github.com/InditexTech/weavejs/compare/0.69.2...0.70.0 - [0.69.2]: https://github.com/InditexTech/weavejs/compare/0.69.1...0.69.2 - [0.69.1]: https://github.com/InditexTech/weavejs/compare/0.69.0...0.69.1 - [0.69.0]: https://github.com/InditexTech/weavejs/compare/0.68.1...0.69.0 - [0.68.1]: https://github.com/InditexTech/weavejs/compare/0.68.0...0.68.1 - [0.68.0]: https://github.com/InditexTech/weavejs/compare/0.67.5...0.68.0 - [0.67.5]: https://github.com/InditexTech/weavejs/compare/0.67.4...0.67.5 - [0.67.4]: https://github.com/InditexTech/weavejs/compare/0.67.3...0.67.4 - [0.67.3]: https://github.com/InditexTech/weavejs/compare/0.67.2...0.67.3 - [0.67.2]: https://github.com/InditexTech/weavejs/compare/0.67.1...0.67.2 - [0.67.1]: https://github.com/InditexTech/weavejs/compare/0.67.0...0.67.1 - [0.67.0]: https://github.com/InditexTech/weavejs/compare/0.66.0...0.67.0 - [0.66.0]: https://github.com/InditexTech/weavejs/compare/0.64.0...0.66.0 - [0.64.0]: https://github.com/InditexTech/weavejs/compare/0.62.4...0.64.0 - [0.62.4]: https://github.com/InditexTech/weavejs/compare/0.62.3...0.62.4 - [0.62.3]: https://github.com/InditexTech/weavejs/compare/0.62.2...0.62.3 - [0.62.2]: https://github.com/InditexTech/weavejs/compare/0.62.1...0.62.2 - [0.62.1]: https://github.com/InditexTech/weavejs/compare/0.62.0...0.62.1 - [0.62.0]: https://github.com/InditexTech/weavejs/compare/0.61.0...0.62.0 - [0.61.0]: https://github.com/InditexTech/weavejs/compare/0.60.0...0.61.0 - [0.60.0]: https://github.com/InditexTech/weavejs/compare/0.59.0...0.60.0 - [0.59.0]: https://github.com/InditexTech/weavejs/compare/0.58.0...0.59.0 - [0.58.0]: https://github.com/InditexTech/weavejs/compare/0.57.1...0.58.0 - [0.57.1]: https://github.com/InditexTech/weavejs/compare/0.57.0...0.57.1 - [0.57.0]: https://github.com/InditexTech/weavejs/compare/0.56.2...0.57.0 - [0.56.2]: https://github.com/InditexTech/weavejs/compare/0.56.1...0.56.2 - [0.56.1]: https://github.com/InditexTech/weavejs/compare/0.56.0...0.56.1 - [0.56.0]: https://github.com/InditexTech/weavejs/compare/0.55.2...0.56.0 - [0.55.2]: https://github.com/InditexTech/weavejs/compare/0.55.1...0.55.2 - [0.55.1]: https://github.com/InditexTech/weavejs/compare/0.55.0...0.55.1 - [0.55.0]: https://github.com/InditexTech/weavejs/compare/0.54.1...0.55.0 - [0.54.1]: https://github.com/InditexTech/weavejs/compare/0.54.0...0.54.1 - [0.54.0]: https://github.com/InditexTech/weavejs/compare/0.53.0...0.54.0 - [0.53.0]: https://github.com/InditexTech/weavejs/compare/0.52.3...0.53.0 - [0.52.3]: https://github.com/InditexTech/weavejs/compare/0.52.2...0.52.3 - [0.52.2]: https://github.com/InditexTech/weavejs/compare/0.52.1...0.52.2 - [0.52.1]: https://github.com/InditexTech/weavejs/compare/0.52.0...0.52.1 - [0.52.0]: https://github.com/InditexTech/weavejs/compare/0.51.0...0.52.0 - [0.51.0]: https://github.com/InditexTech/weavejs/compare/0.50.0...0.51.0 - [0.50.0]: https://github.com/InditexTech/weavejs/compare/0.49.0...0.50.0 - [0.49.0]: https://github.com/InditexTech/weavejs/compare/0.48.0...0.49.0 - [0.48.0]: https://github.com/InditexTech/weavejs/compare/0.47.1...0.48.0 - [0.47.1]: https://github.com/InditexTech/weavejs/compare/0.47.0...0.47.1 - [0.47.0]: https://github.com/InditexTech/weavejs/compare/0.46.1...0.47.0 - [0.46.1]: https://github.com/InditexTech/weavejs/compare/0.46.0...0.46.1 - [0.46.0]: https://github.com/InditexTech/weavejs/compare/0.45.0...0.46.0 - [0.45.0]: https://github.com/InditexTech/weavejs/compare/0.44.0...0.45.0 - [0.44.0]: https://github.com/InditexTech/weavejs/compare/0.43.0...0.44.0 - [0.43.0]: https://github.com/InditexTech/weavejs/compare/0.42.2...0.43.0 - [0.42.2]: https://github.com/InditexTech/weavejs/compare/0.42.1...0.42.2 - [0.42.1]: https://github.com/InditexTech/weavejs/compare/0.42.0...0.42.1 - [0.42.0]: https://github.com/InditexTech/weavejs/compare/0.41.0...0.42.0 - [0.41.0]: https://github.com/InditexTech/weavejs/compare/0.40.2...0.41.0 - [0.40.2]: https://github.com/InditexTech/weavejs/compare/0.40.1...0.40.2 - [0.40.1]: https://github.com/InditexTech/weavejs/compare/0.40.0...0.40.1 - [0.40.0]: https://github.com/InditexTech/weavejs/compare/0.39.3...0.40.0 - [0.39.3]: https://github.com/InditexTech/weavejs/compare/0.39.2...0.39.3 - [0.39.2]: https://github.com/InditexTech/weavejs/compare/0.39.1...0.39.2 - [0.39.1]: https://github.com/InditexTech/weavejs/compare/0.39.0...0.39.1 - [0.39.0]: https://github.com/InditexTech/weavejs/compare/0.38.0...0.39.0 - [0.38.0]: https://github.com/InditexTech/weavejs/compare/0.37.0...0.38.0 - [0.37.0]: https://github.com/InditexTech/weavejs/compare/0.36.0...0.37.0 - [0.36.0]: https://github.com/InditexTech/weavejs/compare/0.35.0...0.36.0 - [0.35.0]: https://github.com/InditexTech/weavejs/compare/0.34.0...0.35.0 - [0.34.0]: https://github.com/InditexTech/weavejs/compare/0.33.0...0.34.0 - [0.33.0]: https://github.com/InditexTech/weavejs/compare/0.32.0...0.33.0 - [0.32.0]: https://github.com/InditexTech/weavejs/compare/0.31.1...0.32.0 - [0.31.1]: https://github.com/InditexTech/weavejs/compare/0.31.0...0.31.1 - [0.31.0]: https://github.com/InditexTech/weavejs/compare/0.30.1...0.31.0 - [0.30.1]: https://github.com/InditexTech/weavejs/compare/0.30.0...0.30.1 - [0.30.0]: https://github.com/InditexTech/weavejs/compare/0.29.1...0.30.0 - [0.29.1]: https://github.com/InditexTech/weavejs/compare/0.29.0...0.29.1 - [0.29.0]: https://github.com/InditexTech/weavejs/compare/0.28.0...0.29.0 - [0.28.0]: https://github.com/InditexTech/weavejs/compare/0.27.4...0.28.0 - [0.27.4]: https://github.com/InditexTech/weavejs/compare/0.27.3...0.27.4 - [0.27.3]: https://github.com/InditexTech/weavejs/compare/0.27.2...0.27.3 - [0.27.2]: https://github.com/InditexTech/weavejs/compare/0.27.1...0.27.2 - [0.27.1]: https://github.com/InditexTech/weavejs/compare/0.27.0...0.27.1 - [0.27.0]: https://github.com/InditexTech/weavejs/compare/0.26.2...0.27.0 - [0.26.2]: https://github.com/InditexTech/weavejs/compare/0.26.1...0.26.2 - [0.26.1]: https://github.com/InditexTech/weavejs/compare/0.26.0...0.26.1 - [0.26.0]: https://github.com/InditexTech/weavejs/compare/0.25.0...0.26.0 - [0.25.0]: https://github.com/InditexTech/weavejs/compare/0.24.1...0.25.0 - [0.24.1]: https://github.com/InditexTech/weavejs/compare/0.24.0...0.24.1 - [0.24.0]: https://github.com/InditexTech/weavejs/compare/0.23.1...0.24.0 - [0.23.1]: https://github.com/InditexTech/weavejs/compare/0.23.0...0.23.1 - [0.23.0]: https://github.com/InditexTech/weavejs/compare/0.22.1...0.23.0 - [0.22.1]: https://github.com/InditexTech/weavejs/compare/0.22.0...0.22.1 - [0.22.0]: https://github.com/InditexTech/weavejs/compare/0.21.2...0.22.0 - [0.21.2]: https://github.com/InditexTech/weavejs/compare/0.21.1...0.21.2 - [0.21.1]: https://github.com/InditexTech/weavejs/compare/0.21.0...0.21.1 - [0.21.0]: https://github.com/InditexTech/weavejs/compare/0.20.4...0.21.0 - [0.20.4]: https://github.com/InditexTech/weavejs/compare/0.20.3...0.20.4 - [0.20.3]: https://github.com/InditexTech/weavejs/compare/0.20.2...0.20.3 - [0.20.2]: https://github.com/InditexTech/weavejs/compare/0.20.1...0.20.2 - [0.20.1]: https://github.com/InditexTech/weavejs/compare/0.20.0...0.20.1 - [0.20.0]: https://github.com/InditexTech/weavejs/compare/0.19.0...0.20.0 - [0.19.0]: https://github.com/InditexTech/weavejs/compare/0.18.0...0.19.0 - [0.18.0]: https://github.com/InditexTech/weavejs/compare/0.17.0...0.18.0 - [0.17.0]: https://github.com/InditexTech/weavejs/compare/0.16.2...0.17.0 - [0.16.2]: https://github.com/InditexTech/weavejs/compare/0.16.1...0.16.2 - [0.16.1]: https://github.com/InditexTech/weavejs/compare/0.16.0...0.16.1 - [0.16.0]: https://github.com/InditexTech/weavejs/compare/0.15.0...0.16.0 - [0.15.0]: https://github.com/InditexTech/weavejs/compare/0.14.3...0.15.0 - [0.14.3]: https://github.com/InditexTech/weavejs/compare/0.14.2...0.14.3 - [0.14.2]: https://github.com/InditexTech/weavejs/compare/0.14.1...0.14.2 - [0.14.1]: https://github.com/InditexTech/weavejs/compare/0.14.0...0.14.1 - [0.14.0]: https://github.com/InditexTech/weavejs/compare/0.13.1...0.14.0 - [0.13.1]: https://github.com/InditexTech/weavejs/compare/0.13.0...0.13.1 - [0.13.0]: https://github.com/InditexTech/weavejs/compare/0.12.1...0.13.0 - [0.12.1]: https://github.com/InditexTech/weavejs/compare/0.12.0...0.12.1 - [0.12.0]: https://github.com/InditexTech/weavejs/compare/0.11.0...0.12.0 - [0.11.0]: https://github.com/InditexTech/weavejs/compare/0.10.3...0.11.0 - [0.10.3]: https://github.com/InditexTech/weavejs/compare/0.10.2...0.10.3 - [0.10.2]: https://github.com/InditexTech/weavejs/compare/0.10.1...0.10.2 - [0.10.1]: https://github.com/InditexTech/weavejs/compare/0.10.0...0.10.1 - [0.10.0]: https://github.com/InditexTech/weavejs/compare/0.9.3...0.10.0 - [0.9.3]: https://github.com/InditexTech/weavejs/compare/0.9.2...0.9.3 - [0.9.2]: https://github.com/InditexTech/weavejs/compare/0.9.1...0.9.2 - [0.9.1]: https://github.com/InditexTech/weavejs/compare/0.9.0...0.9.1 - [0.9.0]: https://github.com/InditexTech/weavejs/compare/0.8.0...0.9.0 - [0.8.0]: https://github.com/InditexTech/weavejs/compare/0.7.1...0.8.0 - [0.7.1]: https://github.com/InditexTech/weavejs/compare/0.7.0...0.7.1 - [0.7.0]: https://github.com/InditexTech/weavejs/compare/0.6.0...0.7.0 - [0.6.0]: https://github.com/InditexTech/weavejs/compare/0.5.0...0.6.0 - [0.5.0]: https://github.com/InditexTech/weavejs/compare/0.4.0...0.5.0 - [0.4.0]: https://github.com/InditexTech/weavejs/compare/0.3.3...0.4.0 - [0.3.3]: https://github.com/InditexTech/weavejs/compare/0.3.2...0.3.3 - [0.3.2]: https://github.com/InditexTech/weavejs/compare/0.3.1...0.3.2 - [0.3.1]: https://github.com/InditexTech/weavejs/compare/0.3.0...0.3.1 - [0.3.0]: https://github.com/InditexTech/weavejs/compare/0.2.1...0.3.0 - [0.2.1]: https://github.com/InditexTech/weavejs/compare/0.2.0...0.2.1 - [0.2.0]: https://github.com/InditexTech/weavejs/compare/0.1.1...0.2.0 - [0.1.1]: https://github.com/InditexTech/weavejs/compare/0.1.0...0.1.1 - [0.1.0]: https://github.com/InditexTech/weavejs/releases/tag/0.1.0 diff --git a/docs/content/docs/main/changelog/index.mdx b/docs/content/docs/main/changelog/index.mdx index 7bcee9dea..e7421e997 100644 --- a/docs/content/docs/main/changelog/index.mdx +++ b/docs/content/docs/main/changelog/index.mdx @@ -5,6 +5,7 @@ description: Check out the latest changes to Weave.js. ## Pre-release versions +- [**0.74.0**](/docs/main/changelog/prerelease/0.74.0) - [**0.73.1**](/docs/main/changelog/prerelease/0.73.1) - [**0.73.0**](/docs/main/changelog/prerelease/0.73.0) - [**0.72.0**](/docs/main/changelog/prerelease/0.72.0) diff --git a/docs/content/docs/main/changelog/prerelease/0.74.0.mdx b/docs/content/docs/main/changelog/prerelease/0.74.0.mdx new file mode 100644 index 000000000..87b918f6b --- /dev/null +++ b/docs/content/docs/main/changelog/prerelease/0.74.0.mdx @@ -0,0 +1,12 @@ +--- +title: v0.74.0 +description: Allow to clone nodes with click + alt and drag +--- + +## Metadata + +- **Release date**: 2025-10-09 + +### Added + +- [#693](https://github.com/InditexTech/weavejs/issues/693) Clone elements on ALT + Click & Drag diff --git a/docs/content/docs/main/changelog/prerelease/meta.json b/docs/content/docs/main/changelog/prerelease/meta.json index d2c1cf31b..bc64b9990 100644 --- a/docs/content/docs/main/changelog/prerelease/meta.json +++ b/docs/content/docs/main/changelog/prerelease/meta.json @@ -2,6 +2,7 @@ "title": "Prerelease versions", "description": "Detailed changelog for Weave.js pre-release versions", "pages": [ + "0.74.0", "0.73.1", "0.73.0", "0.72.1", From 66dd3763491954731468309cf14cf0554e652843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 13:31:58 +0200 Subject: [PATCH 5/7] chore: update docs --- .../api-reference/plugins/stage-panning.mdx | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/content/docs/sdk/api-reference/plugins/stage-panning.mdx b/docs/content/docs/sdk/api-reference/plugins/stage-panning.mdx index 24dfd903d..a55a80921 100644 --- a/docs/content/docs/sdk/api-reference/plugins/stage-panning.mdx +++ b/docs/content/docs/sdk/api-reference/plugins/stage-panning.mdx @@ -30,5 +30,50 @@ import { WeaveStagePanningPlugin } from "@inditextech/weave-sdk"; ## Instantiation ```ts -new WeaveStagePanningPlugin(); +new WeaveStagePanningPlugin(params?: WeaveStagePanningPluginParams); ``` + +## TypeScript types + +```ts +type WeaveStagePanningPluginParams = { + config?: Partial; +}; + +type WeaveStagePanningPluginConfig = { + edgePanOffset: number; + edgePanSpeed: number; +}; +``` + +## Parameters + +For `WeaveStagePanningPluginParams`: + + + +For `WeaveStagePanningPluginConfig`: + + From 7bbb9c95152959216e9b9dfd331688ce77b3b58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 13:34:52 +0200 Subject: [PATCH 6/7] chore: make stage panning edge not scale aware --- .../sdk/src/plugins/stage-panning/stage-panning.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts index a6d3775a6..89458b82b 100644 --- a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts +++ b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts @@ -287,22 +287,22 @@ export class WeaveStagePanningPlugin extends WeavePlugin { if (!pos) return; - const isNearLeft = pos.x < offset; + const isNearLeft = pos.x < offset / stage.scaleX(); if (isNearLeft) { e.target.x(e.target.x() - speed / stage.scaleX()); } - const isNearRight = pos.x > stage.width() - offset; + const isNearRight = pos.x > stage.width() - offset / stage.scaleX(); if (isNearRight) { e.target.x(e.target.x() + speed / stage.scaleX()); } - const isNearTop = pos.y < offset; + const isNearTop = pos.y < offset / stage.scaleY(); if (isNearTop) { e.target.y(e.target.y() - speed / stage.scaleX()); } - const isNearBottom = pos.y > stage.height() - offset; + const isNearBottom = pos.y > stage.height() - offset / stage.scaleY(); if (isNearBottom) { e.target.y(e.target.y() + speed / stage.scaleX()); } From 6643fe079a09046e9fe9938d60c398aca541dd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Thu, 9 Oct 2025 13:39:06 +0200 Subject: [PATCH 7/7] chore: fix sonar qube issues --- code/packages/sdk/src/plugins/stage-panning/stage-panning.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts index 89458b82b..911d6b74b 100644 --- a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts +++ b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts @@ -273,8 +273,7 @@ export class WeaveStagePanningPlugin extends WeavePlugin { const duration = 1000 / 60; if ( - typeof this.targetScrollIntervals[e.target.getAttrs().id ?? ''] !== - 'undefined' + this.targetScrollIntervals[e.target.getAttrs().id ?? ''] !== undefined ) { return; } @@ -312,7 +311,7 @@ export class WeaveStagePanningPlugin extends WeavePlugin { duration ); - if (typeof this.stageScrollInterval !== 'undefined') { + if (this.stageScrollInterval !== undefined) { return; }