diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a37c43ec24d..41357b9876c 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -60,6 +60,10 @@ This was first fixed in 2022.3.0 but broken in 2024.3.0; it is now fixed again. - https://github.com/eclipse-sirius/sirius-web/issues/4280[#4280] [diagram] Fix direct edit with F2 when the palette is opened - https://github.com/eclipse-sirius/sirius-web/issues/4302[#4302] [diagram] Fix edges label flashing - https://github.com/eclipse-sirius/sirius-web/issues/4312[#4312] [sirius-web] The _Details_ view dit not react to its input is deselected, showing potentially stale information +- https://github.com/eclipse-sirius/sirius-web/issues/4301[#4301] [vs-code] Fix VSCode extension regressions. ++ Fix Explorer ++ Contribute the Ellipse node ++ Contribute the Selection Dialog === New Features diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index 2966eb56476..72098a6f0fc 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -78,7 +78,7 @@ import { ProjectDownloadMenuItemExtension } from '../views/project-browser/list- import { ProjectSettingTabContribution } from '../views/project-settings/ProjectSettingsView.types'; import { projectSettingsTabExtensionPoint } from '../views/project-settings/ProjectSettingsViewExtensionPoints'; import { ProjectImagesSettings } from '../views/project-settings/images/ProjectImagesSettings'; -import { ellipseNodeStyleDocumentTransform } from './ElipseNodeDocumentTransform'; +import { ellipseNodeStyleDocumentTransform } from './EllipseNodeDocumentTransform'; import { referenceWidgetDocumentTransform } from './ReferenceWidgetDocumentTransform'; const getType = (representation: RepresentationMetadata): string | null => { diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/ElipseNodeDocumentTransform.ts b/packages/sirius-web/frontend/sirius-web-application/src/extension/EllipseNodeDocumentTransform.ts similarity index 100% rename from packages/sirius-web/frontend/sirius-web-application/src/extension/ElipseNodeDocumentTransform.ts rename to packages/sirius-web/frontend/sirius-web-application/src/extension/EllipseNodeDocumentTransform.ts diff --git a/packages/sirius-web/frontend/sirius-web-application/src/index.ts b/packages/sirius-web/frontend/sirius-web-application/src/index.ts index b786f74cb7a..3cc0c24b811 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/index.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/index.ts @@ -16,6 +16,7 @@ export type { SiriusWebApplicationProps } from './application/SiriusWebApplicati export { DiagramRepresentationConfiguration } from './diagrams/DiagramRepresentationConfiguration'; export type { NodeTypeRegistry } from './diagrams/DiagramRepresentationConfiguration.types'; export { DefaultExtensionRegistryMergeStrategy } from './extension/DefaultExtensionRegistryMergeStrategy'; +export { ellipseNodeStyleDocumentTransform } from './extension/EllipseNodeDocumentTransform'; export { referenceWidgetDocumentTransform } from './extension/ReferenceWidgetDocumentTransform'; export type { FooterProps } from './footer/Footer.types'; export { footerExtensionPoint } from './footer/FooterExtensionPoints'; diff --git a/vscode-extension/src/data/ProjectData.ts b/vscode-extension/src/data/ProjectData.ts index c038ef4e33f..2525ccafedb 100644 --- a/vscode-extension/src/data/ProjectData.ts +++ b/vscode-extension/src/data/ProjectData.ts @@ -42,7 +42,7 @@ export class ProjectData { } fetchModels(serverAddress: string, cookie: string, expandedItems: string[]): ModelData[] { - const graphQLSubscription = getTreeEventSubscription(8); + const graphQLSubscription = getTreeEventSubscription(8, 'explorerEvent', 'ExplorerEventInput'); const headers = { Cookie: cookie, }; @@ -68,10 +68,10 @@ export class ProjectData { variables: { input: { id: uuid(), - treeId: 'explorer://', + representationId: `explorer://?treeDescriptionId=explorer_tree_description&expandedIds=[${expandedItems + .map(encodeURIComponent) + .join(',')}]&activeFilterIds=[]`, editingContextId: this.id, - expanded: expandedItems, - activeFilterIds: [], }, }, }; @@ -82,7 +82,7 @@ export class ProjectData { client.onmessage = (message) => { if (message?.data) { const response = JSON.parse(message.data as string); - const documents = response.payload?.data?.treeEvent?.tree?.children; + const documents = response.payload?.data?.explorerEvent?.tree?.children; if (response.id === this.subscriptionTreeEventId && documents) { this.modelsData = this.buildModelData(documents); commands.executeCommand('siriusweb.displayProjectContents', this.serverId, this.id); diff --git a/vscode-extension/src/data/getTreeEventSubscription.ts b/vscode-extension/src/data/getTreeEventSubscription.ts index a9a616158b5..b258ea7b1a4 100644 --- a/vscode-extension/src/data/getTreeEventSubscription.ts +++ b/vscode-extension/src/data/getTreeEventSubscription.ts @@ -11,11 +11,11 @@ * Obeo - initial API and implementation *******************************************************************************/ -export const getTreeEventSubscription = (depth: number) => { +export const getTreeEventSubscription = (depth: number, eventType: string, eventInputType: string) => { const treeChildren = recursiveGetChildren(depth); const subscription = ` -subscription treeEvent($input: TreeEventInput!) { - treeEvent(input: $input) { +subscription ${eventType}($input: ${eventInputType}!) { + ${eventType}(input: $input) { __typename ... on TreeRefreshedEventPayload { id diff --git a/vscode-extension/src/view/app/App.tsx b/vscode-extension/src/view/app/App.tsx index 7a8d90fdf01..59c148d2025 100644 --- a/vscode-extension/src/view/app/App.tsx +++ b/vscode-extension/src/view/app/App.tsx @@ -16,14 +16,23 @@ import { Selection, SelectionContextProvider, } from '@eclipse-sirius/sirius-components-core'; -import { DiagramRepresentation } from '@eclipse-sirius/sirius-components-diagrams'; +import { + DiagramRepresentation, + NodeTypeContext, + NodeTypeContextValue, + NodeTypeContribution, +} from '@eclipse-sirius/sirius-components-diagrams'; import { FormDescriptionEditorRepresentation } from '@eclipse-sirius/sirius-components-formdescriptioneditors'; import { FormRepresentation } from '@eclipse-sirius/sirius-components-forms'; import { DetailsView } from '@eclipse-sirius/sirius-web-application'; import { Theme, ThemeProvider } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import './reset.css'; +import { EllipseNode } from './nodes/EllipseNode'; +import { EllipseNodeConverter } from './nodes/EllipseNodeConverter'; +import { EllipseNodeLayoutHandler } from './nodes/EllipseNodeLayoutHandler'; import { siriusWebTheme as defaultTheme } from './theme/siriusWebTheme'; + +import './reset.css'; import './variables.css'; interface AppState { @@ -146,14 +155,23 @@ export const App = ({ }; component = ; } + + const nodeTypeRegistryValue: NodeTypeContextValue = { + nodeLayoutHandlers: [new EllipseNodeLayoutHandler()], + nodeConverters: [new EllipseNodeConverter()], + nodeTypeContributions: [], + }; + return ( - - - {state.editingContextId && state.authenticate ? {component} : null} - + + + + {state.editingContextId && state.authenticate ? {component} : null} + + diff --git a/vscode-extension/src/view/app/index.tsx b/vscode-extension/src/view/app/index.tsx index 04e4497b802..670a5e6eff2 100644 --- a/vscode-extension/src/view/app/index.tsx +++ b/vscode-extension/src/view/app/index.tsx @@ -15,14 +15,23 @@ import { ApolloClient, ApolloProvider, DefaultOptions, HttpLink, InMemoryCache, import { WebSocketLink } from '@apollo/client/link/ws'; import { getMainDefinition } from '@apollo/client/utilities'; import { ExtensionProvider, ServerContext } from '@eclipse-sirius/sirius-components-core'; -import { referenceWidgetDocumentTransform } from '@eclipse-sirius/sirius-web-application'; +import { + DiagramDialogContribution, + diagramDialogContributionExtensionPoint, +} from '@eclipse-sirius/sirius-components-diagrams'; +import { SelectionDialog } from '@eclipse-sirius/sirius-components-selection'; +import { + ellipseNodeStyleDocumentTransform, + referenceWidgetDocumentTransform, +} from '@eclipse-sirius/sirius-web-application'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { App } from './App'; -import './index.css'; import { defaultExtensionRegistry } from './registry/DefaultExtensionRegistry'; import { ToastProvider } from './toast/ToastProvider'; +import './index.css'; + declare global { interface Window { acquireVsCodeApi(): any; @@ -81,10 +90,27 @@ const ApolloGraphQLClient = new ApolloClient({ link: splitLink, cache: new InMemoryCache({ addTypename: true }), defaultOptions, - documentTransform: referenceWidgetDocumentTransform, + documentTransform: referenceWidgetDocumentTransform.concat(ellipseNodeStyleDocumentTransform), +}); + +const diagramDialogContributions: DiagramDialogContribution[] = [ + { + canHandle: (dialogDescriptionId: string) => { + return dialogDescriptionId.startsWith('siriusComponents://selectionDialogDescription'); + }, + component: SelectionDialog, + }, +]; + +defaultExtensionRegistry.putData(diagramDialogContributionExtensionPoint, { + identifier: `siriusweb_${diagramDialogContributionExtensionPoint.identifier}`, + data: diagramDialogContributions, }); -ReactDOM.render( +const container = document.getElementById('root'); +const root = createRoot(container!); + +root.render( @@ -101,6 +127,5 @@ ReactDOM.render( - , - document.getElementById('root') + ); diff --git a/vscode-extension/src/view/app/nodes/EllipseNode.tsx b/vscode-extension/src/view/app/nodes/EllipseNode.tsx new file mode 100644 index 00000000000..dc1971b593d --- /dev/null +++ b/vscode-extension/src/view/app/nodes/EllipseNode.tsx @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2023, 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; +import { + ConnectionCreationHandles, + ConnectionHandles, + ConnectionTargetHandle, + DiagramContext, + DiagramContextValue, + DiagramElementPalette, + Label, + useConnectorNodeStyle, + useDrop, + useDropNodeStyle, + useRefreshConnectionHandles, +} from '@eclipse-sirius/sirius-components-diagrams'; +import { Theme, useTheme } from '@mui/material/styles'; +import { Node, NodeProps, NodeResizer } from '@xyflow/react'; +import React, { memo, useContext } from 'react'; +import { EllipseNodeData, NodeComponentsMap } from './EllipseNode.types'; + +const ellipseNodeStyle = ( + theme: Theme, + style: React.CSSProperties, + selected: boolean, + hovered: boolean, + faded: boolean +): React.CSSProperties => { + const ellipseNodeStyle: React.CSSProperties = { + display: 'flex', + padding: '8px', + width: '100%', + height: '100%', + borderRadius: '50%', + border: 'black solid 1px', + opacity: faded ? '0.4' : '', + ...style, + background: getCSSColor(String(style.background), theme), + }; + + if (selected || hovered) { + ellipseNodeStyle.outline = `${theme.palette.selected} solid 1px`; + } + + return ellipseNodeStyle; +}; + +const resizeLineStyle = (theme: Theme): React.CSSProperties => { + return { borderWidth: theme.spacing(0.15) }; +}; + +const resizeHandleStyle = (theme: Theme): React.CSSProperties => { + return { + width: theme.spacing(1), + height: theme.spacing(1), + borderRadius: '100%', + }; +}; + +export const EllipseNode: NodeComponentsMap['ellipseNode'] = memo( + ({ data, id, selected, dragging }: NodeProps>) => { + const { readOnly } = useContext(DiagramContext); + const theme = useTheme(); + const { onDrop, onDragOver } = useDrop(); + const { style: connectionFeedbackStyle } = useConnectorNodeStyle(id, data.nodeDescription.id); + const { style: dropFeedbackStyle } = useDropNodeStyle(data.isDropNodeTarget, data.isDropNodeCandidate, dragging); + + const handleOnDrop = (event: React.DragEvent) => { + onDrop(event, id); + }; + + useRefreshConnectionHandles(id, data.connectionHandles); + + return ( + <> + {data.nodeDescription?.userResizable !== 'NONE' && !readOnly ? ( + !data.isBorderNode} + keepAspectRatio={data.nodeDescription?.keepAspectRatio} + /> + ) : null} + + {data.insideLabel ? : null} + {!!selected ? ( + + ) : null} + {!!selected ? : null} + + + + > + ); + } +); diff --git a/vscode-extension/src/view/app/nodes/EllipseNode.types.ts b/vscode-extension/src/view/app/nodes/EllipseNode.types.ts new file mode 100644 index 00000000000..665dc12265b --- /dev/null +++ b/vscode-extension/src/view/app/nodes/EllipseNode.types.ts @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2023, 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { GQLNodeStyle, NodeData } from '@eclipse-sirius/sirius-components-diagrams'; +import { Node, NodeProps } from '@xyflow/react'; +import { FC } from 'react'; + +export interface EllipseNodeData extends NodeData {} + +export interface GQLEllipseNodeStyle extends GQLNodeStyle { + background: string; + borderColor: string; + borderStyle: string; + borderSize: string; +} + +export interface NodeDataMap { + ellipseNode: EllipseNodeData; +} +export type NodeComponentsMap = { + [K in keyof NodeDataMap]: FC>>; +}; diff --git a/vscode-extension/src/view/app/nodes/EllipseNodeConverter.ts b/vscode-extension/src/view/app/nodes/EllipseNodeConverter.ts new file mode 100644 index 00000000000..b1999e288ea --- /dev/null +++ b/vscode-extension/src/view/app/nodes/EllipseNodeConverter.ts @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2023, 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { + BorderNodePosition, + ConnectionHandle, + GQLDiagram, + GQLDiagramDescription, + GQLEdge, + GQLNode, + GQLNodeDescription, + GQLNodeLayoutData, + GQLNodeStyle, + GQLViewModifier, + IConvertEngine, + INodeConverter, + convertHandles, + convertInsideLabel, + convertLineStyle, + convertOutsideLabels, + defaultHeight, + defaultWidth, + isListLayoutStrategy, +} from '@eclipse-sirius/sirius-components-diagrams'; +import { Node, XYPosition } from '@xyflow/react'; +import { EllipseNodeData, GQLEllipseNodeStyle } from './EllipseNode.types'; + +const defaultPosition: XYPosition = { x: 0, y: 0 }; + +const toEllipseNode = ( + gqlDiagram: GQLDiagram, + gqlNode: GQLNode, + gqlParentNode: GQLNode | null, + nodeDescription: GQLNodeDescription, + isBorderNode: boolean, + gqlEdges: GQLEdge[] +): Node => { + const { + targetObjectId, + targetObjectLabel, + targetObjectKind, + descriptionId, + id, + insideLabel, + outsideLabels, + state, + pinned, + style, + labelEditable, + } = gqlNode; + + const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges); + const gqlNodeLayoutData: GQLNodeLayoutData | undefined = gqlDiagram.layoutData.nodeLayoutData.find( + (nodeLayoutData) => nodeLayoutData.id === id + ); + const isNew = gqlNodeLayoutData === undefined; + const resizedByUser = gqlNodeLayoutData?.resizedByUser ?? false; + + const data: EllipseNodeData = { + targetObjectId, + targetObjectLabel, + targetObjectKind, + descriptionId, + style: { + display: 'flex', + background: style.background, + borderColor: style.borderColor, + borderWidth: style.borderSize, + borderStyle: convertLineStyle(style.borderStyle), + }, + insideLabel: null, + outsideLabels: convertOutsideLabels(outsideLabels), + faded: state === GQLViewModifier.Faded, + pinned, + isBorderNode: isBorderNode, + nodeDescription, + defaultWidth: gqlNode.defaultWidth, + defaultHeight: gqlNode.defaultHeight, + borderNodePosition: isBorderNode ? BorderNodePosition.EAST : null, + connectionHandles, + labelEditable, + isNew, + resizedByUser, + isListChild: isListLayoutStrategy(gqlParentNode?.childrenLayoutStrategy), + isDropNodeTarget: false, + isDropNodeCandidate: false, + isHovered: false, + }; + + data.insideLabel = convertInsideLabel( + insideLabel, + data, + `${style.borderSize}px ${style.borderStyle} ${style.borderColor}` + ); + + const node: Node = { + id, + type: 'ellipseNode', + data, + position: defaultPosition, + hidden: gqlNode.state === GQLViewModifier.Hidden, + }; + + if (gqlParentNode) { + node.parentId = gqlParentNode.id; + } + + const nodeLayoutData = gqlDiagram.layoutData.nodeLayoutData.filter((data) => data.id === id)[0]; + if (nodeLayoutData) { + const { + position, + size: { height, width }, + } = nodeLayoutData; + node.position = position; + node.height = height; + node.width = width; + node.style = { + ...node.style, + width: `${node.width}px`, + height: `${node.height}px`, + }; + } else { + node.height = data.defaultHeight ?? defaultHeight; + node.width = data.defaultWidth ?? defaultWidth; + } + + return node; +}; + +export class EllipseNodeConverter implements INodeConverter { + canHandle(gqlNode: GQLNode) { + return gqlNode.style.__typename === 'EllipseNodeStyle'; + } + + handle( + convertEngine: IConvertEngine, + gqlDiagram: GQLDiagram, + gqlNode: GQLNode, + gqlEdges: GQLEdge[], + parentNode: GQLNode | null, + isBorderNode: boolean, + nodes: Node[], + diagramDescription: GQLDiagramDescription, + nodeDescriptions: GQLNodeDescription[] + ) { + const nodeDescription = nodeDescriptions.find((description) => description.id === gqlNode.descriptionId); + if (nodeDescription) { + nodes.push(toEllipseNode(gqlDiagram, gqlNode, parentNode, nodeDescription, isBorderNode, gqlEdges)); + } + + const borderNodeDescriptions: GQLNodeDescription[] = (nodeDescription?.borderNodeDescriptionIds ?? []).flatMap( + (nodeDescriptionId) => + diagramDescription.nodeDescriptions.filter((nodeDescription) => nodeDescription.id === nodeDescriptionId) + ); + const childNodeDescriptions: GQLNodeDescription[] = (nodeDescription?.childNodeDescriptionIds ?? []).flatMap( + (nodeDescriptionId) => + diagramDescription.nodeDescriptions.filter((nodeDescription) => nodeDescription.id === nodeDescriptionId) + ); + + convertEngine.convertNodes( + gqlDiagram, + gqlNode.borderNodes ?? [], + gqlNode, + nodes, + diagramDescription, + borderNodeDescriptions + ); + convertEngine.convertNodes( + gqlDiagram, + gqlNode.childNodes ?? [], + gqlNode, + nodes, + diagramDescription, + childNodeDescriptions + ); + } +} diff --git a/vscode-extension/src/view/app/nodes/EllipseNodeLayoutHandler.ts b/vscode-extension/src/view/app/nodes/EllipseNodeLayoutHandler.ts new file mode 100644 index 00000000000..713d6175553 --- /dev/null +++ b/vscode-extension/src/view/app/nodes/EllipseNodeLayoutHandler.ts @@ -0,0 +1,280 @@ +/******************************************************************************* + * Copyright (c) 2023, 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { + Diagram, + DiagramNodeType, + ForcedDimensions, + ILayoutEngine, + INodeLayoutHandler, + NodeData, + applyRatioOnNewNodeSizeValue, + computeNodesBox, + computePreviousPosition, + computePreviousSize, + findNodeIndex, + getBorderNodeExtent, + getChildNodePosition, + getDefaultOrMinHeight, + getDefaultOrMinWidth, + getEastBorderNodeFootprintHeight, + getHeaderHeightFootprint, + getInsideLabelWidthConstraint, + getNorthBorderNodeFootprintWidth, + getSouthBorderNodeFootprintWidth, + getWestBorderNodeFootprintHeight, + setBorderNodesPosition, +} from '@eclipse-sirius/sirius-components-diagrams'; +import { Dimensions, Node, Position, XYPosition } from '@xyflow/react'; +import { NodeHandle } from '@xyflow/system'; + +const borderNodeOffset = 5; + +const findBorderNodePosition = ( + borderNodePosition: XYPosition | undefined, + parentNode: Node | undefined +): number | null => { + if (borderNodePosition && parentNode?.width && parentNode.height) { + if (borderNodePosition.y < parentNode.height / 2) { + return borderNodePosition.x < parentNode.width / 2 ? 0 : 1; + } else { + return borderNodePosition.x < parentNode.width / 2 ? 2 : 3; + } + } + return null; +}; + +export class EllipseNodeLayoutHandler implements INodeLayoutHandler { + canHandle(node: Node) { + return node.type === 'ellipseNode'; + } + + handle( + layoutEngine: ILayoutEngine, + previousDiagram: Diagram | null, + node: Node, + visibleNodes: Node[], + directChildren: Node[], + newlyAddedNode: Node | undefined, + forceDimensions?: ForcedDimensions + ) { + layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, newlyAddedNode); + + const nodeIndex = findNodeIndex(visibleNodes, node.id); + const nodeElement = document.getElementById(`${node.id}-ellipseNode-${nodeIndex}`)?.children[0]; + const borderWidth = nodeElement ? parseFloat(window.getComputedStyle(nodeElement).borderWidth) : 0; + const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`); + + const borderNodes = directChildren.filter((node) => node.data.isBorderNode); + const directNodesChildren = directChildren.filter((child) => !child.data.isBorderNode); + + // Update children position to be under the label and at the right padding. + directNodesChildren.forEach((child, index) => { + const previousNode = (previousDiagram?.nodes ?? []).find((previouseNode) => previouseNode.id === child.id); + const previousPosition = computePreviousPosition(previousNode, child); + const createdNode = newlyAddedNode?.id === child.id ? newlyAddedNode : undefined; + const headerHeightFootprint = labelElement ? getHeaderHeightFootprint(labelElement, null, null) : 0; + + if (!!createdNode) { + child.position = createdNode.position; + if (child.position.y < borderWidth + headerHeightFootprint) { + child.position = { ...child.position, y: borderWidth + headerHeightFootprint }; + } + } else if (previousPosition) { + child.position = previousPosition; + if (child.position.y < borderWidth + headerHeightFootprint) { + child.position = { ...child.position, y: borderWidth + headerHeightFootprint }; + } + if (child.position.x < borderWidth) { + child.position = { ...child.position, x: borderWidth }; + } + } else { + child.position = child.position = getChildNodePosition(visibleNodes, child, headerHeightFootprint, borderWidth); + const previousSibling = directNodesChildren[index - 1]; + if (previousSibling) { + child.position = getChildNodePosition( + visibleNodes, + child, + headerHeightFootprint, + borderWidth, + previousSibling + ); + } + } + }); + + // Update node to layout size + // WARN: We suppose label are always on top of children (that wrong) + const childrenContentBox = computeNodesBox(visibleNodes, directNodesChildren); // WARN: The current content box algorithm does not take the margin of direct children (it should) + const directChildrenAwareNodeWidth = childrenContentBox.x + childrenContentBox.width; + const northBorderNodeFootprintWidth = getNorthBorderNodeFootprintWidth(visibleNodes, borderNodes, previousDiagram); + const southBorderNodeFootprintWidth = getSouthBorderNodeFootprintWidth(visibleNodes, borderNodes, previousDiagram); + const labelOnlyWidth = getInsideLabelWidthConstraint(node.data.insideLabel, labelElement); + + const nodeMinComputeWidth = + Math.max( + directChildrenAwareNodeWidth, + labelOnlyWidth, + northBorderNodeFootprintWidth, + southBorderNodeFootprintWidth + ) + + borderWidth * 2; + + // WARN: the label is not used for the height because children are already position under the label + const directChildrenAwareNodeHeight = childrenContentBox.y + childrenContentBox.height; + const eastBorderNodeFootprintHeight = getEastBorderNodeFootprintHeight(visibleNodes, borderNodes, previousDiagram); + const westBorderNodeFootprintHeight = getWestBorderNodeFootprintHeight(visibleNodes, borderNodes, previousDiagram); + + const nodeMinComputeHeight = + Math.max(directChildrenAwareNodeHeight, eastBorderNodeFootprintHeight, westBorderNodeFootprintHeight) + + borderWidth * 2; + + const nodeWith = forceDimensions?.width ?? getDefaultOrMinWidth(nodeMinComputeWidth, node); + const nodeHeight = getDefaultOrMinHeight(nodeMinComputeHeight, node); + + const previousNode = (previousDiagram?.nodes ?? []).find((previouseNode) => previouseNode.id === node.id); + const previousDimensions = computePreviousSize(previousNode, node); + if (node.data.resizedByUser) { + if (nodeMinComputeWidth > previousDimensions.width) { + node.width = nodeMinComputeWidth; + } else { + node.width = previousDimensions.width; + } + if (nodeMinComputeHeight > previousDimensions.height) { + node.height = nodeMinComputeHeight; + } else { + node.height = previousDimensions.height; + } + } else { + node.width = nodeWith; + node.height = nodeHeight; + } + + if (node.data.nodeDescription?.keepAspectRatio) { + applyRatioOnNewNodeSizeValue(node); + } + + // Update border nodes positions + borderNodes.forEach((borderNode) => { + borderNode.extent = getBorderNodeExtent(node, borderNode); + }); + setBorderNodesPosition(borderNodes, node, previousDiagram, this.calculateCustomNodeBorderNodePosition); + } + + calculateCustomNodeEdgeHandlePosition( + node: Node, + handlePosition: Position, + handle: NodeHandle + ): XYPosition { + let offsetX = handle?.width ?? 0 / 2; + let offsetY = handle?.height ?? 0 / 2; + const nodeWidth: number = node.width ?? 0; + const nodeHeight: number = node.height ?? 0; + const a: number = nodeWidth / 2; + const b: number = nodeHeight / 2; + + let realY: number = handle.y; + let realX: number = handle.x; + switch (handlePosition) { + case Position.Left: + realX = Math.sqrt((1 - Math.pow(handle.y + offsetY - b, 2) / Math.pow(b, 2)) * Math.pow(a, 2)) + a; + realX = nodeWidth - realX; + break; + case Position.Right: + realX = Math.sqrt((1 - Math.pow(handle.y + offsetY - b, 2) / Math.pow(b, 2)) * Math.pow(a, 2)) + a; + offsetX = -offsetX; + break; + case Position.Top: + realY = Math.sqrt((1 - Math.pow(handle.x + offsetX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + realY = nodeHeight - realY; + break; + case Position.Bottom: + realY = Math.sqrt((1 - Math.pow(handle.x + offsetX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + offsetY = -offsetY; + break; + } + + return { + x: realX + offsetX, + y: realY + offsetY, + }; + } + + calculateCustomNodeBorderNodePosition( + parentNode: Node, + borderNode: XYPosition & Dimensions, + isDragging: boolean + ): XYPosition { + let offsetX: number = 0; + let offsetY: number = 0; + const parentNodeWidth: number = parentNode.width ?? 0; + const parentNodeHeight: number = parentNode.height ?? 0; + const a: number = parentNodeWidth / 2; + const b: number = parentNodeHeight / 2; + const pos: number | null = findBorderNodePosition(borderNode, parentNode); + let realY: number = borderNode.y; + let realX: number; + if (borderNode.x < 0) { + return { + x: -borderNode.width + borderNodeOffset, + y: b - borderNode.height / 2, + }; + } else if (borderNode.x >= parentNodeWidth - borderNodeOffset) { + return { + x: parentNodeWidth - borderNodeOffset, + y: b - borderNode.height / 2, + }; + } else { + realX = borderNode.x; + } + if (!isDragging) { + switch (pos) { + case 0: + case 2: + realX += borderNode.width; + break; + default: + break; + } + } + switch (pos) { + case 0: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + realY = parentNodeHeight - realY; + offsetY = -borderNode.height + borderNodeOffset; + offsetX = -borderNode.width; + break; + case 1: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + realY = parentNodeHeight - realY; + offsetY = -borderNode.height + borderNodeOffset; + break; + case 2: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + offsetY = -borderNodeOffset; + offsetX = -borderNode.width; + break; + case 3: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + offsetY = -borderNodeOffset; + break; + } + if (isNaN(realY)) { + realY = b; + } + + return { + x: realX + offsetX, + y: realY + offsetY, + }; + } +}