commit a5ae1fcf191927a4586442c32354dffec2407ad1
parent 8e564eba12c8ae0ccc7ebd8188e7da70e1a5b14f
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date: Mon, 12 Aug 2024 16:40:01 -0700
Entirely rewrite floorplan editor
The previous interface used coordinates relative to many different
things (the screen, the document, absolute, relative, etc. etc.)
which was the main source of problems.
The new interface uses a single type of coordinate: An absolute
position in the SVG document (which can be used as a relative offset
too). It also uses the svg.js library (aswell as the svg.panzoom.js
plugin for it) to help with managing the SVG. Introducing third-party
libraries isn't something I do lightly, but I think it is worth it.
Diffstat:
11 files changed, 423 insertions(+), 549 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,6 +1,27 @@
prefix = /var/www/htdocs/www.spaceplanner.app
+libs =\
+ github.com/svgdotjs/svg.js@3.2.4 \
+ github.com/svgdotjs/svg.panzoom.js@2.1.2
install:
- rsync -va --del files/ ${prefix}
+ rsync "$$(libnames ${libs} | sed 's/^/--exclude=/')" -va --del files/ ${prefix}
-.PHONY: install
+uninstall:
+ rm -rf ${prefix}/*
+
+install_libs:
+ for lib in $$(libnames ${libs}); do \
+ rm -rf ${prefix}/lib/"$$lib"; \
+ mkdir -p ${prefix}/lib/"$$lib"; \
+ (cd "lib/$$lib"/src/src && pax -w .) | (cd ${prefix}/lib/"$$lib" && pax -r); \
+ done
+
+update_libs:
+.for lib in ${libs}
+ getlib "${lib}"
+.endfor
+
+clean:
+ rm -rf lib/
+
+.PHONY: install install_libs update_libs
diff --git a/files/floorplans/floorplan/editor.js b/files/floorplans/floorplan/editor.js
@@ -0,0 +1,179 @@
+import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js"
+
+SVG.extend(SVG.Element, {
+ select: function() {
+ console.debug("User selected", this.node)
+ this.root().find(".last_selected")
+ .removeClass("last_selected")
+ this.root().find(".selected")
+ .removeClass("selected")
+ .addClass("last_selected")
+ return this.addClass("selected")
+ },
+
+ findOneMax: function(selector) {
+ let results = this.find(selector)
+ if (results.length > 1) {
+ throw new Error("Found more than one element")
+ }
+ if (results.length == 1)
+ return results[0]
+ return undefined
+ }
+})
+
+SVG.extend(SVG.Circle, {
+ // Maybe this already exists?
+ pos: function() {
+ let attrs = this.attr(["cx", "cy"])
+ return { x: attrs.cx, y: attrs.cy }
+ }
+})
+
+export class FloorplanEditor {
+ constructor(svg) {
+ this.draw = svg
+ this.mode
+ this.modes = {}
+ this.mode_states = {}
+
+ let floorplan = this.draw.group().attr({ id: "floorplan" })
+ floorplan.group().attr({ id: "walls" }) // lines
+ floorplan.group().attr({ id: "points" }) // circles
+ }
+
+ addMode(name, mode) {
+ if (this.modes[name]) {
+ throw new Error("Mode already exists")
+ }
+ if (!mode) {
+ throw new Error("No mode")
+ }
+
+ this.modes[name] = {}
+ for (let key in mode) {
+ if (key !== "handlers") {
+ this.modes[name][key] = mode[key]
+ }
+ }
+
+ // to pass use in another function
+ let state = this
+ this.modes[name].handlers = {}
+ for (let type in mode.handlers) {
+ this.modes[name]["handlers"][type] = []
+
+ let a = mode.handlers[type]
+ if (typeof a === "function") {
+ a = [ a ]
+ } else if (typeof a !== "object") {
+ delete this.modes[name]
+ throw new Error("Expected function or object")
+ }
+
+ for (let i in a) {
+ console.debug("Create mode handler", name, type, a[i])
+ let f = function(event) {
+ // NOTE: Maybe handler states should be local to each mode too?
+ return a[i](event, state, state["mode_states"][f])
+ }
+ this["mode_states"][f] = {}
+ this["modes"][name]["handlers"][type].push(f)
+ }
+ }
+
+ console.log("Add mode", mode)
+ return this
+ }
+
+ useMode(newmode) {
+ if (newmode && !this.modes[newmode]) {
+ throw new Error("'" + newmode + "': Invalid mode")
+ }
+
+ if (newmode === this.mode) {
+ return this
+ }
+
+ if (this.mode) {
+ remove_mode_handlers(this.draw, this.modes[this.mode].handlers)
+ }
+
+ if (newmode) {
+ let points = this.draw.findOne("#points")
+ if (this.modes[newmode].points) {
+ points.attr("visibility", null)
+ } else {
+ points.attr("visibility", "hidden")
+ }
+ add_mode_handlers(this.draw, this.modes[newmode].handlers)
+ }
+
+ this.mode = newmode
+ console.log("Mode", this.mode)
+ return this
+ }
+
+ addPoint(point) {
+ let already = this.pointAt(point)
+ if (already) {
+ return already.select()
+ }
+ return this.draw.findOne("#points")
+ .circle(4)
+ .addClass("point")
+ .move(point.x, point.y)
+ .select()
+ }
+
+ pointAt(point) {
+ let pointInside = null
+ this.draw.findOne("#points")
+ .children().each(function(child) {
+ if (child.inside(point.x, point.y)) {
+ pointInside = child
+ }
+ })
+ return pointInside
+ }
+
+ addWall() {
+ let points = this.selectedPoints()
+ return this.draw.find("#walls")
+ .line(points.b.x, points.b.y, points.a.x, points.a.y)
+ .stroke("black")
+ }
+
+ selectedPoints() {
+ return {
+ a: this.selectedPoint(),
+ b: this.lastSelectedPoint()
+ }
+ }
+
+ selectedPoint() {
+ return this.draw.findOneMax("#points > .selected").pos()
+ }
+
+ lastSelectedPoint() {
+ return this.draw.findOneMax("#points > .last_selected").pos()
+ }
+}
+
+function remove_mode_handlers(target, mode_handlers) {
+ for (let event in mode_handlers) {
+ for (let handler in mode_handlers[event]) {
+ console.debug("Remove mode handler", event, handler, "from", target)
+ target.off(event, mode_handlers[event][handler])
+ }
+ }
+}
+
+function add_mode_handlers(target, mode_handlers) {
+ for (let event in mode_handlers) {
+ for (let handler in mode_handlers[event]) {
+ console.debug("Add mode handler", event, handler, "to", target)
+ target.on(event, mode_handlers[event][handler])
+ }
+ }
+}
diff --git a/files/floorplans/floorplan/graphics.js b/files/floorplans/floorplan/graphics.js
@@ -1,31 +0,0 @@
-export const svg = {
- element: function(name) {
- return document.createElementNS("http://www.w3.org/2000/svg", name)
- },
-
- line: function(x1, y1, x2, y2) {
- let line = svg.element("line")
- line.setAttribute("x1", x1)
- line.setAttribute("y1", y1)
- line.setAttribute("x2", x2)
- line.setAttribute("y2", y2)
- return line
- },
-
- transform: function(svg, element, method, ...values) {
- let t = svg.createSVGTransform()
- console.debug("transform", method, values)
- t[method](...values)
-
- element.transform.baseVal.appendItem(t)
- return t
- }
-}
-
-export function point(x, y) {
- return { x: x, y: y }
-}
-
-export function rect(x, y, width, height) {
- return { x: x, y: y, width: width, height: height }
-}
diff --git a/files/floorplans/floorplan/index.html b/files/floorplans/floorplan/index.html
@@ -3,29 +3,13 @@
<title>Spaceplanner - Floorplans</title>
<link rel="stylesheet" type="text/css" href="/css/main.css">
<link rel="stylesheet" type="text/css" href="./main.css">
- <!-- Not sure why I can't include it directly in floorplan svg, but this can work -->
<link rel="stylesheet" type="text/css" href="./svg.css"/>
<script type="module" src="./main.js"></script>
</head>
<html>
- <body>
- <h1 class="fp_name">Floorplan</h1>
-
- <div id="mode_selector"></div>
-
- <svg id="floorplan" width="4096" height="4096" viewbox="0 0 800 800">
- <script type="module" href="./svg.js"></script>
- <title>Floorplan</title>
-
- <!-- Everything here "scales" -->
- <g id="svg_data" class="scales">
- <polyline id="walls"/>
- <g id="furnature"></g>
- </g>
-
- <g id="svg_ui">
- <g id="grid" class="scales"></g>
- </g>
- </svg>
+ <body id="floorplan_container">
+ <header>
+ <h1 class="fp_name">Floorplan</h1>
+ </header>
</body>
</html>
diff --git a/files/floorplans/floorplan/main.css b/files/floorplans/floorplan/main.css
@@ -1,13 +1,52 @@
+body {
+ display: flex;
+ flex-flow: column;
+ box-sizing: border-box;
+ height: 100vh;
+ background-color: #F3F3F3;
+ margin: 0;
+ padding: min(2vh, 2vw);
+}
+
+header > h1 {
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+}
+
+header > .toolbar {
+ list-style: none;
+ padding: 0;
+ margin: .25em;
+}
+
svg {
- border: thin solid black;
+ flex: 1;
+ border: ridge thin;
+ background-color: white;
+}
+
+.modes_selector > ul {
+ display: inline;
+ padding: 0;
+ list-style: none;
+}
+
+.modes_selector > ul > li {
+ display: inline;
+}
+
+.mode_selector {
+ margin: .25em;
+ background: none;
+ border: none;
+ border-bottom: medium solid lightgrey;
+}
+
+.mode_selector:hover {
+ border-color: grey;
}
-#debug_cursor_text {
- position: absolute;
- top: 5%;
- left: 5%;
- border-radius: .1em;
- padding: 1em;
- background-color: rgba(0, 0, 0, .85);
- color: red;
+.mode_selector.selected {
+ border-color: blue;
}
diff --git a/files/floorplans/floorplan/main.js b/files/floorplans/floorplan/main.js
@@ -1,13 +1,118 @@
-import * as api from "/lib/api.js"
-import * as etc from "/lib/etc.js"
+import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js"
+import "/lib/github.com/svgdotjs/svg.panzoom.js/svg.panzoom.js"
import * as ui from "/lib/ui.js"
+import { FloorplanEditor as Editor } from "./editor.js"
+const buttons = {
+ left: 0,
+ middle: 1,
+ right: 2
+}
+
function init() {
- etc.authorize()
- etc.bar()
-
let floorplan = (new URLSearchParams(new URL(document.URL).search)).get("name")
+ if (!floorplan) {
+ document.location.href = "/floorplans"
+ }
document.querySelector("h1").textContent = floorplan
+
+ let draw = SVG()
+ .addTo("#floorplan_container")
+ .viewbox("0 0 400 400")
+ .panZoom({
+ panButton: buttons.right,
+ zoomMin: .25,
+ zoomMax: 4,
+ zoomFactor: .5
+ })
+
+ let editor = new Editor(draw)
+ for (let mode in modes) {
+ editor.addMode(mode, modes[mode])
+ }
+ editor.useMode("testing")
+
+ let toolbar = document.querySelector("header")
+ .appendChild(document.createElement("ul"))
+ toolbar.classList.add("toolbar")
+ toolbar.appendChild(modesSelector(editor, "Modes:"))
+}
+
+function modesSelector(editor, text) {
+ let form = document.createElement("form")
+ form.classList.add("modes_selector")
+ if (text) {
+ form.appendChild(document.createTextNode(text))
+ }
+
+ let list = form.appendChild(document.createElement("ul"))
+ for (let mode in editor.modes) {
+ let selector = list.appendChild(document.createElement("li"))
+ .appendChild(ui.input(mode, "Switch to " + mode + " mode", {
+ attributes: { type: "button", value: mode },
+ handlers: { click: function(event) {
+ editor.useMode(event.target.name)
+ event.target.parentNode.parentNode
+ .querySelectorAll("li > .selected")
+ .forEach(function(sel) {
+ sel.classList.remove("selected")
+ })
+ event.target.classList.add("selected")
+ }}
+ }))
+ selector.classList.add("mode_selector")
+ if (mode == editor.mode) {
+ selector.classList.add("selected")
+ }
+ }
+
+ return form
+}
+
+let modes = {
+ none: {
+ handlers: {
+ contextmenu: preventDefaultHandler
+ }
+ },
+ testing: {
+ points: true,
+ handlers: {
+ /*
+ * To allow using right click for panZoom's panning
+ * (not sure if contextmenu is always right click
+ * though.
+ */
+ contextmenu: preventDefaultHandler,
+ click: addWallHandler
+ }
+ }
+}
+
+// click
+function addWallHandler(click, state) {
+ if (click.type !== "click") {
+ throw new Error("Expected click event")
+ }
+ if (click.shiftKey) {
+ return
+ }
+
+ state.addPoint(state.draw.point(click.clientX, click.clientY))
+ .on("click", function(event) {
+ if (event.shiftKey) {
+ this.select()
+ event.preventDefault()
+ }
+ })
+ if (state.draw.findOne("#points").children().length >= 2) {
+ state.addWall()
+ }
+ click.preventDefault()
+}
+
+function preventDefaultHandler(event) {
+ event.preventDefault()
}
-window.onload = etc.handle_wrap(init)
+window.onload = init
diff --git a/files/floorplans/floorplan/svg.css b/files/floorplans/floorplan/svg.css
@@ -1,28 +1,15 @@
-* {
- stroke-width: 6;
-}
+/* SVG element CSS */
-#walls {
- fill: none;
+#walls > line {
stroke: black;
+ stroke-width: 2;
}
-.preview {
+.point {
+ r: 2;
fill: lightgrey;
- stroke: lightgrey;
-}
-
-#grid_1 > line {
- stroke: black;
- stroke-width: 1;
-}
-
-#grid_2 > line {
- stroke: #FAFAFA;
- stroke-width: 1;
}
-#debug_cursor {
- fill: red;
- r: .25em;
+.point.selected {
+ fill: blue;
}
diff --git a/files/floorplans/floorplan/svg.js b/files/floorplans/floorplan/svg.js
@@ -1,461 +0,0 @@
-import * as graphics from "./graphics.js"
-import * as etc from "/lib/etc.js"
-import * as ui from "/lib/ui.js"
-
-/*
- * NOTE: This system is okay, but I was thinking of restructuring
- * it to look like this:
- * units =
- * { system: [ { value: 10, name: "inch" }, { value: 12, name: "foot" } ] }
- * because while it's a little more messy, it would be nice to have
- * each element build on the previous unit. Maybe it doesn't matter
- * though because I'll probably only ever hardcode units in like this.
- */
-const units = {
- // systems
- imperial: {
- inch: 10,
- foot: 10 * 12
- },
- metric: {
- meter: 254,
- centimeter: 25.4
- }
-}
-
-function init() {
- let state = {
- walls: etc.require_id("walls"),
- // [ { x: X, y: Y }, ... ]
- // I considered using a nested array instead, but I think this is more appropriate
- points: [],
- units: units["imperial"],
- svg: etc.require_id("floorplan"),
- svg_ui: etc.require_id("svg_ui"),
- svg_data: etc.require_id("svg_data"),
- scale: 1,
- flags: {}
- }
-
- init_modes(state, modes)
- switch_mode(state, "precise")
- let mode_selector = etc.require_id("mode_selector")
- for (let mode in modes) {
- mode_selector.append(ui.input(mode, "Switch to " + mode + " mode",
- { attributes: { type: "submit", value: mode }, handlers: { click: function() { switch_mode(state, mode) } } }
- ))
- }
- state.svg.before(mode_selector)
-
- let size = canvas_size(state)
- etc.require_id("floorplan").prepend(make_grid(state.units, size.width, size.height))
- let view = viewbox(state)
- set_scale(state, view.width / size.width)
-
- /*
- * Already called in set_scale, I suppose set_scale and the like should
- * set a flag, and whenever the user interface should be updated (say
- * on a timer) call this.
- */
- update_transforms(state)
-}
-
-function make_grid(units, width, height) {
- let grid = graphics.svg.element("g")
- grid.id = "grid"
- grid.setAttribute("class", "scales")
-
- let sorted = sort_units(units)
- for (let i in sorted) {
- let g = graphics.svg.element("g")
- g.id = "grid_" + (sorted.length - i)
-
- let unit = sorted[i]
- for (let x = unit.val; x < width; x += unit.val) {
- g.append(graphics.svg.line(x, 0, x, height))
- }
- for (let y = unit.val; y < height; y += unit.val) {
- g.append(graphics.svg.line(0, y, width, y))
- }
- grid.append(g)
- }
-
- return grid
-}
-
-function sort_units(units) {
- let a = []
- for (let unit in units) {
- a.push({ name: unit, val: units[unit] })
- }
- return a.sort(function(a, b) { return (a["val"] < b["val"]) ? -1 : 1 })
-}
-
-function init_modes(state, modes) {
- state["modes"] = {}
- for (let mode in modes) {
- state["modes"][mode] = {}
- for (let event_name in modes[mode]) {
- let a = modes[mode][event_name]
- if (typeof a === "function") {
- a = [ a ]
- } else if (typeof a !== "object") {
- throw new Error("Expected function or object")
- }
- state["modes"][mode][event_name] = []
- for (let i in a) {
- console.debug("init_modes", mode, event_name, i, a[i])
- state["modes"][mode][event_name].push(function(event) {
- return a[i](state, event)
- })
- }
- }
- }
-}
-
-function switch_mode(state, newmode) {
- console.debug("switch_mode", newmode, state)
- if (newmode && !modes[newmode]) {
- throw new Error("'" + newmode + "': Invalid mode")
- }
- if (newmode === state.mode) {
- return
- }
- if (state.mode) {
- remove_mode_handlers(state.svg, state.modes[state.mode])
- }
- if (newmode) {
- add_mode_handlers(state.svg, state.modes[newmode])
- }
- state.mode = newmode
-}
-
-let modes = {
- precise: {
- contextmenu: function(state, event) {
- event.preventDefault()
- },
- mousedown: viewbox_movement_handler,
- mousemove: [freedraw_move_handler, viewbox_movement_handler, debug_mouse_position_handler],
- mouseleave: viewbox_movement_handler,
- mouseup: viewbox_movement_handler,
- click: freedraw_click_handler,
- auxclick: viewbox_scale_handler
- },
- add: {
- mousemove: freedraw_move_handler,
- click: freedraw_click_handler
- }
-}
-
-// Listen on auxclick
-function viewbox_scale_handler(state, click) {
- if (click.button != 1) {
- return
- }
- let from = view_to_real(state, graphics.point(click.offsetX, click.offsetY))
- if (click.shiftKey) {
- set_scale(state, state.scale - .5, from)
- } else {
- set_scale(state, state.scale + .5, from)
- }
- click.preventDefault()
-}
-
-// Listen on mousedown, mouseup, mouseleave, and mousemove
-function viewbox_movement_handler(state, mouse) {
- if (mouse.type === "mouseleave") {
- state.flags.moving = false
- console.debug("Movement (left)", state.flags.moving)
- } else if (mouse.button === 2) {
- if (mouse.type === "mousedown") {
- state.flags.moving = true
- } else if (mouse.type === "mouseup") {
- state.flags.moving = false
- }
- console.debug("Movement (up/down)", state.flags.moving)
- }
- if (state.flags.moving && mouse.type === "mousemove") {
- let offset = view_to_real_scaled(state,
- graphics.point(mouse.movementX, mouse.movementY))
- let view = viewbox(state)
- let p = graphics.point(view.x - offset.x, view.y - offset.y)
- update_viewbox(state, graphics.rect(
- p.x, p.y, view.width, view.height)
- )
- update_movable(state)
-
- }
-}
-
-// Listen on mousemove
-function freedraw_move_handler(state, mouse) {
- let line = document.querySelector("line.preview")
- if (!line) {
- line = graphics.svg.element("line")
- line.setAttribute("class","preview scales")
- state.svg.append(line)
- update_scalable(state)
- }
-
- let last = last_point(state)
-
- let p = real_to_absolute(state, view_to_real(state, graphics.point(mouse.offsetX, mouse.offsetY)))
-
- state["preview_point"] = p
-
- if (!last) {
- line.setAttribute("hidden", true)
- } else {
- if (!mouse.shiftKey) {
- axis_snap(state["preview_point"], last)
- }
- line.removeAttribute("hidden")
- line.setAttribute("x1", last.x)
- line.setAttribute("y1", last.y)
- line.setAttribute("x2", p.x)
- line.setAttribute("y2", p.y)
- }
-}
-
-// Listen on click
-function freedraw_click_handler(state, click) {
- if (click.button != 0) {
- return
- }
- if (!state["preview_point"]) {
- throw new Error("Expected preview_point")
- }
- add_points(state, state["preview_point"])
- click.preventDefault()
-}
-
-// listen on mousemove
-function debug_mouse_position_handler(state, mouse) {
- let cursor = document.getElementById("debug_cursor")
- if (!cursor) {
- cursor = graphics.svg.element("circle")
- cursor.id = "debug_cursor"
- cursor.setAttribute("class", "moves")
- state.svg_ui.append(cursor)
- }
- let text = document.getElementById("debug_cursor_text")
- if (!text) {
- text = document.createElement("span")
- text.id = "debug_cursor_text"
- document.body.append(text)
- }
-
- let p = view_to_real_scaled(state, graphics.point(mouse.offsetX, mouse.offsetY))
- text.textContent = `Mouse: ${p["x"]}x${p["y"]}`
- cursor.setAttribute("cx", p.x)
- cursor.setAttribute("cy", p.y)
-}
-
-function remove_mode_handlers(element, mode_handlers) {
- for (let event in mode_handlers) {
- for (let handler in mode_handlers[event]) {
- console.debug("remove mode handler", event, handler, "from", element)
- element.removeEventListener(event, mode_handlers[event][handler])
- }
- }
-}
-
-function add_mode_handlers(element, mode_handlers) {
- for (let event in mode_handlers) {
- for (let handler in mode_handlers[event]) {
- console.debug("add mode handler", event, handler, "from", element)
- element.addEventListener(event, mode_handlers[event][handler], false)
- }
- }
-}
-
-function update_transforms(state) {
- let view = viewbox(state)
- let elements = Array.from(document.querySelectorAll(".scales, .moves"))
-
- for (let i in elements) {
- elements[i].transform.baseVal.clear()
- if (elements[i].classList.contains("scales")) {
- graphics.svg.transform(state.svg, elements[i], "setScale", state["scale"], state["scale"])
- }
- if (elements[i].classList.contains("moves")) {
- graphics.svg.transform(state.svg, elements[i], "setTranslate", view.x, view.y)
- }
- }
-}
-
-/*
- * In the future I may make seperate implementations for these, not sure yet.
- * Keeping possibilities open
- */
-function update_movable(state) {
- update_transforms(state)
-}
-
-function update_scalable(state) {
- update_transforms(state)
-}
-
-
-function update_points_display(state) {
- let s = ""
- for (let i in state.points) {
- if (i > 0) {
- s += " "
- }
- s += state.points[i].x + ',' + state.points[i].y
- }
- state.walls.setAttribute("points", s)
-}
-
-function add_points(state, ...points) {
- for (let i in points) {
- if (typeof points[i].x !== "number" || typeof points[i].y !== "number") {
- throw new Error("Invalid point")
- }
- }
- state["points"].push(...points)
- update_points_display(state)
-}
-
-function last_point(state) {
- return state["points"][state["points"].length - 1]
-}
-
-function axis_snap(point, on) {
- let axis = axis_snap_which(on, point)
- point[axis] = on[axis]
- return point
-}
-
-function axis_snap_which(a, b) {
- if (Math.abs(a.x - b.x) > Math.abs(a.y - b.y)) {
- return "y"
- } else {
- return "x"
- }
-}
-
-function viewbox(state) {
- let a = state.svg.getAttribute("viewBox").split(' ')
- for (let i in a) {
- a[i] = Number(a[i])
- }
- return graphics.rect(a[0], a[1], a[2], a[3])
-}
-
-// Newview is graphics.rect
-function update_viewbox(state, newview) {
- limit_viewbox_position(state, newview)
- console.log("Viewbox", newview)
- state.svg.setAttribute("viewBox", [newview.x, newview.y, newview.width, newview.height].join(' '))
-}
-
-function limit_viewbox_position(state, view) {
- let maxsize = scale(state, canvas_size(state))
- let maxp = graphics.point(maxsize.width - view.width, maxsize.height - view.height)
- if (view.x < 0) {
- view.x = 0
- } else if (view.x > maxp.x) {
- console.log(`viewbox x restricted to max of ${maxp.x}`)
- view.x = maxp.x
- }
- if (view.y < 0) {
- view.y = 0
- } else if (view.y > maxp.y) {
- console.log(`viewbox y restricted to max of ${maxp.y}`)
- view.y = maxp.y
- }
-
- return view;
-}
-
-
-function canvas_size(state) {
- return { width: Number(state.svg.getAttribute("width")), height: Number(state.svg.getAttribute("height")) }
-}
-
-// from is a viewbox coordinate, obviously I guess
-function set_scale(state, scale, from) {
- if (typeof scale !== "number") {
- throw new Error(scale + ": Invalid scale")
- }
-
- let size = canvas_size(state)
- let view = viewbox(state)
- let furthest = Math.min(view.width / size.width, view.height / size.height)
- if (scale < furthest) {
- console.log(`Unable to zoom out any further than ${furthest} (no more content)`)
- scale = furthest
- }
- if (state["scale"] === scale) {
- return
- }
-
- state["scale"] = scale
-
- if (!from) {
- from = graphics.point(
- view.width / 2,
- view.height / 2
- )
- }
- update_viewbox(state, graphics.rect(
- from.x / view.width,
- from.y / view.height,
- view.width,
- view.height
- )
- )
- update_transforms(state)
- console.log("Scale", scale)
-}
-
-function view_to_real(state, p) {
- let view = viewbox(state)
- let size = canvas_size(state)
- let scale = state["scale"] * 1
-
- let r = graphics.point(
- ((p.x / (size.width / view.width))) / scale,
- ((p.y / (size.height / view.height))) / scale
- )
- //console.debug(`Viewbox to real coord: ${p.x}x${p.y} -> ${r.x}x${r.y} [scale ${state.scale}] [view ${viewbox(state).x}, ${viewbox(state).width}] [canvas ${canvas_size(state).width}]`)
- return r
-}
-
-function view_to_real_scaled(state, p) {
- return scale(state, view_to_real(state, p))
-}
-
-function real_to_absolute(state, p) {
- let view = viewbox(state)
-
- return graphics.point(
- p.x + (view.x / state["scale"]),
- p.y+ (view.y / state["scale"])
- )
-}
-
-function scale(state, obj) {
- for (let i in obj) {
- if (typeof obj[i] !== "number") {
- throw new Error("expected number")
- }
- obj[i] *= state["scale"]
- }
- return obj
-}
-
-function unscale(state, obj) {
- for (let i in obj) {
- if (typeof obj[i] !== "number") {
- throw new Error("expected number")
- }
- obj[i] /= state["scale"]
- }
- return obj
-}
-
-init()
diff --git a/getlib b/getlib
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+set -e
+
+base="${1:%@*}"
+tag="${1#*@}"
+url=https://"$base"
+dir=lib/"$base"/src
+
+if ! test -d "$dir"
+then
+ git clone -q "$url" "$dir"
+ fetch=false
+fi
+
+cd "$dir"
+
+${fetch:-true} &&
+ git fetch -q
+git checkout -q "$tag"
+
+# Reset would work, but this seems safer
+# (In case the git directory is somehow deleted, etc.)
+git restore *
+
+if test -d ../patches; then
+ for p in ../patches/*
+ do
+ patch -suNp1 < "$p"
+ done
+fi
+
+echo "$dir"
diff --git a/lib/github.com/svgdotjs/svg.panzoom.js/patches/include.diff b/lib/github.com/svgdotjs/svg.panzoom.js/patches/include.diff
@@ -0,0 +1,10 @@
+diff --git a/src/svg.panzoom.js b/src/svg.panzoom.js
+index cb4d5dc..70989c3 100644
+--- a/src/svg.panzoom.js
++++ b/src/svg.panzoom.js
+@@ -1,4 +1,4 @@
+-import { Svg, on, off, extend, Matrix, Box } from '@svgdotjs/svg.js'
++import { Svg, on, off, extend, Matrix, Box } from '/lib/github.com/svgdotjs/svg.js/main.js'
+
+ const normalizeEvent = ev =>
+ ev.touches || [{ clientX: ev.clientX, clientY: ev.clientY }]
diff --git a/libnames b/libnames
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+set -e
+
+printf '%s\n' "$@" | awk -F '/' -vOFS=/ '{
+ sub("@[^/]+$", "", $0)
+ print
+}'