UNPKG

@maxgraph/core

Version:

maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

485 lines (481 loc) 21.8 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors Copyright (c) 2006-2015, JGraph Ltd Copyright (c) 2006-2015, Gaudenz Alder Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StencilShapeConfig = void 0; const ConnectionConstraint_js_1 = __importDefault(require("../../other/ConnectionConstraint.js")); const Rectangle_js_1 = __importDefault(require("../../geometry/Rectangle.js")); const Shape_js_1 = __importDefault(require("../Shape.js")); const Constants_js_1 = require("../../../util/Constants.js"); const StencilShapeRegistry_js_1 = require("./StencilShapeRegistry.js"); const domUtils_js_1 = require("../../../util/domUtils.js"); const Point_js_1 = __importDefault(require("../../geometry/Point.js")); const utils_js_1 = require("../../../internal/utils.js"); const i18n_utils_js_1 = require("../../../internal/i18n-utils.js"); /** * Configure global settings for stencil shapes. * @experimental subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. * @since 0.11.0 * @category Configuration * @category Shape */ exports.StencilShapeConfig = { /** * Specifies if the use of eval is allowed for evaluating text content and images. * Set this to `true` if stencils can not contain user input. * * **WARNING**: Enabling this switch carries a possible security risk. * * @default false */ allowEval: false, /** * Specifies the default value for the localized attribute of the text element. * @default false */ defaultLocalized: false, }; // To manage the following attribute described in stencils.xsd // <xs:attribute name="large-arc-flag" use="required" type="xs:decimal"/> // <xs:attribute name="sweep-flag" use="required" type="xs:decimal"/> const toBoolean = (value) => value !== '0'; /** * Implements a generic shape which is based on an XML node as a description. * * The XSD for the stencil description is available in the `stencils.xsd` file. * * @category Shape */ class StencilShape extends Shape_js_1.default { constructor(desc) { super(); /** * Holds an array of {@link ConnectionConstraint}s as defined in the shape. */ this.constraints = []; /** * Holds the aspect of the shape. Default is 'auto'. */ this.aspect = 'auto'; /** * Holds the width of the shape. Default is 100. */ this.w0 = 100; /** * Holds the height of the shape. Default is 100. */ this.h0 = 100; /** * Holds the XML node with the stencil description. */ // bgNode: Element; this.bgNode = null; /** * Holds the XML node with the stencil description. */ this.fgNode = null; /** * Holds the strokewidth direction from the description. */ this.strokeWidthValue = null; this.desc = desc; this.parseDescription(); this.parseConstraints(); } /** * Reads <w0>, <h0>, <aspect>, <bgNodes> and <fgNodes> from <desc>. */ parseDescription() { // LATER: Preprocess nodes for faster painting this.fgNode = this.desc.getElementsByTagName('foreground')[0]; this.bgNode = this.desc.getElementsByTagName('background')[0]; this.w0 = Number(this.desc.getAttribute('w') || 100); this.h0 = Number(this.desc.getAttribute('h') || 100); // Possible values for aspect are: variable and fixed where // variable means fill the available space and fixed means // use w0 and h0 to compute the aspect. const aspect = this.desc.getAttribute('aspect'); this.aspect = aspect ?? 'variable'; // Possible values for strokewidth are all numbers and "inherit" // where the inherit means take the value from the style (ie. the // user-defined stroke-width). Note that the strokewidth is scaled // by the minimum scaling that is used to draw the shape (sx, sy). const sw = this.desc.getAttribute('strokewidth'); this.strokeWidthValue = !(0, utils_js_1.isNullish)(sw) ? sw : '1'; } /** * Reads the constraints from {@link desc} into {@link constraints} using {@link parseConstraint}. */ parseConstraints() { const conns = this.desc.getElementsByTagName('connections')[0]; if (conns) { const tmp = (0, domUtils_js_1.getChildNodes)(conns); if (tmp.length > 0) { this.constraints = []; for (let i = 0; i < tmp.length; i += 1) { this.constraints.push(this.parseConstraint(tmp[i])); } } } } /** * Parses the given XML node and returns its {@link ConnectionConstraint}. */ parseConstraint(node) { const x = Number(node.getAttribute('x')); const y = Number(node.getAttribute('y')); const perimeter = node.getAttribute('perimeter') === '1'; const name = node.getAttribute('name'); return new ConnectionConstraint_js_1.default(new Point_js_1.default(x, y), perimeter, name); } /** * Gets the given attribute as a text. The return value from <evaluateAttribute> * is used as a key to {@link Resources#get} if the localized attribute in the text * node is 1 or if <defaultLocalized> is true. */ evaluateTextAttribute(node, attribute, shape) { let result = this.evaluateAttribute(node, attribute, shape); const loc = node.getAttribute('localized'); if ((exports.StencilShapeConfig.defaultLocalized && !loc) || loc === '1') { result = (0, i18n_utils_js_1.translate)(result); } return result; } /** * Gets the attribute for the given name from the given node. If the attribute * does not exist then the text content of the node is evaluated and if it is * a function it is invoked with <shape> as the only argument and the return * value is used as the attribute value to be returned. */ evaluateAttribute(node, attribute, shape) { let result = node.getAttribute(attribute); if (!result) { const text = (0, domUtils_js_1.getTextContent)(node); if (text && exports.StencilShapeConfig.allowEval) { const funct = (0, utils_js_1.doEval)(text); if (typeof funct === 'function') { result = funct(shape); } } } return result; } /** * Draws this stencil inside the given bounds. */ drawShape(canvas, shape, x, y, w, h) { const stack = canvas.states.slice(); // TODO: Internal structure (array of special structs?), relative and absolute // coordinates (eg. note shape, process vs star, actor etc.), text rendering // and non-proportional scaling, how to implement pluggable edge shapes // (start, segment, end blocks), pluggable markers, how to implement // swimlanes (title area) with this API, add icon, horizontal/vertical // label, indicator for all shapes, rotation const direction = shape.style?.direction; const aspect = this.computeAspect(shape, x, y, w, h, direction); const minScale = Math.min(aspect.width, aspect.height); const sw = this.strokeWidthValue === 'inherit' ? (shape.style?.strokeWidth ?? 1) : Number(this.strokeWidthValue) * minScale; canvas.setStrokeWidth(sw); // Draws a transparent rectangle for catching events if (shape.style?.pointerEvents ?? false) { canvas.setStrokeColor(Constants_js_1.NONE); canvas.rect(x, y, w, h); canvas.stroke(); canvas.setStrokeColor(shape.stroke); } this.drawChildren(canvas, shape, x, y, w, h, this.bgNode, aspect, false, true); this.drawChildren(canvas, shape, x, y, w, h, this.fgNode, aspect, true, !shape.outline || shape.style == null || !(shape.style.backgroundOutline ?? false)); // Restores stack for unequal count of save/restore calls if (canvas.states.length != stack.length) { canvas.states = stack; } } /** * Draws this stencil inside the given bounds. */ drawChildren(canvas, shape, x, y, w, h, node, aspect, disableShadow, paint) { if (node && w > 0 && h > 0) { let tmp = node.firstChild; while (tmp) { if ((0, utils_js_1.isElement)(tmp)) { this.drawNode(canvas, shape, tmp, aspect, disableShadow, paint); } tmp = tmp.nextSibling; } } } /** * Returns a rectangle that contains the offset in x and y and the horizontal * and vertical scale in width and height used to draw this shape inside the * given {@link Rectangle}. * * @param shape {@link Shape} to be drawn. * @param bounds {@link Rectangle} that should contain the stencil. * @param direction Optional direction of the shape to be darwn. */ computeAspect(shape = null, x, y, w, h, direction) { let x0 = x; let y0 = y; let sx = w / this.w0; let sy = h / this.h0; const inverse = direction === 'north' || direction === 'south'; if (inverse) { sy = w / this.h0; sx = h / this.w0; const delta = (w - h) / 2; x0 += delta; y0 -= delta; } if (this.aspect === 'fixed') { sy = Math.min(sx, sy); sx = sy; // Centers the shape inside the available space if (inverse) { x0 += (h - this.w0 * sx) / 2; y0 += (w - this.h0 * sy) / 2; } else { x0 += (w - this.w0 * sx) / 2; y0 += (h - this.h0 * sy) / 2; } } return new Rectangle_js_1.default(x0, y0, sx, sy); } /** * Draws this stencil inside the given bounds. */ drawNode(canvas, shape, node, aspect, disableShadow, paint) { const name = node.nodeName; const x0 = aspect.x; const y0 = aspect.y; const sx = aspect.width; const sy = aspect.height; const minScale = Math.min(sx, sy); if (name === 'save') { canvas.save(); } else if (name === 'restore') { canvas.restore(); } else if (paint) { if (name === 'path') { canvas.begin(); let parseRegularly = true; if (node.getAttribute('rounded') == '1') { parseRegularly = false; const arcSize = Number(node.getAttribute('arcSize')); let pointCount = 0; const segs = []; // Renders the elements inside the given path let childNode = node.firstChild; while (childNode != null) { if ((0, utils_js_1.isElement)(childNode)) { const childName = childNode.nodeName; if (childName === 'move' || childName === 'line') { if (childName === 'move' || segs.length === 0) { segs.push([]); } segs[segs.length - 1].push(new Point_js_1.default(x0 + Number(childNode.getAttribute('x')) * sx, y0 + Number(childNode.getAttribute('y')) * sy)); pointCount++; } else { // We only support move and line for rounded corners parseRegularly = true; break; } } childNode = childNode.nextSibling; } if (!parseRegularly && pointCount > 0) { for (let i = 0; i < segs.length; i += 1) { let close = false; const ps = segs[i][0]; const pe = segs[i][segs[i].length - 1]; if (ps.x === pe.x && ps.y === pe.y) { segs[i].pop(); close = true; } this.addPoints(canvas, segs[i], true, arcSize, close); } } else { parseRegularly = true; } } if (parseRegularly) { // Renders the elements inside the given path let childNode = node.firstChild; while (childNode) { if ((0, utils_js_1.isElement)(childNode)) { this.drawNode(canvas, shape, childNode, aspect, disableShadow, paint); } childNode = childNode.nextSibling; } } } else if (name === 'close') { canvas.close(); } else if (name === 'move') { canvas.moveTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy); } else if (name === 'line') { canvas.lineTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy); } else if (name === 'quad') { canvas.quadTo(x0 + Number(node.getAttribute('x1')) * sx, y0 + Number(node.getAttribute('y1')) * sy, x0 + Number(node.getAttribute('x2')) * sx, y0 + Number(node.getAttribute('y2')) * sy); } else if (name === 'curve') { canvas.curveTo(x0 + Number(node.getAttribute('x1')) * sx, y0 + Number(node.getAttribute('y1')) * sy, x0 + Number(node.getAttribute('x2')) * sx, y0 + Number(node.getAttribute('y2')) * sy, x0 + Number(node.getAttribute('x3')) * sx, y0 + Number(node.getAttribute('y3')) * sy); } else if (name === 'arc') { canvas.arcTo(Number(node.getAttribute('rx')) * sx, Number(node.getAttribute('ry')) * sy, Number(node.getAttribute('x-axis-rotation')), toBoolean(node.getAttribute('large-arc-flag')), toBoolean(node.getAttribute('sweep-flag')), x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy); } else if (name === 'rect') { canvas.rect(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy, Number(node.getAttribute('w')) * sx, Number(node.getAttribute('h')) * sy); } else if (name === 'roundrect') { let arcsize = Number(node.getAttribute('arcsize')); if (arcsize === 0) { arcsize = Constants_js_1.RECTANGLE_ROUNDING_FACTOR * 100; } const w = Number(node.getAttribute('w')) * sx; const h = Number(node.getAttribute('h')) * sy; const factor = Number(arcsize) / 100; const r = Math.min(w * factor, h * factor); canvas.roundrect(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy, w, h, r, r); } else if (name === 'ellipse') { canvas.ellipse(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy, Number(node.getAttribute('w')) * sx, Number(node.getAttribute('h')) * sy); } else if (name === 'image') { if (!shape.outline) { const src = this.evaluateAttribute(node, 'src', shape); canvas.image(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy, Number(node.getAttribute('w')) * sx, Number(node.getAttribute('h')) * sy, src, false, node.getAttribute('flipH') === '1', node.getAttribute('flipV') === '1'); } } else if (name === 'text') { if (!shape.outline) { const str = this.evaluateTextAttribute(node, 'str', shape); let rotation = node.getAttribute('vertical') == '1' ? -90 : 0; if (node.getAttribute('align-shape') === '0') { const dr = shape.rotation; // Depends on flipping const flipH = shape.style?.flipH ?? false; const flipV = shape.style?.flipV ?? false; if (flipH && flipV) { rotation -= dr; } else if (flipH || flipV) { rotation += dr; } else { rotation -= dr; } } rotation -= Number(node.getAttribute('rotation')); canvas.text(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy, 0, 0, str, node.getAttribute('align') ?? 'left', node.getAttribute('valign') ?? 'top', false, '', 'auto', false, rotation, 'auto'); } } else if (name === 'include-shape') { const stencil = StencilShapeRegistry_js_1.StencilShapeRegistry.get(node.getAttribute('name')); if (stencil) { const x = x0 + Number(node.getAttribute('x')) * sx; const y = y0 + Number(node.getAttribute('y')) * sy; const w = Number(node.getAttribute('w')) * sx; const h = Number(node.getAttribute('h')) * sy; stencil.drawShape(canvas, shape, x, y, w, h); } } else if (name === 'fillstroke') { canvas.fillAndStroke(); } else if (name === 'fill') { canvas.fill(); } else if (name === 'stroke') { canvas.stroke(); } else if (name === 'strokewidth') { const s = node.getAttribute('fixed') === '1' ? 1 : minScale; canvas.setStrokeWidth(Number(node.getAttribute('width')) * s); } else if (name === 'dashed') { canvas.setDashed(node.getAttribute('dashed') === '1'); } else if (name === 'dashpattern') { let value = node.getAttribute('pattern'); if (value != null) { const tmp = value.split(' '); const pat = []; for (let i = 0; i < tmp.length; i += 1) { if (tmp[i].length > 0) { pat.push(Number(tmp[i]) * minScale); } } value = pat.join(' '); canvas.setDashPattern(value); } } else if (name === 'strokecolor') { canvas.setStrokeColor(node.getAttribute('color')); } else if (name === 'linecap') { canvas.setLineCap(node.getAttribute('cap')); } else if (name === 'linejoin') { canvas.setLineJoin(node.getAttribute('join')); } else if (name === 'miterlimit') { canvas.setMiterLimit(Number(node.getAttribute('limit'))); } else if (name === 'fillcolor') { canvas.setFillColor(node.getAttribute('color')); } else if (name === 'alpha') { canvas.setAlpha(Number(node.getAttribute('alpha'))); } else if (name === 'fillalpha') { canvas.setAlpha(Number(node.getAttribute('alpha'))); } else if (name === 'strokealpha') { canvas.setAlpha(Number(node.getAttribute('alpha'))); } else if (name === 'fontcolor') { canvas.setFontColor(node.getAttribute('color')); } else if (name === 'fontstyle') { canvas.setFontStyle(Number(node.getAttribute('style'))); } else if (name === 'fontfamily') { canvas.setFontFamily(node.getAttribute('family')); } else if (name === 'fontsize') { canvas.setFontSize(Number(node.getAttribute('size')) * minScale); } if (disableShadow && (name === 'fillstroke' || name === 'fill' || name === 'stroke')) { disableShadow = false; canvas.setShadow(false); } } } } exports.default = StencilShape;