UNPKG

@kieler/klighd-core

Version:

Core KLighD diagram visualization with Sprotty

761 lines 68.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getJunctionPointRenderings = exports.renderKRendering = exports.getRendering = exports.renderSingleSVGLine = exports.renderSVGLine = exports.renderSingleSVGEllipse = exports.renderSVGEllipse = exports.renderSingleSVGArc = exports.renderSVGArc = exports.renderSingleSVGUse = exports.renderSingleSVGImage = exports.renderSVGUse = exports.renderSVGImage = exports.renderSVGImageDef = exports.renderSingleSVGRect = exports.renderSVGRect = exports.renderWithShadow = exports.renderError = exports.renderChildRenderings = exports.renderKText = exports.renderLine = exports.renderRectangularShape = exports.renderChildArea = void 0; const sprotty_1 = require("sprotty"); // eslint-disable-line @typescript-eslint/no-unused-vars const depth_map_1 = require("./depth-map"); const render_options_registry_1 = require("./options/render-options-registry"); const skgraph_models_1 = require("./skgraph-models"); const skgraph_utils_1 = require("./skgraph-utils"); const views_common_1 = require("./views-common"); const views_styles_1 = require("./views-styles"); /* global sessionStorage */ // ----------------------------- Functions for rendering different KRendering as VNodes in svg -------------------------------------------- /** * Translates a KChildArea rendering into an SVG rendering. * @param rendering The rendering. * @param parent The parent element. * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. */ function renderChildArea(rendering, parent, boundsAndTransformation, context) { // 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 = (0, views_common_1.reverseTransformations)(totalTansformation.filter((transformation) => (0, views_common_1.isTranslation)(transformation))); const gAttrs = Object.assign({}, (reverseTranslation.length !== 0 ? { transform: reverseTranslation.map(views_common_1.transformationToSVGString).join('') } : {})); if (parent.areChildAreaChildrenRendered) { console.error('This element contains multiple child areas, skipping this one.'); return (0, sprotty_1.svg)("g", null); } // remember, that this parent's children are now already rendered parent.areChildAreaChildrenRendered = true; const element = ((0, sprotty_1.svg)("g", Object.assign({}, gAttrs, { id: rendering.properties['klighd.lsp.rendering.id'] }), context.renderChildAreaChildren(parent))); // get scale factor and apply to child area if (parent.properties === undefined || parent.properties['org.eclipse.elk.topdown.scaleFactor'] === undefined) { return element; } const topdownScaleFactor = parent.properties['org.eclipse.elk.topdown.scaleFactor']; return (0, sprotty_1.svg)("g", { transform: `scale (${topdownScaleFactor})` }, "$", element); } exports.renderChildArea = renderChildArea; /** * Translates a rectangular rendering into an SVG rendering. * This includes KEllipse, KRectangle and KRoundedRectangle. * @param rendering The rendering. * @param parent The parent element. * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. * @param childOfNodeTitle If this rendering is a child of a node title. May override special renderings */ function renderRectangularShape(rendering, parent, boundsAndTransformation, styles, stylesToPropagate, context, childOfNodeTitle) { var _a, _b; const gAttrs = Object.assign({}, (boundsAndTransformation.transformation.length !== 0 ? { transform: boundsAndTransformation.transformation.map(views_common_1.transformationToSVGString).join('') } : {})); // Check the invisibility first. If this rendering is supposed to be invisible, do not render it, // only render its children transformed by the transformation already calculated. if ((0, views_styles_1.isInvisible)(styles)) { return ((0, sprotty_1.svg)("g", Object.assign({}, gAttrs), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); } // Default case. Calculate all svg objects and attributes needed to build this rendering from the styles and the rendering. const colorStyles = (0, views_styles_1.getSvgColorStyles)(styles, context, parent); // objects rendered here that have no background should get a invisible, but clickable background so that users do not click through the non-available background. if (colorStyles.background === views_styles_1.DEFAULT_FILL) { colorStyles.background = views_styles_1.DEFAULT_CLICKABLE_FILL; } const paperShadows = context.renderOptionsRegistry.getValueOrDefault(render_options_registry_1.Shadows) === render_options_registry_1.ShadowOption.PAPER_MODE; const shadowStyles = paperShadows ? (0, views_styles_1.getSvgShadowStyles)(styles, context) : undefined; const lineStyles = (0, views_styles_1.getSvgLineStyles)(styles, parent, context); const lineWidth = (_b = (_a = styles.kLineWidth) === null || _a === void 0 ? void 0 : _a.lineWidth) !== null && _b !== void 0 ? _b : views_styles_1.DEFAULT_LINE_WIDTH; // Create the svg element for this rendering. let element; switch (rendering.type) { case skgraph_models_1.K_ARC: { const kArcRendering = rendering; let sweepFlag = 0; let angle = kArcRendering.arcAngle; // For a negative angle, rotate the other way around. if (angle < 0) { angle = -angle; sweepFlag = 1; } // If the angle is bigger than or equal to 360 degrees, use the same rendering as a KEllipse via fallthrough to that rendering instead. if (angle < 360) { // Calculation to get the start and endpoint of the arc from the angles given. // Reduce the width and height by half the linewidth on both sides, so the ellipse really stays within the given bounds. const width = boundsAndTransformation.bounds.width - lineWidth; const height = boundsAndTransformation.bounds.height - lineWidth; const rX = width / 2; const rY = height / 2; const midX = rX + lineWidth / 2; const midY = rY + lineWidth / 2; const startX = midX + rX * Math.cos((kArcRendering.startAngle * Math.PI) / 180); const startY = midY - rY * Math.sin((kArcRendering.startAngle * Math.PI) / 180); const endAngle = kArcRendering.startAngle + kArcRendering.arcAngle; const endX = midX + rX * Math.cos((endAngle * Math.PI) / 180); const endY = midY - rY * Math.sin((endAngle * Math.PI) / 180); // If the angle is bigger or equal 180 degrees, use the large arc as of the w3c path specification // https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands const largeArcFlag = angle >= 180 ? 1 : 0; // Rotation is not handled via KArcs but via KRotations, so leave this value as 0. const rotate = 0; // The main arc. let d = `M${startX},${startY}A${rX},${rY},${rotate},${largeArcFlag},${sweepFlag},${endX},${endY}`; switch (kArcRendering.arcType) { case skgraph_models_1.Arc.OPEN: { // Open chords do not have any additional lines. break; } case skgraph_models_1.Arc.CHORD: { // Add a straight line from the end to the beginning point. d += `L${startX},${startY}`; break; } case skgraph_models_1.Arc.PIE: { // Add a straight line from the end to the center and then back to the beginning point. d += `L${midX},${midY}L${startX},${startY}`; break; } default: { console.error('error in views-rendering.tsx, unexpected Arc in switch'); } } element = ((0, sprotty_1.svg)("g", Object.assign({ id: rendering.properties['klighd.lsp.rendering.id'] }, gAttrs), ...renderSVGArc(lineStyles, colorStyles, shadowStyles, d, styles.kShadow), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); break; } else { // Fallthrough to KEllipse case. } } // eslint-disable-next-line case skgraph_models_1.K_ELLIPSE: { element = ((0, sprotty_1.svg)("g", Object.assign({ id: rendering.properties['klighd.lsp.rendering.id'] }, gAttrs), ...renderSVGEllipse(boundsAndTransformation.bounds, lineWidth, lineStyles, colorStyles, shadowStyles, styles.kShadow), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); break; } case skgraph_models_1.K_RECTANGLE: case skgraph_models_1.K_ROUNDED_RECTANGLE: { // like this the rx and ry will be undefined during the rendering of a roundedRectangle and therefore those fields will be left out. // Rounded rectangles work in svg just like regular rectangles just with those two added variables, so this call will result in a regular rectangle. // Rendering-specific attributes const rx = rendering.cornerWidth; const ry = rendering.cornerHeight; element = ((0, sprotty_1.svg)("g", Object.assign({ id: rendering.properties['klighd.lsp.rendering.id'] }, gAttrs), ...renderSVGRect(boundsAndTransformation.bounds, lineWidth, rx, ry, lineStyles, colorStyles, shadowStyles, styles.kShadow), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); break; } case skgraph_models_1.K_IMAGE: { const kImage = rendering; const { clipShape } = kImage; const id = rendering.properties['klighd.lsp.rendering.id']; const clipId = `${id}$clip`; let href; if (kImage.bundleName === 'URI') { // Bundle name "URI" is a special handling to interpret the imagePath as a URI. // Here, we just add that URI to the SVG, expecting that it will be available. // Note, that this does mean the URI has to be available whereever the SVG will be opened, even after saving. // An alternative here would be to download and cache that image in code and include it as an embedded base64 data URI instead. href = kImage.imagePath; } else { // Other images have been cached in session storage and can be embedded in the top-level defs and referenced directly. const fullImagePath = `${rendering.bundleName}:${rendering.imagePath}`; const imageId = `image$${fullImagePath}`; href = `#${imageId}`; // Remember the shadow definition to be added at the top level of the SVG, if the same shadow has not been defined previously. if (!context.renderingDefs.has(imageId)) { const extension = fullImagePath.slice(fullImagePath.lastIndexOf('.') + 1); const imageDataURI = `data:image/${extension};base64,${sessionStorage.getItem(fullImagePath)}`; context.renderingDefs.set(imageId, renderSVGImageDef(imageId, imageDataURI)); } } let clipPath; // Render the clip shape within an SVG clipPath element to be used as a clipping mask for the image. if (clipShape !== undefined) { clipShape.isClipRendering = true; const outerClipShape = renderKRendering(clipShape, parent, stylesToPropagate, context, childOfNodeTitle); // renderings start with an outermost <g> element. If that is the case, remove that element and use its child instead. if ((outerClipShape === null || outerClipShape === void 0 ? void 0 : outerClipShape.sel) === 'g' && (outerClipShape === null || outerClipShape === void 0 ? void 0 : outerClipShape.children) !== undefined) { clipPath = (0, sprotty_1.svg)("clipPath", { id: clipId }, outerClipShape === null || outerClipShape === void 0 ? void 0 : outerClipShape.children[0]); } gAttrs.style = { clipPath: `url(#${clipId})`, }; } // Render the image. let imageRenderings; if (kImage.bundleName === 'URI') { imageRenderings = renderSVGImage(boundsAndTransformation.bounds, shadowStyles, href, styles.kShadow); } else { imageRenderings = renderSVGUse(boundsAndTransformation.bounds, shadowStyles, href, styles.kShadow); } element = ((0, sprotty_1.svg)("g", Object.assign({ id: id }, gAttrs), ...clipPath ? [clipPath] : [], ...imageRenderings)); break; } default: { // This case can never happen. If it still does, happy debugging! throw new Error('Rendering is neither an KArc, KEllipse, KImage, nor a KRectangle or KRoundedRectangle!'); } } return element; } exports.renderRectangularShape = renderRectangularShape; /** * Translates a line rendering into an SVG rendering. * This includes all subclasses of and the KPolyline rendering itself. * @param rendering The rendering. * @param parent The parent element. * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. * @param childOfNodeTitle If this rendering is a child of a node title. May override special renderings */ function renderLine(rendering, parent, boundsAndTransformation, styles, stylesToPropagate, context, childOfNodeTitle) { const gAttrs = Object.assign({}, (boundsAndTransformation.transformation.length !== 0 ? { transform: boundsAndTransformation.transformation.map(views_common_1.transformationToSVGString).join('') } : {})); // Check the invisibility first. If this rendering is supposed to be invisible, do not render it, // only render its children transformed by the transformation already calculated. if ((0, views_styles_1.isInvisible)(styles)) { return ((0, sprotty_1.svg)("g", Object.assign({}, gAttrs), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); } // Default case. Calculate all svg objects and attributes needed to build this rendering from the styles and the rendering. const colorStyles = (0, views_styles_1.getSvgColorStyles)(styles, context, parent); // Any non-closed line segment cannot be filled with any color. if (rendering.type !== skgraph_models_1.K_POLYGON) { colorStyles.background = views_styles_1.DEFAULT_FILL; } const paperShadows = context.renderOptionsRegistry.getValueOrDefault(render_options_registry_1.Shadows) === render_options_registry_1.ShadowOption.PAPER_MODE; const shadowStyles = paperShadows ? (0, views_styles_1.getSvgShadowStyles)(styles, context) : undefined; const lineStyles = (0, views_styles_1.getSvgLineStyles)(styles, parent, context); const points = (0, views_common_1.getPoints)(parent, rendering, boundsAndTransformation); if (points.length === 0) { return (0, sprotty_1.svg)("g", null, renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle)); } // now define the line's path. let path = ''; switch (rendering.type) { case skgraph_models_1.K_SPLINE: { path += `M${points[0].x},${points[0].y}`; for (let i = 1; i < points.length; i += 3) { const remainingPoints = points.length - i; if (remainingPoints === 1) { // if one routing point is left, draw a straight line to there. path += `L${points[i].x},${points[i].y}`; } else if (remainingPoints === 2) { // if two routing points are left, draw a quadratic bezier curve with those two points. path += `Q${points[i].x},${points[i].y} ${points[i + 1].x},${points[i + 1].y}`; } else { // if three or more routing points are left, draw a cubic bezier curve with those points. path += `C${points[i].x},${points[i].y} ` + `${points[i + 1].x},${points[i + 1].y} ` + `${points[i + 2].x},${points[i + 2].y}`; } } break; } case skgraph_models_1.K_POLYLINE: // Fall through to next case. KPolylines are just KPolygons without the closing end. case skgraph_models_1.K_POLYGON: { path += `M${points[0].x},${points[0].y}`; for (let i = 1; i < points.length; i++) { path += `L${points[i].x},${points[i].y}`; } if (rendering.type === skgraph_models_1.K_POLYGON) { path += 'Z'; } break; } case skgraph_models_1.K_ROUNDED_BENDS_POLYLINE: { // Rendering-specific attributes const { bendRadius } = rendering; // now define the rounded polyline's path. path += `M${points[0].x},${points[0].y}`; for (let i = 1; i < points.length - 1; i++) { const p0 = points[i - 1]; const p = points[i]; const p1 = points[i + 1]; // last point const x0 = p0.x; const y0 = p0.y; // current point where a bend should be rendered const xp = p.x; const yp = p.y; // next point const x1 = p1.x; const y1 = p1.y; // distance between the last point and the current point const dist0 = Math.sqrt((x0 - xp) * (x0 - xp) + (y0 - yp) * (y0 - yp)); // distance between the current point and the next point const dist1 = Math.sqrt((x1 - xp) * (x1 - xp) + (y1 - yp) * (y1 - yp)); // If the previous / next point is too close, use a smaller bend radius const usedBendRadius = Math.min(bendRadius, dist0 / 2, dist1 / 2); // start and end points of the bend let xs; let ys; let xe; let ye; if (usedBendRadius === 0) { // Avoid division by zero if two points are identical. xs = xp; ys = yp; xe = xp; ye = yp; } else { xs = xp + (usedBendRadius * (x0 - xp)) / dist0; ys = yp + (usedBendRadius * (y0 - yp)) / dist0; xe = xp + (usedBendRadius * (x1 - xp)) / dist1; ye = yp + (usedBendRadius * (y1 - yp)) / dist1; } // draw a line to the start of the bend point (from the last end of its bend) // and then draw the bend with the control points of the point itself and the bend end point. path += `L${xs},${ys}Q${xp},${yp} ${xe},${ye}`; } if (points.length > 1) { const lastPoint = points[points.length - 1]; path += `L${lastPoint.x},${lastPoint.y}`; } break; } default: { console.error('error in views-rendering.tsx, unexpected rendering type in switch'); } } // Create the svg element for this rendering. // Only apply the fast shadow to KPolygons, other shadows are not allowed there. const element = ((0, sprotty_1.svg)("g", Object.assign({ id: rendering.properties['klighd.lsp.rendering.id'] }, gAttrs), ...renderSVGLine(lineStyles, colorStyles, shadowStyles, path, rendering.type === skgraph_models_1.K_POLYGON ? styles.kShadow : undefined), renderChildRenderings(rendering, parent, stylesToPropagate, context, childOfNodeTitle))); return element; } exports.renderLine = renderLine; /** * Translates a text rendering into an SVG text rendering. * @param rendering The rendering. * @param parent The parent element. * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. * @param childOfNodeTitle If this rendering is a child of a node title. May override special renderings */ function renderKText(rendering, parent, boundsAndTransformation, styles, context, childOfNodeTitle) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; // Find the text to write first. let text; // KText elements as renderings of labels have their text in the KLabel, not the KText if ('text' in parent) { // if parent is KLabel text = parent.text; } else { text = rendering.text; } if (parent.properties['de.cau.cs.kieler.klighd.labels.textOverride'] !== undefined) { text = parent.properties['de.cau.cs.kieler.klighd.labels.textOverride']; } // If no text can be found, return here. if (text === undefined) return (0, sprotty_1.svg)("g", null); // The text split into an array for each individual line const lines = text.split('\n'); // Check the invisibility first. If this rendering is supposed to be invisible, do not render it, // only render its children transformed by the transformation already calculated. if ((0, views_styles_1.isInvisible)(styles)) { return (0, sprotty_1.svg)("g", null); } // Default case. Calculate all svg objects and attributes needed to build this rendering from the styles and the rendering. const colorStyles = (0, views_styles_1.getSvgColorStyles)(styles, context, parent); // Calculate the background, if needed, as a rectangle to be placed behind the text. let background; if (colorStyles.background.color !== 'none') { const boundingBoxAndTransformation = (0, views_common_1.findBoundsAndTransformationData)(rendering, styles, parent, context, false, true); background = ((0, sprotty_1.svg)("rect", { x: (_b = (_a = boundingBoxAndTransformation === null || boundingBoxAndTransformation === void 0 ? void 0 : boundingBoxAndTransformation.bounds) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : 0, y: (_d = (_c = boundingBoxAndTransformation === null || boundingBoxAndTransformation === void 0 ? void 0 : boundingBoxAndTransformation.bounds) === null || _c === void 0 ? void 0 : _c.y) !== null && _d !== void 0 ? _d : 0, width: (_f = (_e = boundingBoxAndTransformation === null || boundingBoxAndTransformation === void 0 ? void 0 : boundingBoxAndTransformation.bounds) === null || _e === void 0 ? void 0 : _e.width) !== null && _f !== void 0 ? _f : 0, height: (_h = (_g = boundingBoxAndTransformation === null || boundingBoxAndTransformation === void 0 ? void 0 : boundingBoxAndTransformation.bounds) === null || _g === void 0 ? void 0 : _g.height) !== null && _h !== void 0 ? _h : 0, fill: colorStyles.background.color, style: { opacity: (_j = colorStyles.background.opacity) !== null && _j !== void 0 ? _j : '1' } })); } const paperShadows = context.renderOptionsRegistry.getValueOrDefault(render_options_registry_1.Shadows) === render_options_registry_1.ShadowOption.PAPER_MODE; const shadowStyles = paperShadows ? (0, views_styles_1.getSvgShadowStyles)(styles, context) : undefined; const textStyles = (0, views_styles_1.getSvgTextStyles)(styles); // Replace text with rectangle, if the text is too small. const simplifySmallTextOption = context.renderOptionsRegistry.getValue(render_options_registry_1.SimplifySmallText); const simplifySmallText = simplifySmallTextOption !== null && simplifySmallTextOption !== void 0 ? simplifySmallTextOption : false; // Only enable, if option is found. if (!context.forceRendering && simplifySmallText && !rendering.properties['klighd.isNodeTitle'] && !childOfNodeTitle) { const simplificationThreshold = context.renderOptionsRegistry.getValueOrDefault(render_options_registry_1.TextSimplificationThreshold); const proportionalHeight = 0.5; // height of replacement compared to full text height if (context.viewport && rendering.properties['klighd.calculated.text.bounds'] && rendering.properties['klighd.calculated.text.bounds'].height * context.viewport.zoom <= simplificationThreshold) { const replacements = background ? [background] : []; lines.forEach((line, index) => { var _a, _b, _c, _d; const xPos = (_b = (_a = boundsAndTransformation === null || boundsAndTransformation === void 0 ? void 0 : boundsAndTransformation.bounds) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : 0; const yPos = ((_c = boundsAndTransformation === null || boundsAndTransformation === void 0 ? void 0 : boundsAndTransformation.bounds) === null || _c === void 0 ? void 0 : _c.y) && rendering.properties['klighd.calculated.text.line.heights'] && ((_d = boundsAndTransformation === null || boundsAndTransformation === void 0 ? void 0 : boundsAndTransformation.bounds) === null || _d === void 0 ? void 0 : _d.height) ? boundsAndTransformation.bounds.y - boundsAndTransformation.bounds.height / 2 + (rendering.properties['klighd.calculated.text.line.heights'][index] / 2) * proportionalHeight : 0; const width = rendering.properties['klighd.calculated.text.line.widths'] ? rendering.properties['klighd.calculated.text.line.widths'][index] : 0; const height = rendering.properties['klighd.calculated.text.line.heights'] ? rendering.properties['klighd.calculated.text.line.heights'][index] * proportionalHeight : 0; // Generate rectangle for each line with color style. const curLine = colorStyles.foreground ? ((0, sprotty_1.svg)("rect", { x: xPos, y: yPos, width: width, height: height, fill: colorStyles.foreground.color, opacity: "0.5" })) : ((0, sprotty_1.svg)("rect", { x: xPos, y: yPos, width: width, height: height, fill: "#000000", opacity: "0.5" })); replacements.push(curLine); }); return (0, sprotty_1.svg)("g", { id: rendering.properties['klighd.lsp.rendering.id'] }, ...replacements); } } // The svg style of the resulting text element. const opacity = context.mListener.hasDragged ? 0.1 : parent.opacity; const style = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ 'dominant-baseline': textStyles.dominantBaseline }, { 'font-family': textStyles.fontFamily }), { 'font-size': textStyles.fontSize }), { 'font-style': textStyles.fontStyle }), { 'font-weight': textStyles.fontWeight }), { 'text-decoration-line': textStyles.textDecorationLine }), { 'text-decoration-style': textStyles.textDecorationStyle }), { opacity }), (colorStyles.foreground ? { 'fill-opacity': colorStyles.foreground.opacity } : {})); // The attributes to be contained in the returned text node. const attrs = Object.assign(Object.assign(Object.assign({ x: boundsAndTransformation.bounds.x, style }, (colorStyles.foreground ? { fill: colorStyles.foreground.color } : {})), (shadowStyles ? { filter: shadowStyles } : {})), { 'xml:space': 'preserve' }); const elements = background ? [background] : []; if (lines.length === 1) { // If the text has only one line, just put the text in the text node directly. attrs.y = boundsAndTransformation.bounds.y; // Force any SVG renderer rendering this text to use the exact width calculated for it. // This avoids overlapping texts or too big gaps at the cost of slightly bigger/tighter glyph spacings // when viewed in a different SVG viewer after exporting. if (rendering.properties['klighd.calculated.text.line.widths']) { attrs.textLength = rendering.properties['klighd.calculated.text.line.widths']; attrs.lengthAdjust = 'spacingAndGlyphs'; } elements.push((0, sprotty_1.svg)("text", Object.assign({}, attrs), ...lines)); } else { // Otherwise, put each line of text in a separate <text> element. const calculatedTextLineWidths = rendering.properties['klighd.calculated.text.line.widths']; const calculatedTextLineHeights = rendering.properties['klighd.calculated.text.line.heights']; let currentY = (_k = boundsAndTransformation.bounds.y) !== null && _k !== void 0 ? _k : 0; if (calculatedTextLineWidths) { attrs.lengthAdjust = 'spacingAndGlyphs'; } lines.forEach((line, index) => { const currentElement = ((0, sprotty_1.svg)("text", Object.assign({}, attrs, { y: currentY }, (calculatedTextLineWidths ? { textLength: calculatedTextLineWidths[index] } : {})), line)); elements.push(currentElement); currentY = calculatedTextLineHeights ? currentY + calculatedTextLineHeights[index] : currentY; }); } const gAttrs = Object.assign({}, (boundsAndTransformation.transformation.length !== 0 ? { transform: boundsAndTransformation.transformation.map(views_common_1.transformationToSVGString).join('') } : {})); // build the element from the above defined attributes and children return ((0, sprotty_1.svg)("g", Object.assign({ id: rendering.properties['klighd.lsp.rendering.id'] }, gAttrs), ...elements)); } exports.renderKText = renderKText; /** * Renders all child renderings of the given container rendering. * @param parentRendering The parent rendering. * @param parent The parent element containing this rendering. * @param propagatedStyles The styles propagated from parent elements that should be taken into account. * @param context The rendering context for this element. * @param childOfNodeTitle If this rendering is a child of a node title. May override special renderings */ function renderChildRenderings(parentRendering, parentElement, propagatedStyles, context, childOfNodeTitle) { // children only should be rendered if the parentElement is not a shadow or the rendering is not a clip rendering. if (!(parentElement instanceof skgraph_models_1.SKNode) || (!parentElement.shadow && !parentRendering.isClipRendering)) { const renderings = []; for (const childRendering of parentRendering.children) { const rendering = getRendering([childRendering], parentElement, propagatedStyles, context, childOfNodeTitle); renderings.push(rendering); } return renderings; } return []; } exports.renderChildRenderings = renderChildRenderings; function renderError(rendering) { return ((0, sprotty_1.svg)("text", null, (`Rendering cannot be drawn!\n` + `Type: ${rendering.type}\n` + `ID: ${rendering.properties['klighd.lsp.rendering.id']}`))); } exports.renderError = renderError; /** * Renders some SVG shape, possibly with an added shadow, as given by the svgFunction. If a simple shadow * should be added, it is added as four copies of the SVG shape with rgba(0,0,0,0.1) and the * offsets defined by the kShadow, if a nice shadow should be added, it is added via SVG filter. * * @param kShadow The shadow definition for the rendering, or undefined if no shadow should be added * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param svgFunction The callback function rendering the wanted SVG shape. x and y are the offsets * for the renderings additional to any other offsets, here for the shadows, kShadow is the shadow * definition as given to this function as well and the params are the other params given to this * function. * @param params The further parameters needed to call the svgFunction other than an x, y, shadowStyles, and * kShadow. * @returns the svg shapes generated by the svg function, with the correct shadow. */ function renderWithShadow(kShadow, shadowStyles, svgFunction, ...params) { // If a shadowStyle is given to this function, we want 'nice' shadows using the filter string. if (shadowStyles !== undefined) { return [svgFunction(undefined, undefined, shadowStyles, undefined, ...params)]; } // Otherwise see if there is a shadow definition and render it the 'fast' KIELER-style. if (kShadow) { // Shadow rendered as four copies of the SVG shape with some defaults set due to the shadow and the original rectangle. return [ svgFunction((4 * kShadow.xOffset) / 4, (4 * kShadow.yOffset) / 4, undefined, kShadow, ...params), svgFunction((3 * kShadow.xOffset) / 4, (3 * kShadow.yOffset) / 4, undefined, kShadow, ...params), svgFunction((2 * kShadow.xOffset) / 4, (2 * kShadow.yOffset) / 4, undefined, kShadow, ...params), svgFunction((1 * kShadow.xOffset) / 4, (1 * kShadow.yOffset) / 4, undefined, kShadow, ...params), svgFunction(undefined, undefined, undefined, undefined, ...params), ]; } return [svgFunction(undefined, undefined, undefined, undefined, ...params)]; } exports.renderWithShadow = renderWithShadow; // ------- All functions turning the rendering data into an SVG with potential shadow style. ---------- // /** * 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.) * @param colorStyles style information for color * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param kShadow general shadow information. * @returns An array of SVG <rects> resulting from this. Only multiple <rect>s if a simple shadow effect should be applied. */ function renderSVGRect(bounds, lineWidth, rx, ry, lineStyles, colorStyles, shadowStyles, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGRect, bounds, rx, ry, lineWidth, lineStyles, colorStyles); } exports.renderSVGRect = renderSVGRect; /** * Renders a rectangle with all given information. * If the rendering is a shadow (has a kShadow parameter), all stroke attributes are ignored (no stroke on the shadow) and a * black fill with 0.1 alpha is returned. * * @param x x offset of the rectangle, to be used for shadows only. * @param y y offset of the rectangle, to be used for shadows only. * @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 <rect>. */ function renderSingleSVGRect(x, y, shadowStyles, kShadow, bounds, rx, ry, lineWidth, lineStyles, colorStyles) { // Offset the x/y by the lineWidth. let theX = x || 0; theX += lineWidth / 2; theX = theX === 0 ? undefined : theX; let theY = y || 0; theY += lineWidth / 2; theY = theY === 0 ? undefined : theY; return ((0, sprotty_1.svg)("rect", Object.assign({ width: bounds.width - lineWidth, height: bounds.height - lineWidth }, (theX ? { x: theX } : {}), (theY ? { y: theY } : {}), (rx ? { rx } : {}), (ry ? { ry } : {}), { style: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (kShadow ? {} : { 'stroke-linecap': lineStyles.lineCap })), (kShadow ? {} : { 'stroke-linejoin': lineStyles.lineJoin })), (kShadow ? {} : { 'stroke-width': lineStyles.lineWidth })), (kShadow ? {} : { 'stroke-dasharray': lineStyles.dashArray })), (kShadow ? {} : { 'stroke-miterlimit': lineStyles.miterLimit })), { opacity: kShadow ? colorStyles.opacity ? String(Number(colorStyles.opacity) * 0.1) : '0.1' : colorStyles.opacity }), (kShadow ? {} : { 'stroke-opacity': colorStyles.foreground.opacity })), (kShadow || colorStyles.background.opacity ? { 'fill-opacity': kShadow ? '1' : colorStyles.background.opacity } : {})) }, (kShadow ? {} : { stroke: colorStyles.foreground.color }), (kShadow ? { fill: 'rgb(0,0,0)' } : { fill: colorStyles.background.color }), (shadowStyles ? { filter: shadowStyles } : {})))); } exports.renderSingleSVGRect = renderSingleSVGRect; /** * Renders an definition for an image to be used later with all given information. The image will have a size of 1x1 to be scaled later in its <use>. * * @param imageURI The image href string * @returns An array of SVG elements, here <image>s and <rect>s resulting from this. <rect>s are added if a shadow effect should be applied. */ function renderSVGImageDef(imageId, imageURI) { return (0, sprotty_1.svg)("image", { width: 1, height: 1, id: imageId, href: imageURI }); } exports.renderSVGImageDef = renderSVGImageDef; /** * Renders an image with all given information. * * @param bounds bounds data calculated for this image. * @param image The image href string * @param kShadow shadow information. * @returns An array of SVG elements, here <image>s and <rect>s resulting from this. <rect>s are added if a shadow effect should be applied. */ function renderSVGImage(bounds, shadowStyles, image, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGImage, bounds, image); } exports.renderSVGImage = renderSVGImage; /** * Renders the use of a previously defined SVG shape. * * @param bounds bounds data calculated for this shape. * @param image The use href string * @param kShadow shadow information. * @returns An array of SVG elements, here <use>s and <rect>s resulting from this. <rect>s are added if a shadow effect should be applied. */ function renderSVGUse(bounds, shadowStyles, image, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGUse, bounds, image); } exports.renderSVGUse = renderSVGUse; /** * Renders an image with all given information. * If the rendering is a shadow, a shadow rect is drawn instead. * * @param x x offset of the image, to be used for shadows only. * @param y y offset of the image, to be used for shadows only. * @param kShadow shadow information. Controls what this method does. * @param bounds bounds data calculated for this image. * @param image The image href string * @returns A single SVG <image> or <rect>. */ function renderSingleSVGImage(x, y, shadowStyles, kShadow, bounds, image) { // A shadow of an image is just a rectangle. if (kShadow) { return ((0, sprotty_1.svg)("rect", Object.assign({}, (x ? { x } : {}), (y ? { y } : {}), { width: bounds.width, height: bounds.height, style: { opacity: '0.1', }, fill: "rgb(0,0,0)" }))); } return ((0, sprotty_1.svg)("image", Object.assign({ width: bounds.width, height: bounds.height, href: image }, (shadowStyles ? { filter: shadowStyles } : {})))); } exports.renderSingleSVGImage = renderSingleSVGImage; /** * Renders a pre-defined SVG element usage with all given information. * If the rendering is a shadow, a shadow rect is drawn instead. * * @param x x offset of the element, to be used for shadows only. * @param y y offset of the element, to be used for shadows only. * @param kShadow shadow information. Controls what this method does. * @param bounds bounds data calculated for this element. * @param href The use href string * @returns A single SVG <use> or <rect>. */ function renderSingleSVGUse(x, y, shadowStyles, kShadow, bounds, href) { // A shadow of an image is just a rectangle. if (kShadow) { return ((0, sprotty_1.svg)("rect", Object.assign({}, (x ? { x } : {}), (y ? { y } : {}), { width: bounds.width, height: bounds.height, style: { opacity: '0.1', }, fill: "rgb(0,0,0)" }))); } return ((0, sprotty_1.svg)("use", Object.assign({ transform: `scale(${bounds.width}, ${bounds.height})`, href: href }, (shadowStyles ? { filter: shadowStyles } : {})))); } exports.renderSingleSVGUse = renderSingleSVGUse; /** * Renders an arc with all given information. * * @param lineStyles style information for lines (stroke etc.) * @param colorStyles style information for color * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param path The 'd' attribute for SVG <path> * @param kShadow general shadow information. * @returns An array of SVG <path>s resulting from this. Only multiple <path>s if a simple shadow effect should be applied. */ function renderSVGArc(lineStyles, colorStyles, shadowStyles, path, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGArc, lineStyles, colorStyles, path); } exports.renderSVGArc = renderSVGArc; /** * Renders an arc with all given information. * If the rendering is a shadow (has a kShadow parameter), all stroke attributes are ignored (no stroke on the shadow) and a * black fill with 0.1 alpha is returned. * * @param x x offset of the arc, to be used for shadows only. * @param y y offset of the arc, to be used for shadows only. * @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 lineStyles style information for lines (stroke etc.) * @param colorStyles style information for color * @param path The 'd' attribute for the SVG <path> * @returns A single SVG <path>. */ function renderSingleSVGArc(x, y, shadowStyles, kShadow, lineStyles, colorStyles, path) { return ((0, sprotty_1.svg)("path", Object.assign({}, (x && y ? { transform: `translate(${x},${y})` } : {}), { d: path, style: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (kShadow ? {} : { 'stroke-linecap': lineStyles.lineCap })), (kShadow ? {} : { 'stroke-linejoin': lineStyles.lineJoin })), (kShadow ? {} : { 'stroke-width': lineStyles.lineWidth })), (kShadow ? {} : { 'stroke-dasharray': lineStyles.dashArray })), (kShadow ? {} : { 'stroke-miterlimit': lineStyles.miterLimit })), { opacity: kShadow ? colorStyles.opacity ? String(Number(colorStyles.opacity) * 0.1) : '0.1' : colorStyles.opacity }), (kShadow ? {} : { 'stroke-opacity': colorStyles.foreground.opacity })), (kShadow || colorStyles.background.opacity ? { 'fill-opacity': kShadow ? '1' : colorStyles.background.opacity } : {})) }, (kShadow ? {} : { stroke: colorStyles.foreground.color }), (kShadow ? { fill: 'rgb(0,0,0)' } : { fill: colorStyles.background.color }), (shadowStyles ? { filter: shadowStyles } : {})))); } exports.renderSingleSVGArc = renderSingleSVGArc; /** * Renders an ellipse with all given information. * * @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 * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param kShadow general shadow information. * @returns An array of SVG <ellipse>s resulting from this. Only multiple <ellipse>s if a simple shadow effect should be applied. */ function renderSVGEllipse(bounds, lineWidth, lineStyles, colorStyles, shadowStyles, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGEllipse, bounds, lineWidth, lineStyles, colorStyles); } exports.renderSVGEllipse = renderSVGEllipse; /** * Renders an ellipse with all given information. * If the rendering is a shadow (has a kShadow parameter), all stroke attributes are ignored (no stroke on the shadow) and a * black fill with 0.1 alpha is returned. * * @param x x offset of the ellipse, to be used for shadows only. * @param y y offset of the ellipse, to be used for shadows only. * @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 <ellipse>. */ function renderSingleSVGEllipse(x, y, shadowStyles, kShadow, bounds, lineWidth, lineStyles, colorStyles) { return ((0, sprotty_1.svg)("ellipse", Object.assign({}, (x && y ? { transform: `translate(${x},${y})` } : {}), { cx: bounds.width / 2, cy: bounds.height / 2, rx: bounds.width / 2 - lineWidth / 2, ry: bounds.height / 2 - lineWidth / 2, style: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (kShadow ? {} : { 'stroke-linecap': lineStyles.lineCap })), (kShadow ? {} : { 'stroke-linejoin': lineStyles.lineJoin })), (kShadow ? {} : { 'stroke-width': lineStyles.lineWidth })), (kShadow ? {} : { 'stroke-dasharray': lineStyles.dashArray })), (kShadow ? {} : { 'stroke-miterlimit': lineStyles.miterLimit })), { opacity: kShadow ? colorStyles.opacity ? String(Number(colorStyles.opacity) * 0.1) : '0.1' : colorStyles.opacity }), (kShadow ? {} : { 'stroke-opacity': colorStyles.foreground.opacity })), (kShadow || colorStyles.background.opacity ? { 'fill-opacity': kShadow ? '1' : colorStyles.background.opacity } : {})) }, (kShadow ? {} : { stroke: colorStyles.foreground.color }), (kShadow ? { fill: 'rgb(0,0,0)' } : { fill: colorStyles.background.color }), (shadowStyles ? { filter: shadowStyles } : {})))); } exports.renderSingleSVGEllipse = renderSingleSVGEllipse; /** * Renders a rendering with a specific path (polyline, polygon, etc.) with all given information. * * @param lineStyles style information for lines (stroke etc.) * @param colorStyles style information for color * @param shadowStyles specific shadow filter ID, if this element should be drawn with a smooth shadow and no simple one. * @param path The 'd' attribute for the SVG <path> * @param kShadow general shadow information. * @returns An array of SVG <path>s resulting from this. Only multiple <path>s if a simple shadow effect should be applied. */ function renderSVGLine(lineStyles, colorStyles, shadowStyles, path, kShadow) { return renderWithShadow(kShadow, shadowStyles, renderSingleSVGLine, lineStyles, colorStyles, path); } exports.renderSVGLine = renderSVGLine; /** * Renders a rendering with a specific path (polyline, polygon, etc.) with all given information. * If the rendering is a shadow (has a kShadow parameter), all stroke attributes are ignored (no stroke on the shadow) and a * black fill with 0.1 alpha is returned. * * @param x x offset of the line, to be used for shadows only. * @param y y offset of the line, to be used for shadows only