g2o-svg
Version:
g2o Scalable Vector Graphics
377 lines (369 loc) • 14.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('g2o')) :
typeof define === 'function' && define.amd ? define(['exports', 'g2o'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.G2OCANVAS = {}, global.g2o));
})(this, (function (exports, g2o) { 'use strict';
class HTMLElementDOM {
doc;
constructor(doc) {
this.doc = doc;
}
addEventListener(target, name, callback) {
target.addEventListener(name, callback);
}
appendChild(parent, child) {
parent.appendChild(child);
}
getAttribute(element, qualifiedName) {
return element.getAttribute(qualifiedName);
}
getBoundingClientRect(element) {
return element.getBoundingClientRect();
}
getElementById(elementId) {
return document.getElementById(elementId);
}
isDocumentBody(element) {
return element === document.body;
}
removeEventListener(target, name, callback) {
target.removeEventListener(name, callback);
}
}
function mod(v, l) {
while (v < 0) {
v += l;
}
return v % l;
}
const floor = Math.floor;
/**
* @param {Number} v - Any float
* @returns {Number} That float trimmed to the third decimal place.
* @description A pretty fast toFixed(3) alternative.
* @see {@link http://jsperf.com/parsefloat-tofixed-vs-math-round/18}
*/
function toFixed(v) {
return floor(v * 1000000) / 1000000;
}
const ns = 'http://www.w3.org/2000/svg';
const xlink = 'http://www.w3.org/1999/xlink';
function createSVGElement(qualifiedName, attrs = {}) {
const elem = document.createElementNS(ns, qualifiedName);
if (attrs && Object.keys(attrs).length > 0) {
setAttributes(elem, attrs);
}
return elem;
}
function setAttributes(elem, attrs) {
// SVGAttributes does not have an index signature.
const styles = attrs;
const keys = Object.keys(attrs);
for (let i = 0; i < keys.length; i++) {
const qualifiedName = keys[i];
const value = styles[qualifiedName];
if (/href/.test(keys[i])) {
elem.setAttributeNS(xlink, qualifiedName, value);
}
else {
elem.setAttribute(qualifiedName, value);
}
}
}
class SVGViewDOM {
downcast(element) {
if (element instanceof SVGElement) {
return element;
}
else {
throw new Error("element is not an SVGElement");
}
}
createSVGElement(qualifiedName, attributes = {}) {
return createSVGElement(qualifiedName, attributes);
}
setAttribute(element, qualifiedName, value) {
element.setAttribute(qualifiedName, value);
}
setAttributes(element, attributes) {
setAttributes(element, attributes);
}
removeAttribute(element, qualifiedName) {
element.removeAttribute(qualifiedName);
}
removeAttributes(element, attributes) {
svg.removeAttributes(element, attributes);
}
appendChild(parent, child) {
parent.appendChild(child);
}
removeChild(parent, child) {
parent.removeChild(child);
}
setTextContent(element, textContent) {
element.textContent = textContent;
}
getParentNode(element) {
return element.parentNode;
}
getLastChild(element) {
return element.lastChild;
}
getElementDefs(svg) {
return get_svg_element_defs(svg);
}
setStyle(element, name, value) {
element.style[name] = value;
}
}
/**
* Finds the SVGDefsElement from the children of the SVGElement.
*/
function get_svg_element_defs(svg) {
const children = svg.children;
const N = children.length;
for (let i = 0; i < N; i++) {
const child = children.item(i);
if (child instanceof SVGDefsElement) {
return child;
}
}
throw new Error();
}
/**
* sets the "hidden" _flagUpdate property.
*/
function set_defs_dirty_flag(defs, dirtyFlag) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defs._flagUpdate = dirtyFlag;
}
/**
* gets the "hidden" _flagUpdate property.
*/
function get_defs_dirty_flag(defs) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return defs._flagUpdate;
}
const svg = {
ns: 'http://www.w3.org/2000/svg',
xlink: 'http://www.w3.org/1999/xlink',
// Create an svg namespaced element.
createElement: function (qualifiedName, attrs = {}) {
const elem = document.createElementNS(svg.ns, qualifiedName);
if (attrs && Object.keys(attrs).length > 0) {
svg.setAttributes(elem, attrs);
}
return elem;
},
// Add attributes from an svg element.
setAttributes: function (elem, attrs) {
// SVGAttributes does not have an index signature.
const styles = attrs;
const keys = Object.keys(attrs);
for (let i = 0; i < keys.length; i++) {
const qualifiedName = keys[i];
const value = styles[qualifiedName];
if (/href/.test(keys[i])) {
elem.setAttributeNS(svg.xlink, qualifiedName, value);
}
else {
elem.setAttribute(qualifiedName, value);
}
}
return this;
},
// Remove attributes from an svg element.
removeAttributes: function (elem, attrs) {
if (elem) {
for (const key in attrs) {
elem.removeAttribute(key);
}
return this;
}
else {
throw new Error("elem MUST be defined.");
}
},
path_from_anchors: function (board, position, attitude, anchors, closed) {
// The anchors are user coordinates and don't include the position and attitude of the body.
// By switching x amd y here we handle a 90 degree coordinate rotation.
// We are not completely done because Text and Images are rotated.
const [screenX, screenY] = screen_functions(board);
const l = anchors.length;
const last = l - 1;
let d; // The elusive last Commands.move point
let string = '';
for (let i = 0; i < l; i++) {
const b = anchors[i];
const prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
const a = anchors[prev];
let command;
let c;
let vx, vy, ux, uy, ar, bl, br, cl;
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
let x = toFixed(screenX(b.x, b.y));
let y = toFixed(screenY(b.x, b.y));
switch (b.command) {
case g2o.Commands.close:
command = g2o.Commands.close;
break;
case g2o.Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
command = g2o.Commands.arc + ' ' + rx + ' ' + ry + ' '
+ xAxisRotation + ' ' + largeArcFlag + ' ' + sweepFlag + ' '
+ x + ' ' + y;
break;
case g2o.Commands.curve:
ar = (a.controls && a.controls.b) || g2o.G20.zero;
bl = (b.controls && b.controls.a) || g2o.G20.zero;
if (a.relative) {
vx = toFixed(screenX(ar.x + a.x, ar.y + a.y));
vy = toFixed(screenY(ar.x + a.x, ar.y + a.y));
}
else {
vx = toFixed(screenX(ar.x, ar.y));
vy = toFixed(screenY(ar.x, ar.y));
}
if (b.relative) {
ux = toFixed(screenX(bl.x + b.x, bl.y + b.y));
uy = toFixed(screenY(bl.x + b.x, bl.y + b.y));
}
else {
ux = toFixed(screenX(bl.x, bl.y));
uy = toFixed(screenY(bl.x, bl.y));
}
command = ((i === 0) ? g2o.Commands.move : g2o.Commands.curve) +
' ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
break;
case g2o.Commands.move: {
d = b;
command = g2o.Commands.move + ' ' + x + ' ' + y;
break;
}
default: {
command = b.command + ' ' + x + ' ' + y;
}
}
// Add a final point and close it off
if (i >= last && closed) {
if (b.command === g2o.Commands.curve) {
// Make sure we close to the most previous Commands.move
c = d;
br = (b.controls && b.controls.b) || b;
cl = (c.controls && c.controls.a) || c;
if (b.relative) {
vx = toFixed(screenX(br.x + b.x, br.y + b.y));
vy = toFixed(screenY(br.x + b.x, br.y + b.y));
}
else {
vx = toFixed(screenX(br.x, br.y));
vy = toFixed(screenY(br.x, br.y));
}
if (c.relative) {
ux = toFixed(screenX(cl.x + c.x, cl.y + c.y));
uy = toFixed(screenY(cl.x + c.x, cl.y + c.y));
}
else {
ux = toFixed(screenX(cl.x, cl.y));
uy = toFixed(screenY(cl.x, cl.y));
}
x = toFixed(screenX(c.x, c.y));
y = toFixed(screenY(c.x, c.y));
command += ' C ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
}
if (b.command !== g2o.Commands.close) {
command += ' Z';
}
}
string += command + ' ';
}
return string;
},
/**
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath
* @param shape
* @param svgElement
* @returns
*/
getClip: function (viewDOM, shape, svgElement) {
let clipPath = shape.zzz.svgClipPathElement;
if (!clipPath) {
clipPath = shape.zzz.svgClipPathElement = viewDOM.createSVGElement('clipPath', { 'clip-rule': 'nonzero' });
}
if (viewDOM.getParentNode(clipPath) === null) {
viewDOM.appendChild(viewDOM.getElementDefs(svgElement), clipPath);
}
return clipPath;
},
defs: {
update: function (svgElement, defs) {
if (get_defs_dirty_flag(defs)) {
const children = Array.prototype.slice.call(defs.children, 0);
for (let i = 0; i < children.length; i++) {
const child = children[i];
const id = child.id;
const selector = `[fill="url(#${id})"],[stroke="url(#${id})"],[clip-path="url(#${id})"]`;
const exists = svgElement.querySelector(selector);
if (!exists) {
defs.removeChild(child);
}
}
set_defs_dirty_flag(defs, false);
}
}
},
};
/**
* If the bounding box is oriented such that y increases in the upwards direction,
* exchange the x and y coordinates because we will be applying a 90 degree rotation.
*/
function screen_functions(board) {
if (board.goofy) {
if (board.crazy) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return [(x, y) => y, (x, y) => x];
}
else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return [(x, y) => x, (x, y) => y];
}
}
else {
if (board.crazy) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return [(x, y) => x, (x, y) => y];
}
else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return [(x, y) => y, (x, y) => x];
}
}
}
class SVGViewFactory {
constructor() {
// Nothing to see here.
}
createView(viewBox, containerId) {
const viewDOM = new SVGViewDOM();
return new g2o.TreeView(viewDOM, viewBox, containerId);
}
}
/**
* A convenience function for initializing a new GraphicsBoard using SVG.
* @param elementOrId HTML identifier (id) of element in which the board is rendered.
* @param options An object that sets some of the board properties.
*/
function initBoard(elementOrId, options = {}) {
const elementDOM = new HTMLElementDOM(window.document);
const viewDOM = new SVGViewDOM();
return new g2o.GraphicsBoard(elementOrId, elementDOM, viewDOM, new SVGViewFactory(), options);
}
exports.SVGViewDOM = SVGViewDOM;
exports.SVGViewFactory = SVGViewFactory;
exports.initBoard = initBoard;
}));
//# sourceMappingURL=index.js.map