diff --git a/web/include/actsvg/web/tools/scripts/template/build-and-run.sh b/web/include/actsvg/web/tools/scripts/template/build-and-run.sh index a09eeed..7c111b9 100755 --- a/web/include/actsvg/web/tools/scripts/template/build-and-run.sh +++ b/web/include/actsvg/web/tools/scripts/template/build-and-run.sh @@ -3,7 +3,7 @@ # Change to the directory of the Bash script cd "$(dirname "$0")" -# Run the build.py script +# Run the build script python3 rebuild.py # Start the Python 3 HTTP server diff --git a/web/include/actsvg/web/tools/scripts/template/favicon.ico b/web/include/actsvg/web/tools/scripts/template/favicon.ico new file mode 100644 index 0000000..634d3f3 Binary files /dev/null and b/web/include/actsvg/web/tools/scripts/template/favicon.ico differ diff --git a/web/include/actsvg/web/tools/scripts/template/index.html b/web/include/actsvg/web/tools/scripts/template/index.html index e69e252..ef5a3e2 100644 --- a/web/include/actsvg/web/tools/scripts/template/index.html +++ b/web/include/actsvg/web/tools/scripts/template/index.html @@ -2,6 +2,9 @@ + + + SVG Viewer @@ -12,8 +15,10 @@

Select SVGs

-
- +
+ + +
diff --git a/web/include/actsvg/web/tools/scripts/template/script.js b/web/include/actsvg/web/tools/scripts/template/script.js index 5776b8d..a13f42a 100644 --- a/web/include/actsvg/web/tools/scripts/template/script.js +++ b/web/include/actsvg/web/tools/scripts/template/script.js @@ -1,64 +1,86 @@ -// Initialize the page by loading a form with a button for each SVG available. -// This is done by reading the paths in the config.json file. -document.addEventListener("DOMContentLoaded", function () { - fetch('./config.json').then(response => response.json()).then(json =>{ - load_form(json); - }); -}); +let SVGResult = document.getElementById('result-svg'); +let formContainer = document.getElementById('formContainer'); +let SVGContainer = document.getElementById("result-div"); + +// Given a collection of paths, returns a collection of containing the respective file text. +async function readFiles(paths){ + let result = [] + for (const path of paths) { + let data = await (await fetch(path)).text(); + result.push(data); + } + return result; +} + +// Removes the and tag to obtain its content. +function removeSVGTag(data){ + let startTag = /]*>/i; + let endTag = /<\/svg>/i; + data = data.replace(startTag, ''); + data = data.replace(endTag, ''); + return data; +} + +async function SVGContentMerge(paths){ + let contents = await readFiles(paths); + return contents.map(removeSVGTag).join('\n'); +} // Loads a form containing a button for each SVG available. function load_form(group) { - let form = document.createElement("form"); - form.id = "checkboxForm"; + let form = document.createElement('form'); + form.id = 'checkboxForm'; group.forEach(item =>{ - itemDiv = document.createElement("div"); - itemDiv.classList.add("form-item"); + itemDiv = document.createElement('div'); + itemDiv.classList.add('form-item'); let label = document.createElement("label"); itemDiv.appendChild(label); - let checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.name = "checkbox"; - checkbox.value = "./svgs/" + item; + let checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'checkbox'; + checkbox.value = './svgs/' + item; label.append(checkbox); - let span = document.createElement("span"); - span.textContent = get_name(item); + let span = document.createElement('span'); + span.textContent = item.replace('.svg', ''); label.appendChild(span); form.appendChild(itemDiv); }); - form.append(document.createElement("br")); + form.append(document.createElement('br')); // Create a button to apply the changes - let apply_button = document.createElement("input"); - apply_button.type = "button"; - apply_button.value = "Apply Selection"; - apply_button.onclick = getSelectedValues; - apply_button.classList.add("apply-button"); + let apply_button = document.createElement('input'); + apply_button.type = 'button'; + apply_button.value = 'Apply Selection'; + apply_button.onclick = applyChanges; + apply_button.classList.add('apply-button'); // Append the apply button to the form form.append(apply_button); // Append the form to the formContainer div - const formContainer = document.getElementById("formContainer"); formContainer.appendChild(form); } -// For removing the file extension. -function get_name(path){ - return path.replace(".svg", ""); -} +// Initialize the page by loading a form with a button for each SVG available. +// This is done by reading the paths in the config.json file. +document.addEventListener('DOMContentLoaded', function () { + fetch('./config.json').then(response => response.json()).then(json =>{ + load_form(json); + }); +}); // Updates the displayed SVG by combining the selected SVGs. -async function getSelectedValues() { - var checkboxes = document.getElementsByName("checkbox"); +async function applyChanges() { + var checkboxes = document.getElementsByName('checkbox'); var selectedValues = []; for (var i = 0; i < checkboxes.length; i++) { @@ -67,52 +89,76 @@ async function getSelectedValues() { } } - selectedValues = selectedValues.reverse(); - let svg = await combineSVGS(selectedValues); - var resultDiv = document.getElementById('result'); - resultDiv.innerHTML = svg; + paths = selectedValues.reverse(); + SVGResult.innerHTML = await SVGContentMerge(paths); } -// Removes the and tag to obtain its content. -function removeSVGTag(data){ - let startTag = /]*>/i; - let endTag = /<\/svg>/i; - data = data.replace(startTag, ''); - data = data.replace(endTag, ''); - return data; + +// For navigation on the svg: + +//--- Adjustable paramters: +// Speed of zoom on scroll. +const zoomFactor = 1.1; +// To calculate the maximum width and height of the viewbox. +const maxHalfLengths = { x: 3000, y: 3000 }; +//---- + +let pivot = { x: 0, y: 0 }; +let position = { x: 0, y: 0 }; +let halfSize = { x: 300, y: 300 }; +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); + +setViewBox(); + +SVGResult.addEventListener("mousemove", onMousemove); +SVGResult.addEventListener("mousedown", onMousedown); +SVGResult.addEventListener("wheel", onWheel); + +const mouse = { + position: { x: 0, y: 0 }, + isDown: false, +}; + +function viewBoxDim(){ + return {x: position.x - halfSize.x, y: position.y - halfSize.y, w: 2*halfSize.x, h: 2*halfSize.y}; } -// Given a collection of paths, returns a collection of containing the respective file text. -async function readFiles(paths){ - let result = [] - for (const path of paths) { - try{ - let data = await (await fetch(path)).text(); - result.push(data); - } - catch(err) - { - console.error(err); - } +function onMousedown(e) { + mouse.position = screenToViewBoxPosition(e.pageX, e.pageY); + window.addEventListener("mouseup", onMouseup); + mouse.isDown = true; +} + +function setViewBox() { + let dim = viewBoxDim(); + SVGResult.setAttribute("viewBox", `${dim.x} ${dim.y} ${dim.w} ${dim.h}`); +} + +function screenToViewBoxPosition(screenX, screenY){ + return { + x: screenX * 2 * halfSize.x/SVGResult.clientWidth, + y: screenY * 2 * halfSize.y/SVGResult.clientHeight } - return result; } -// Creates an SVG from a list of svg objects by joining and appending svg start and end tags. -function createSVG(contents){ - let result = [] - result.push('\n'); - contents.forEach(e => { - result.push(e); - result.push('\n'); - }); - result.push(''); - return result.join(''); +function onMousemove(e) { + if (mouse.isDown) { + let pos = screenToViewBoxPosition(e.pageX, e.pageY); + let dx = (pos.x - mouse.position.x); + let dy = (pos.y - mouse.position.y); + position = {x: clamp(position.x - dx, -maxHalfLengths.x, maxHalfLengths.x), y: clamp(position.y - dy, -maxHalfLengths.y, maxHalfLengths.y)}; + mouse.position = pos; + setViewBox(); + } } -// Given a list of paths to svgs, returns an svg of their combined content. -async function combineSVGS(paths){ - let contents = await readFiles(paths); - contents = contents.map(removeSVGTag); - return createSVG(contents); +function onMouseup(e) { + window.removeEventListener("mouseup", onMouseup); + mouse.isDown = false; +} + +function onWheel(e) { + const scale = e.deltaY > 0 ? zoomFactor : 1/zoomFactor; + halfSize = {x: clamp(halfSize.x * scale, 1, maxHalfLengths.x), y: clamp(halfSize.y * scale, 1, maxHalfLengths.y)}; + setViewBox(); } diff --git a/web/include/actsvg/web/tools/scripts/template/styles.css b/web/include/actsvg/web/tools/scripts/template/styles.css index 9dcf5cf..5ba90ca 100644 --- a/web/include/actsvg/web/tools/scripts/template/styles.css +++ b/web/include/actsvg/web/tools/scripts/template/styles.css @@ -59,9 +59,15 @@ font-family: monospace; } -.result{ - box-shadow: 0 4px 17px rgba(0, 0, 0, 0.35); - width: 60%; - height: 60%; +.result-div{ + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); + height: 40%; + width: 40%; display: inline-block; + cursor: grab; } + +.result-div svg g{ + cursor: pointer; +} + diff --git a/web/include/actsvg/web/web_builder.hpp b/web/include/actsvg/web/web_builder.hpp index facdcf5..24808f4 100644 --- a/web/include/actsvg/web/web_builder.hpp +++ b/web/include/actsvg/web/web_builder.hpp @@ -63,6 +63,8 @@ bool alphanumericCompare(const svg::object& svg1, const svg::object& svg2) { }; /// @brief Class for generating a web page to view and merge svgs. +/// @note When used a debugging tool and rebuilding mutliple times, +/// if the webpage does not refresh as expected it is likely caused by browser caching. class web_builder{ public: @@ -73,6 +75,8 @@ class web_builder{ /// @param svgs the svgs avaible for selection on the web page. /// @param order_comparator a compartor function to determine /// the display order of the svgs. + /// @note When used a debugging tool and rebuilding mutliple times, + /// if the webpage does not refresh as expected it is likely caused by browser caching. template void build(const std::filesystem::path& output_directory, const iterator_t& svgs, comparison_function order_comparator) { @@ -89,6 +93,8 @@ class web_builder{ /// @param svgs the svgs avaible for selection on the web page. /// @note uses an alpha-numeric comparator on the svgs' ids /// to determine the display order. + /// @note When used a debugging tool and rebuilding mutliple times, + /// if the webpage does not refresh as expected it is likely caused by browser caching. template void build(const std::filesystem::path& output_directory, const iterator_t& svgs) { diff --git a/web/include/actsvg/web/webpage_text.hpp b/web/include/actsvg/web/webpage_text.hpp index 3075ce8..b528a8d 100644 --- a/web/include/actsvg/web/webpage_text.hpp +++ b/web/include/actsvg/web/webpage_text.hpp @@ -16,6 +16,9 @@ const std::string index_text = R"( + + + SVG Viewer @@ -26,8 +29,10 @@ const std::string index_text = R"( -
- +
+ + +
@@ -37,67 +42,89 @@ const std::string index_text = R"( )"; -const std::string script_text = R"(// Initialize the page by loading a form with a button for each SVG available. -// This is done by reading the paths in the config.json file. -document.addEventListener("DOMContentLoaded", function () { - fetch('./config.json').then(response => response.json()).then(json =>{ - load_form(json); - }); -}); +const std::string script_text = R"(let SVGResult = document.getElementById('result-svg'); +let formContainer = document.getElementById('formContainer'); +let SVGContainer = document.getElementById("result-div"); + +// Given a collection of paths, returns a collection of containing the respective file text. +async function readFiles(paths){ + let result = [] + for (const path of paths) { + let data = await (await fetch(path)).text(); + result.push(data); + } + return result; +} + +// Removes the and tag to obtain its content. +function removeSVGTag(data){ + let startTag = /]*>/i; + let endTag = /<\/svg>/i; + data = data.replace(startTag, ''); + data = data.replace(endTag, ''); + return data; +} + +async function SVGContentMerge(paths){ + let contents = await readFiles(paths); + return contents.map(removeSVGTag).join('\n'); +} // Loads a form containing a button for each SVG available. function load_form(group) { - let form = document.createElement("form"); - form.id = "checkboxForm"; + let form = document.createElement('form'); + form.id = 'checkboxForm'; group.forEach(item =>{ - itemDiv = document.createElement("div"); - itemDiv.classList.add("form-item"); + itemDiv = document.createElement('div'); + itemDiv.classList.add('form-item'); let label = document.createElement("label"); itemDiv.appendChild(label); - let checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.name = "checkbox"; - checkbox.value = "./svgs/" + item; + let checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'checkbox'; + checkbox.value = './svgs/' + item; label.append(checkbox); - let span = document.createElement("span"); - span.textContent = get_name(item); + let span = document.createElement('span'); + span.textContent = item.replace('.svg', ''); label.appendChild(span); form.appendChild(itemDiv); }); - form.append(document.createElement("br")); + form.append(document.createElement('br')); // Create a button to apply the changes - let apply_button = document.createElement("input"); - apply_button.type = "button"; - apply_button.value = "Apply Selection"; - apply_button.onclick = getSelectedValues; - apply_button.classList.add("apply-button"); + let apply_button = document.createElement('input'); + apply_button.type = 'button'; + apply_button.value = 'Apply Selection'; + apply_button.onclick = applyChanges; + apply_button.classList.add('apply-button'); // Append the apply button to the form form.append(apply_button); // Append the form to the formContainer div - const formContainer = document.getElementById("formContainer"); formContainer.appendChild(form); } -// For removing the file extension. -function get_name(path){ - return path.replace(".svg", ""); -} +// Initialize the page by loading a form with a button for each SVG available. +// This is done by reading the paths in the config.json file. +document.addEventListener('DOMContentLoaded', function () { + fetch('./config.json').then(response => response.json()).then(json =>{ + load_form(json); + }); +}); // Updates the displayed SVG by combining the selected SVGs. -async function getSelectedValues() { - var checkboxes = document.getElementsByName("checkbox"); +async function applyChanges() { + var checkboxes = document.getElementsByName('checkbox'); var selectedValues = []; for (var i = 0; i < checkboxes.length; i++) { @@ -106,54 +133,78 @@ async function getSelectedValues() { } } - selectedValues = selectedValues.reverse(); - let svg = await combineSVGS(selectedValues); - var resultDiv = document.getElementById('result'); - resultDiv.innerHTML = svg; + paths = selectedValues.reverse(); + SVGResult.innerHTML = await SVGContentMerge(paths); } -// Removes the and tag to obtain its content. -function removeSVGTag(data){ - let startTag = /]*>/i; - let endTag = /<\/svg>/i; - data = data.replace(startTag, ''); - data = data.replace(endTag, ''); - return data; + +// For navigation on the svg: + +//--- Adjustable paramters: +// Speed of zoom on scroll. +const zoomFactor = 1.1; +// To calculate the maximum width and height of the viewbox. +const maxHalfLengths = { x: 3000, y: 3000 }; +//---- + +let pivot = { x: 0, y: 0 }; +let position = { x: 0, y: 0 }; +let halfSize = { x: 300, y: 300 }; +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); + +setViewBox(); + +SVGResult.addEventListener("mousemove", onMousemove); +SVGResult.addEventListener("mousedown", onMousedown); +SVGResult.addEventListener("wheel", onWheel); + +const mouse = { + position: { x: 0, y: 0 }, + isDown: false, +}; + +function viewBoxDim(){ + return {x: position.x - halfSize.x, y: position.y - halfSize.y, w: 2*halfSize.x, h: 2*halfSize.y}; } -// Given a collection of paths, returns a collection of containing the respective file text. -async function readFiles(paths){ - let result = [] - for (const path of paths) { - try{ - let data = await (await fetch(path)).text(); - result.push(data); - } - catch(err) - { - console.error(err); - } +function onMousedown(e) { + mouse.position = screenToViewBoxPosition(e.pageX, e.pageY); + window.addEventListener("mouseup", onMouseup); + mouse.isDown = true; +} + +function setViewBox() { + let dim = viewBoxDim(); + SVGResult.setAttribute("viewBox", `${dim.x} ${dim.y} ${dim.w} ${dim.h}`); +} + +function screenToViewBoxPosition(screenX, screenY){ + return { + x: screenX * 2 * halfSize.x/SVGResult.clientWidth, + y: screenY * 2 * halfSize.y/SVGResult.clientHeight } - return result; } -// Creates an SVG from a list of svg objects by joining and appending svg start and end tags. -function createSVG(contents){ - let result = [] - result.push('\n'); - contents.forEach(e => { - result.push(e); - result.push('\n'); - }); - result.push(''); - return result.join(''); +function onMousemove(e) { + if (mouse.isDown) { + let pos = screenToViewBoxPosition(e.pageX, e.pageY); + let dx = (pos.x - mouse.position.x); + let dy = (pos.y - mouse.position.y); + position = {x: clamp(position.x - dx, -maxHalfLengths.x, maxHalfLengths.x), y: clamp(position.y - dy, -maxHalfLengths.y, maxHalfLengths.y)}; + mouse.position = pos; + setViewBox(); + } } -// Given a list of paths to svgs, returns an svg of their combined content. -async function combineSVGS(paths){ - let contents = await readFiles(paths); - contents = contents.map(removeSVGTag); - return createSVG(contents); +function onMouseup(e) { + window.removeEventListener("mouseup", onMouseup); + mouse.isDown = false; +} + +function onWheel(e) { + const scale = e.deltaY > 0 ? zoomFactor : 1/zoomFactor; + halfSize = {x: clamp(halfSize.x * scale, 1, maxHalfLengths.x), y: clamp(halfSize.y * scale, 1, maxHalfLengths.y)}; + setViewBox(); } )"; @@ -218,12 +269,18 @@ const std::string css_text = R"(.app{ font-family: monospace; } -.result{ - box-shadow: 0 4px 17px rgba(0, 0, 0, 0.35); - width: 60%; - height: 60%; +.result-div{ + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); + height: 40%; + width: 40%; display: inline-block; + cursor: grab; } + +.result-div svg g{ + cursor: pointer; +} + )"; const std::string rebuild_text = R"(import os