diff --git a/packages/klighd-core/src/views-common.ts b/packages/klighd-core/src/views-common.ts index b5370dcd..1563d1e5 100644 --- a/packages/klighd-core/src/views-common.ts +++ b/packages/klighd-core/src/views-common.ts @@ -570,6 +570,42 @@ export function transformationToSVGString(transformation: Transformation): strin } } +/** + * Reverses an array of transformations such that applying the transformation and its reverse counterpart will result in the identity transformation. + * @param transformations The transformations to reverse. + * @returns The reversed transformations. + */ +export function reverseTransformations(transformations: Transformation[]): Transformation[] { + return transformations.map(transformation => reverseTransformation(transformation)).reverse() +} + +/** + * Reverses a transformation such that applying the transformation and its reverse counterpart will result in the identity transformation. + * @param transformation The transformation to reverse. + * @returns The reversed transformation. + */ +export function reverseTransformation(transformation: Transformation): Transformation { + if (isTranslation(transformation)) { + return { + kind: 'translate', + x: -transformation.x, + y: -transformation.y + } as Translation + } else if (isRotation(transformation)) { + return { + kind: 'rotate', + angle: -transformation.angle, + x: transformation.x, + y: transformation.y + } as Rotation + } else { + return { + kind: 'scale', + factor: 1 / (transformation as Scale).factor + } as Scale + } +} + /** * Calculates the SVG transformation string that has to be applied to the SVG element. * @param bounds The bounds of the rendering. diff --git a/packages/klighd-core/src/views-rendering.tsx b/packages/klighd-core/src/views-rendering.tsx index 08bb9662..fc4fb0e2 100644 --- a/packages/klighd-core/src/views-rendering.tsx +++ b/packages/klighd-core/src/views-rendering.tsx @@ -28,7 +28,7 @@ import { K_ROUNDED_BENDS_POLYLINE, K_ROUNDED_RECTANGLE, K_SPLINE, K_TEXT, SKEdge, SKGraphElement, SKLabel, SKNode, VerticalAlignment } from './skgraph-models'; import { hasAction } from './skgraph-utils'; -import { BoundsAndTransformation, calculateX, findBoundsAndTransformationData, getKRendering, getPoints, isRotation, isTranslation, Rotation, Scale, Transformation, transformationToSVGString, Translation } from './views-common'; +import { BoundsAndTransformation, calculateX, findBoundsAndTransformationData, getKRendering, getPoints, isRotation, isTranslation, reverseTransformations, Rotation, Scale, Transformation, transformationToSVGString, Translation } from './views-common'; import { ColorStyles, DEFAULT_CLICKABLE_FILL, DEFAULT_FILL, getKStyles, getSvgColorStyle, getSvgColorStyles, getSvgLineStyles, getSvgShadowStyles, getSvgTextStyles, isInvisible, KStyles, LineStyles @@ -43,7 +43,30 @@ import { * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. */ -export function renderChildArea(rendering: KChildArea, parent: SKGraphElement, propagatedStyles: KStyles, context: SKGraphModelRenderer): VNode { +export function renderChildArea( + rendering: KChildArea, + parent: SKGraphElement, + boundsAndTransformation: BoundsAndTransformation, + context: SKGraphModelRenderer): VNode { + + // Sprotty expects the graph elements to always be relative to the parent element, while KLighD usually has the graph elements relative to the child area. + // Here we expect the graph elements to behave as Sprotty expects, thus requiring to reverse offset the transformation towards this child area again. + + // First, we need to find the total transformations that were applied to the child area. + const totalTansformation = [...context.titleStorage.getTransformations()] + + // Second, we need to find the transformation only applicable to the current child area. + const childAreaTransformation = boundsAndTransformation.transformation + + // Finally, we need to reverse the translation that was applied to every element hierarchially above of the child area. + // Note that this causes a little difference in what the coordinates are relative to the parent graph element, as entire child area rotations that are possible in KLighD are + // not possible in Sprotty. + totalTansformation.splice(totalTansformation.length - childAreaTransformation.length, childAreaTransformation.length) + const reverseTranslation: Transformation[] = reverseTransformations(totalTansformation.filter(transformation => isTranslation(transformation))) + + const gAttrs = { + ...(reverseTranslation.length !== 0 ? { transform: reverseTranslation.map(transformationToSVGString).join('') } : {}) + } if (parent.areChildAreaChildrenRendered) { console.error('This element contains multiple child areas, skipping this one.') return @@ -51,7 +74,7 @@ export function renderChildArea(rendering: KChildArea, parent: SKGraphElement, p // remember, that this parent's children are now already rendered parent.areChildAreaChildrenRendered = true - const element = + const element = {context.renderChildAreaChildren(parent)} @@ -166,7 +189,7 @@ export function renderRectangularShape( // eslint-disable-next-line case K_ELLIPSE: { element = - {...renderSVGEllipse(boundsAndTransformation.bounds, lineStyles, colorStyles, shadowStyles, styles.kShadow)} + {...renderSVGEllipse(boundsAndTransformation.bounds, styles.kLineWidth.lineWidth, lineStyles, colorStyles, shadowStyles, styles.kShadow)} {renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle)} break @@ -181,7 +204,7 @@ export function renderRectangularShape( const ry = (rendering as KRoundedRectangle).cornerHeight element = - {...renderSVGRect(boundsAndTransformation.bounds, rx, ry, lineStyles, colorStyles, shadowStyles, styles.kShadow)} + {...renderSVGRect(boundsAndTransformation.bounds, styles.kLineWidth.lineWidth, rx, ry, lineStyles, colorStyles, shadowStyles, styles.kShadow)} {renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle)} break @@ -567,6 +590,7 @@ export function renderWithShadow( * Renders a rectangle with all given information. * * @param bounds bounds data calculated for this rectangle. + * @param lineWidth width of the line to offset the rectangle's position and size by. * @param rx rx parameter of SVG rect * @param ry ry parameter of SVG rect * @param lineStyles style information for lines (stroke etc.) @@ -575,8 +599,8 @@ export function renderWithShadow( * @param kShadow general shadow information. * @returns An array of SVG resulting from this. Only multiple s if a simple shadow effect should be applied. */ -export function renderSVGRect(bounds: Bounds, rx: number, ry: number, lineStyles: LineStyles, colorStyles: ColorStyles, shadowStyles: string | undefined, kShadow: KShadow | undefined): VNode[] { - return renderWithShadow(kShadow, shadowStyles, renderSingleSVGRect, bounds, rx, ry, lineStyles, colorStyles) +export function renderSVGRect(bounds: Bounds, lineWidth: number, rx: number, ry: number, lineStyles: LineStyles, colorStyles: ColorStyles, shadowStyles: string | undefined, kShadow: KShadow | undefined): VNode[] { + return renderWithShadow(kShadow, shadowStyles, renderSingleSVGRect, bounds, rx, ry, lineWidth, lineStyles, colorStyles) } /** @@ -589,18 +613,27 @@ export function renderSVGRect(bounds: Bounds, rx: number, ry: number, lineStyles * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param kShadow shadow information. Controls what this method does. * @param bounds bounds data calculated for this rectangle. + * @param lineWidth width of the line to offset the rectangle's position and size by. * @param rx rx parameter of SVG rect * @param ry ry parameter of SVG rect * @param lineStyles style information for lines (stroke etc.) * @param colorStyles style information for color * @returns A single SVG . */ -export function renderSingleSVGRect(x: number | undefined, y: number | undefined, shadowStyles: string | undefined, kShadow: KShadow | undefined, bounds: Bounds, rx: number, ry: number, lineStyles: LineStyles, colorStyles: ColorStyles): VNode { +export function renderSingleSVGRect(x: number | undefined, y: number | undefined, shadowStyles: string | undefined, kShadow: KShadow | undefined, bounds: Bounds, rx: number, ry: number, lineWidth: number, lineStyles: LineStyles, colorStyles: ColorStyles): VNode { + // Offset the x/y by the lineWidth. + let theX: number | undefined = x ? x : 0 + theX += lineWidth / 2 + theX = theX === 0 ? undefined : theX + let theY: number | undefined = y ? y : 0 + theY += lineWidth / 2 + theY = theY === 0 ? undefined : theY + return s resulting from this. Only multiple s if a simple shadow effect should be applied. */ -export function renderSVGEllipse(bounds: Bounds, lineStyles: LineStyles, colorStyles: ColorStyles, shadowStyles: string | undefined, kShadow: KShadow | undefined): VNode[] { - return renderWithShadow(kShadow, shadowStyles, renderSingleSVGEllipse, bounds, lineStyles, colorStyles) +export function renderSVGEllipse(bounds: Bounds, lineWidth: number, lineStyles: LineStyles, colorStyles: ColorStyles, shadowStyles: string | undefined, kShadow: KShadow | undefined): VNode[] { + return renderWithShadow(kShadow, shadowStyles, renderSingleSVGEllipse, bounds, lineWidth, lineStyles, colorStyles) } /** @@ -736,17 +770,18 @@ export function renderSVGEllipse(bounds: Bounds, lineStyles: LineStyles, colorSt * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param kShadow shadow information. Controls what this method does. * @param bounds bounds data calculated for this ellipse. + * @param lineWidth width of the line to offset the ellipse's position and size by. * @param lineStyles style information for lines (stroke etc.) * @param colorStyles style information for color * @returns A single SVG . */ -export function renderSingleSVGEllipse(x: number | undefined, y: number | undefined, shadowStyles: string | undefined, kShadow: KShadow | undefined, bounds: Bounds, lineStyles: LineStyles, colorStyles: ColorStyles): VNode { +export function renderSingleSVGEllipse(x: number | undefined, y: number | undefined, shadowStyles: string | undefined, kShadow: KShadow | undefined, bounds: Bounds, lineWidth: number, lineStyles: LineStyles, colorStyles: ColorStyles): VNode { return