UNPKG

svgedit

Version:

Powerful SVG-Editor for your browser

449 lines (448 loc) 16.7 kB
import { __vitePreload } from "../_virtual/_vite/preload-helper.js"; //#region src/editor/extensions/ext-connector/ext-connector.js function __variableDynamicImportRuntime0__(path) { switch (path) { case "./locale/en.js": return __vitePreload(() => import("./locale/en.js"), [], import.meta.url); case "./locale/fr.js": return __vitePreload(() => import("./locale/fr.js"), [], import.meta.url); case "./locale/zh-CN.js": return __vitePreload(() => import("./locale/zh-CN.js"), [], import.meta.url); default: return new Promise(function(resolve, reject) { (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(reject.bind(null, /* @__PURE__ */ new Error("Unknown variable dynamic import: " + path))); }); } } /** * @file ext-connector.js * * @license MIT * * @copyright 2010 Alexis Deveria * @copyright 2023 Optimistik SAS * */ var name = "connector"; var loadExtensionTranslation = async function(svgEditor) { let translationModule; const lang = svgEditor.configObj.pref("lang"); try { translationModule = await __variableDynamicImportRuntime0__(`./locale/${lang}.js`); } catch (_error) { console.warn(`Missing translation (${lang}) for ${name} - using 'en'`); translationModule = await __vitePreload(() => import("./locale/en.js"), [], import.meta.url); } svgEditor.i18next.addResourceBundle(lang, name, translationModule.default); }; var ext_connector_default = { name, async init(S) { const svgEditor = this; const { svgCanvas } = svgEditor; const { getElement, $id, $click, addSVGElementsFromJson } = svgCanvas; const { svgroot, selectorManager } = S; const seNs = svgCanvas.getEditorNS(); await loadExtensionTranslation(svgEditor); let startX; let startY; let curLine; let startElem; let endElem; let started = false; let connections = []; const originalGroupSelectedElements = svgCanvas.groupSelectedElements; svgCanvas.groupSelectedElements = function(...args) { svgCanvas.removeFromSelection(document.querySelectorAll("[id^=\"conn_\"]")); return originalGroupSelectedElements.apply(this, args); }; const originalMoveSelectedElements = svgCanvas.moveSelectedElements; svgCanvas.moveSelectedElements = function(...args) { const cmd = originalMoveSelectedElements.apply(this, args); updateConnectors(svgCanvas.getSelectedElements()); return cmd; }; /** * getBBintersect * @param {Float} x * @param {Float} y * @param {module:utilities.BBoxObject} bb * @param {Float} offset * @returns {module:math.XYObject} */ const getBBintersect = (x, y, bb, offset) => { if (offset) { bb = { ...bb }; bb.width += offset; bb.height += offset; bb.x -= offset / 2; bb.y -= offset / 2; } const midX = bb.x + bb.width / 2; const midY = bb.y + bb.height / 2; const lenX = x - midX; const lenY = y - midY; const slope = Math.abs(lenY / lenX); let ratio; if (slope < bb.height / bb.width) ratio = bb.width / 2 / Math.abs(lenX); else ratio = lenY ? bb.height / 2 / Math.abs(lenY) : 0; return { x: midX + lenX * ratio, y: midY + lenY * ratio }; }; /** * getOffset * @param {"start"|"end"} side - The side of the line ("start" or "end") where the marker may be present. * @param {Element} line - The line element to check for a marker. * @returns {Float} - Returns the calculated offset if a marker is present, otherwise returns 0. */ const getOffset = (side, line) => { const hasMarker = line.getAttribute("marker-" + side); const size = line.getAttribute("stroke-width") * 5; return hasMarker ? size : 0; }; /** * showPanel * @param {boolean} on - Determines whether to show or hide the elements. * @returns {void} */ const showPanel = (on) => { let connRules = $id("connector_rules"); if (!connRules) { connRules = document.createElement("style"); connRules.setAttribute("id", "connector_rules"); document.getElementsByTagName("head")[0].appendChild(connRules); } connRules.textContent = !on ? "" : "#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }"; if ($id("connector_rules")) $id("connector_rules").style.display = on ? "block" : "none"; }; /** * setPoint * @param {Element} elem - The SVG element. * @param {Integer|"end"} pos - The position index or "end". * @param {Float} x - The x-coordinate. * @param {Float} y - The y-coordinate. * @param {boolean} [setMid] - Whether to set the midpoint. * @returns {void} */ const setPoint = (elem, pos, x, y, setMid) => { const pts = elem.points; const pt = svgroot.createSVGPoint(); pt.x = x; pt.y = y; if (pos === "end") pos = pts.numberOfItems - 1; pts.replaceItem(pt, pos); if (setMid) { const ptStart = pts.getItem(0); const ptEnd = pts.getItem(pts.numberOfItems - 1); setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); } }; /** * @param {Float} diffX * @param {Float} diffY * @returns {void} */ const updatePoints = (line, conn, bb, altBB, pre, altPre) => { const srcX = altBB.x + altBB.width / 2; const srcY = altBB.y + altBB.height / 2; const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); setPoint(line, conn.is_start ? 0 : "end", pt.x, pt.y, true); const pt2 = getBBintersect(pt.x, pt.y, altBB, getOffset(altPre, line)); setPoint(line, conn.is_start ? "end" : 0, pt2.x, pt2.y, true); }; const updateLine = (diffX, diffY) => { const dataStorage = svgCanvas.getDataStorage(); for (const conn of connections) { const { connector: line, is_start: isStart, start_x: startX, start_y: startY } = conn; const pre = isStart ? "start" : "end"; const altPre = isStart ? "end" : "start"; const bb = { ...dataStorage.get(line, `${pre}_bb`) }; bb.x = startX + diffX; bb.y = startY + diffY; dataStorage.put(line, `${pre}_bb`, bb); const altBB = dataStorage.get(line, `${altPre}_bb`); updatePoints(line, conn, bb, altBB, pre, altPre); } }; const findConnectors = (elems = []) => { const dataStorage = svgCanvas.getDataStorage(); const connectors = document.querySelectorAll("[id^=\"conn_\"]"); connections = []; for (const connector of connectors) { let addThis = false; const parts = []; for (const [i, pos] of ["start", "end"].entries()) { let part = dataStorage.get(connector, `c_${pos}`); if (!part) { part = document.getElementById(connector.attributes["se:connector"].value.split(" ")[i]); dataStorage.put(connector, `c_${pos}`, part.id); dataStorage.put(connector, `${pos}_bb`, svgCanvas.getStrokedBBox([part])); } else part = document.getElementById(part); parts.push(part); } for (let i = 0; i < 2; i++) { const cElem = parts[i]; const parents = svgCanvas.getParents(cElem?.parentNode); for (const el of parents) if (elems.includes(el)) { addThis = true; break; } if (!cElem || !cElem.parentNode) { connector.remove(); continue; } if (elems.includes(cElem) || addThis) { const bb = svgCanvas.getStrokedBBox([cElem]); connections.push({ elem: cElem, connector, is_start: i === 0, start_x: bb.x, start_y: bb.y }); } } } }; /** * Updates the connectors based on selected elements. * @param {Element[]} [elems] - Optional array of selected elements. * @returns {void} */ const updateConnectors = (elems) => { const dataStorage = svgCanvas.getDataStorage(); findConnectors(elems); if (connections.length) for (const conn of connections) { const { elem, connector: line, is_start: isStart, start_x: startX, start_y: startY } = conn; const pre = isStart ? "start" : "end"; const bb = svgCanvas.getStrokedBBox([elem]); bb.x = startX; bb.y = startY; dataStorage.put(line, `${pre}_bb`, bb); const altPre = isStart ? "end" : "start"; const bb2 = dataStorage.get(line, `${altPre}_bb`); const srcX = bb2?.x + bb2?.width / 2; const srcY = bb2?.y + bb2?.height / 2; const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); setPoint(line, isStart ? 0 : "end", pt.x, pt.y, true); const pt2 = getBBintersect(pt.x, pt.y, dataStorage.get(line, `${altPre}_bb`), getOffset(altPre, line)); setPoint(line, isStart ? "end" : 0, pt2.x, pt2.y, true); } }; /** * Do on reset. * @returns {void} */ const reset = () => { const dataStorage = svgCanvas.getDataStorage(); svgCanvas.getSvgContent().querySelectorAll("*").forEach((element) => { const conn = element.getAttributeNS(seNs, "connector"); if (conn) { const connData = conn.split(" "); const sbb = svgCanvas.getStrokedBBox([getElement(connData[0])]); const ebb = svgCanvas.getStrokedBBox([getElement(connData[1])]); dataStorage.put(element, "c_start", connData[0]); dataStorage.put(element, "c_end", connData[1]); dataStorage.put(element, "start_bb", sbb); dataStorage.put(element, "end_bb", ebb); svgCanvas.getEditorNS(true); } }); }; reset(); return { name: svgEditor.i18next.t(`${name}:name`), callback() { const buttonTemplate = document.createElement("template"); buttonTemplate.innerHTML = ` <se-button id="tool_connect" title="${`${name}:buttons.0.title`}" src="conn.svg"></se-button> `; $id("tools_left").append(buttonTemplate.content.cloneNode(true)); $click($id("tool_connect"), () => { if (this.leftPanel.updateLeftPanel("tool_connect")) svgCanvas.setMode("connector"); }); }, mouseDown(opts) { const dataStorage = svgCanvas.getDataStorage(); const svgContent = svgCanvas.getSvgContent(); const { event: e, start_x: startX, start_y: startY } = opts; const mode = svgCanvas.getMode(); const { curConfig: { initStroke } } = svgEditor.configObj; if (mode === "connector") { if (started) return void 0; const mouseTarget = e.target; if (svgCanvas.getParents(mouseTarget.parentNode).includes(svgContent)) { startElem = svgCanvas.getClosest(mouseTarget.parentNode, "foreignObject") || mouseTarget; const bb = svgCanvas.getStrokedBBox([startElem]); const x = bb.x + bb.width / 2; const y = bb.y + bb.height / 2; started = true; curLine = addSVGElementsFromJson({ element: "polyline", attr: { id: "conn_" + svgCanvas.getNextId(), points: `${x},${y} ${x},${y} ${startX},${startY}`, stroke: initStroke.color === "none" ? "none" : `#${initStroke.color}`, "stroke-width": !startElem.stroke_width || startElem.stroke_width === 0 ? initStroke.width : startElem.stroke_width, fill: "none", opacity: initStroke.opacity, style: "pointer-events:none" } }); dataStorage.put(curLine, "start_bb", bb); } return { started: true }; } if (mode === "select") findConnectors(opts.selectedElements); }, mouseMove(opts) { if (connections.length === 0) return; const dataStorage = svgCanvas.getDataStorage(); const zoom = svgCanvas.getZoom(); const x = opts.mouse_x / zoom; const y = opts.mouse_y / zoom; /** @todo We have a concern if startX or startY are undefined */ if (!startX || !startY) return; const diffX = x - startX; const diffY = y - startY; const mode = svgCanvas.getMode(); if (mode === "connector" && started) { const pt = getBBintersect(x, y, dataStorage.get(curLine, "start_bb"), getOffset("start", curLine)); startX = pt.x; startY = pt.y; setPoint(curLine, 0, pt.x, pt.y, true); setPoint(curLine, "end", x, y, true); } else if (mode === "select") { for (const elem of svgCanvas.getSelectedElements()) if (elem && dataStorage.has(elem, "c_start")) { svgCanvas.removeFromSelection([elem]); elem.transform.baseVal.clear(); } if (connections.length) updateLine(diffX, diffY); } }, mouseUp(opts) { const dataStorage = svgCanvas.getDataStorage(); const svgContent = svgCanvas.getSvgContent(); const { event: e } = opts; let mouseTarget = e.target; if (svgCanvas.getMode() !== "connector") return void 0; const fo = svgCanvas.getClosest(mouseTarget.parentNode, "foreignObject"); if (fo) mouseTarget = fo; const isInSvgContent = svgCanvas.getParents(mouseTarget.parentNode).includes(svgContent); if (mouseTarget === startElem) { started = true; return { keep: true, element: null, started }; } if (!isInSvgContent) { curLine?.remove(); started = false; return { keep: false, element: null, started }; } endElem = mouseTarget; const startId = startElem?.id || ""; const endId = endElem?.id || ""; const connStr = `${startId} ${endId}`; const altStr = `${endId} ${startId}`; if (Array.from(document.querySelectorAll("[id^=\"conn_\"]")).filter((conn) => conn.getAttributeNS(seNs, "connector") === connStr || conn.getAttributeNS(seNs, "connector") === altStr).length) { curLine.remove(); return { keep: false, element: null, started: false }; } const bb = svgCanvas.getStrokedBBox([endElem]); const pt = getBBintersect(startX, startY, bb, getOffset("start", curLine)); setPoint(curLine, "end", pt.x, pt.y, true); dataStorage.put(curLine, "c_start", startId); dataStorage.put(curLine, "c_end", endId); dataStorage.put(curLine, "end_bb", bb); curLine.setAttributeNS(seNs, "se:connector", connStr); curLine.setAttribute("opacity", 1); svgCanvas.addToSelection([curLine]); svgCanvas.moveToBottomSelectedElement(); selectorManager.requestSelector(curLine).showGrips(false); started = false; return { keep: true, element: curLine, started }; }, selectedChanged(opts) { const dataStorage = svgCanvas.getDataStorage(); if (!svgCanvas.getSvgContent().querySelectorAll("[id^=\"conn_\"]").length) return; if (svgCanvas.getMode() === "connector") svgCanvas.setMode("select"); const { elems: selElems } = opts; for (const elem of selElems) if (elem && dataStorage.has(elem, "c_start")) { selectorManager.requestSelector(elem).showGrips(false); showPanel(opts.selectedElement && !opts.multiselected); } else showPanel(false); updateConnectors(svgCanvas.getSelectedElements()); }, elementChanged(opts) { const dataStorage = svgCanvas.getDataStorage(); let [elem] = opts.elems; if (!elem) return; if (elem.tagName === "svg" && elem.id === "svgcontent") reset(); const { markerStart, markerMid, markerEnd } = elem.attributes; if (markerStart || markerMid || markerEnd) { curLine = elem; dataStorage.put(elem, "start_off", Boolean(markerStart)); dataStorage.put(elem, "end_off", Boolean(markerEnd)); if (elem.tagName === "line" && markerMid) { const { x1, x2, y1, y2, id } = elem.attributes; const midPt = `${(Number(x1.value) + Number(x2.value)) / 2},${(Number(y1.value) + Number(y2.value)) / 2}`; const pline = addSVGElementsFromJson({ element: "polyline", attr: { points: `${x1.value},${y1.value} ${midPt} ${x2.value},${y2.value}`, stroke: elem.getAttribute("stroke"), "stroke-width": elem.getAttribute("stroke-width"), "marker-mid": markerMid.value, fill: "none", opacity: elem.getAttribute("opacity") || 1 } }); elem.insertAdjacentElement("afterend", pline); elem.remove(); svgCanvas.clearSelection(); pline.id = id.value; svgCanvas.addToSelection([pline]); elem = pline; } } if (elem?.id.startsWith("conn_")) { const start = getElement(dataStorage.get(elem, "c_start")); updateConnectors([start]); } else updateConnectors(svgCanvas.getSelectedElements()); }, IDsUpdated(input) { const remove = []; input.elems.forEach(function(elem) { if ("se:connector" in elem.attr) { elem.attr["se:connector"] = elem.attr["se:connector"].split(" ").map(function(oldID) { return input.changes[oldID]; }).join(" "); if (!/. ./.test(elem.attr["se:connector"])) remove.push(elem.attr.id); } }); return { remove }; }, toolButtonStateUpdate(opts) { const button = document.getElementById("tool_connect"); if (opts.nostroke && button.pressed === true) svgEditor.clickSelect(); button.disabled = opts.nostroke; } }; } }; //#endregion export { ext_connector_default as default }; //# sourceMappingURL=ext-connector.js.map