www.spaceplanner.app

Web client to the spaceplanner API
git clone git://jacobedwards.org/www.spaceplanner.app
Log | Files | Refs

commit 6e19218dfcff0330348a22e5182e951ab74266a8
parent 7c594a6798cf848220a830268a21d7d345a7fd43
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon,  9 Sep 2024 14:10:10 -0700

Add some more furniture manipulation functions

You can now move furniture and easily edit the parameters of existing
furniture.

Diffstat:
Mfiles/floorplans/floorplan/backend.js | 90+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mfiles/floorplans/floorplan/editor.js | 29++++++++++++++++++-----------
Mfiles/floorplans/floorplan/main.js | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mfiles/lib/etc.js | 2+-
4 files changed, 206 insertions(+), 99 deletions(-)

diff --git a/files/floorplans/floorplan/backend.js b/files/floorplans/floorplan/backend.js @@ -86,6 +86,8 @@ class BackendHistory { return group } + this.truncate() + if (this.groups.length === 0) { return pushGroup(this) } @@ -559,45 +561,65 @@ export class FloorplanBackend { } mapFurniture(params, id) { - let fm = id ? this.reqObj(id) : {} - - if (params.furniture_id != undefined) { - if (this.obj(params.furniture_id) == undefined) { - throw new Error("invalid furniture id for furniture map") - } - fm.furniture_id = params.furniture_id - } else if (fm.furniture_id == undefined) { - throw new Error("Missing furniture id") - } - if (params.x != undefined) { - if (typeof params.x !== "number") { - throw new Error("Invalid x coordinate") + let backend = this + const validInt = function(input, cur) { + let x = Math.round(input ?? cur) + if (isNaN(x)) { + throw new Error(input + " is NaN") } - fm.x = params.x + return x } - if (params.y != undefined) { - if (typeof params.x !== "number") { - throw new Error("Invalid y coordinate") + const validSize = function(input, cur) { + let x = validInt(input, cur) + if (x <= 0) { + throw new Error(input + " must be a positive number") } - fm.y = params.y - } - if (params.angle != undefined) { - if (typeof params.angle !== "number" || params.angle < 0 || - params.angle >= 360) { - throw new Error(params.angle + ": Invalid angle") + return x + } + let parsers = { + x: validInt, + y: validInt, + width: validSize, + depth: validSize, + angle: function(input, cur) { + if (input == undefined) { + return cur ?? 0 + } + let x = validInt(input) + if (x < 0 || x >= 360) { + throw new Error(input + ": Angle must be between 0 and 359 degrees") + } + return x + }, + layout: function(input, cur) { + if (input == undefined) { + return cur ?? "1" + } + if (typeof input !== "string") { + throw new Error(input + ": Layout should be a string") + } + return input + }, + furniture_id: function(id, cur) { + if (id == undefined) { + if (cur == null) { + throw new Error("Missing furniture id") + } + return cur + } + if (backend.obj(id) == undefined) { + throw new Error("invalid furniture id for furniture map") + } + return id } - fm.angle = params.angle } - if (params.layout != undefined) { - if (typeof params.layout !== "string") { - throw new Error(params.layout + ": Invalid layout") - } - fm.layout = params.layout - } else { - // for now, this should be handled by the server later - fm.layout = "1" + + let fm = id ? this.reqObj(id) : {} + for (let param in parsers) { + fm[param] = parsers[param](params[param], fm ? fm[param] : undefined) } - return this.addData("furniture_maps", fm) + + return this.addData(id ?? "furniture_maps", fm) } unmapFurniture(id, options) { @@ -606,7 +628,7 @@ export class FloorplanBackend { addMappedFurniture(params, id) { params.furniture_id = this.addFurniture(params, id ? this.reqObj(id).furniture_id : null) - return this.mapFurniture(params) + return this.mapFurniture(params, id) } reqObj(id) { diff --git a/files/floorplans/floorplan/editor.js b/files/floorplans/floorplan/editor.js @@ -539,17 +539,19 @@ export class FloorplanEditor { } addFurniture(params, id) { - this.backend.addFurniture(params, id) + return this.backend.addFurniture(params, id) } mapFurniture(params, id) { - this.backend.mapFurniture(params, id) + id = this.backend.mapFurniture(params, id) this.updateDisplay() + return id } addMappedFurniture(params, id) { - this.backend.addMappedFurniture(params, id) + id = this.backend.addMappedFurniture(params, id) this.updateDisplay() + return id } selectedPoints() { @@ -640,27 +642,28 @@ export class FloorplanEditor { } }, furniture: function(id, value) { - let maps = editor.backend.cache - for (let id in maps) { - if (maps[id].furniture_id == id) { - let m = editor.draw.findOneMax(byId(id)) + let maps = editor.backend.cache.furniture_maps + for (let mid in maps) { + if (maps[mid].furniture_id == id) { + let m = editor.draw.findOneMax(byId(mid)) if (m != null) { m.size(value.width, value.depth) + console.log("New name", furniture_name(value)) + m.findOne("title").words(furniture_name(value)) } } } }, furniture_maps: function(id, value) { let fm = editor.draw.findOneMax(byId(id)) - let f = editor.backend.cache.furniture[value.furniture_id] if (!fm) { - console.log(f, editor.layoutG()) + let f = editor.backend.reqObj(value.furniture_id) fm = editor.layoutG().rect(f.width, f.depth) - .cx(value.x).cy(value.y) .fill("black") .attr({ id }) + fm.element("title").words(furniture_name(f)) } - fm.element("title").words(f.name ?? f.type) + fm.cx(value.x).cy(value.y) fm.transform({ rotate: value.angle }) @@ -803,3 +806,7 @@ export function getID(thing, type) { function byId(id) { return "#" + id } + +function furniture_name(f) { + return f.name ? `${f.name} (${f.type})` : f.type +} diff --git a/files/floorplans/floorplan/main.js b/files/floorplans/floorplan/main.js @@ -117,8 +117,6 @@ function init() { ) )) - editor.draw.on("select", function(event) { selectHandler(event, editor) }) - editor.backend.pull() .then(function() { if (editor.draw.findExactlyOne("#points").children().length === 0) { @@ -127,7 +125,7 @@ function init() { }) } -function selectHandler(event, editor) { +function selectHandler(event, editor, state) { let old = document.getElementById("selOps") if (!event.detail.selected) { if (old) { @@ -172,6 +170,21 @@ function selectHandler(event, editor) { ) } + let fmaps = ids.filter(function(item) { return backend.idType(item) === "furmap" }) + if (fmaps.length === 1) { + c.appendChild( + ui.input("edit", "Edit selected furniture", { + attributes: { + type: "button", + value: "Edit furniture" + }, + handlers: { click: function() { + document.body.appendChild(furnitureMenu(editor, fmaps[0])) + }} + }) + ) + } + if (old) { old.replaceWith(c) } else { @@ -239,10 +252,11 @@ let modes = { handlers: { contextmenu: preventDefaultHandler, mousedown: [selectionHandler, precisePointHandler, furnitureHandler], - mousemove: [precisePointHandler, precisePointMapHandler], - mouseup: precisePointHandler, + mousemove: [precisePointHandler, precisePointMapHandler, furnitureHandler], + mouseup: [precisePointHandler, furnitureHandler], keydown: [zoomKeysHandler, undoRedoHandler], - dblclick: precisePointMapHandler + dblclick: [precisePointMapHandler, furnitureHandler], + select: selectHandler } } } @@ -606,51 +620,116 @@ function precisePointMapHandler(event, editor) { } } -function furnitureHandler(ev, editor) { - if (ev.type != "mousedown" || ev.button !== buttons.left) { - return - } - if (editor.draw.findOne(".selected") != null) { - return +// mousedown, mouseup, mousemove, dblclick +function furnitureHandler(ev, editor, state) { + const doMove = function() { + // racy + if (state.move) { + let id = state.moving.attr("id") + let p = editor.draw.point(state.move.x, state.move.y) + editor.mapFurniture({ x: p.x, y: p.y }, id) + delete state.move + } } - ev.preventDefault() - let menu = document.getElementById("furniture_menu") - if (menu != null) { - menu.remove() + + let sel = editor.draw.find("#furniture_layouts > * > .selected").array() + if (sel.length === 0 && ev.type === "dblclick" && ev.button === buttons.left) { + ev.preventDefault() + if (state.menu) { + state.menu.remove() + delete state.menu + } + state.menu = document.body.appendChild( + furnitureMenu(editor, editor.draw.point(ev.clientX, ev.clientY))) + } else if (sel.length === 1) { + if (ev.type === "dblclick") { + ev.preventDefault() + document.body.appendChild(furnitureMenu(editor, lib.getID(sel[0]))) + } else if (ev.type === "mousedown") { + ev.preventDefault() + state.moving = sel[0] + return + } else { + if (!state.moving) { + return + } + if (ev.type === "mouseup") { + ev.preventDefault() + doMove() + delete state.moving + return + } else if (ev.type === "mousemove") { + ev.preventDefault() + if (state.move) { + state.move = { x: ev.clientX, y: ev.clientY } + } else { + state.move = { x: ev.clientX, y: ev.clientY } + setTimeout(doMove, 60) + } + } + } } - menu = furnitureMenu(editor, editor.draw.point(ev.clientX, ev.clientY)) - menu.id = "furniture_menu" - document.body.append(menu) } function enumSelection(input, values) { let a = typeof(values.keys) === "function" for (let i in values) { - console.log("EnumSel", i, values[i]) let opt = input.appendChild(document.createElement("option")) opt.appendChild(document.createTextNode(a ? values[i] : i)) } } -function furnitureMenu(editor, p) { - if (p == null) { - p = { x: 0, y: 0 } +function furnitureMenu(editor, pointOrID) { + console.warn("Furniture menu") + const def = function(obj) { + return obj[defKey(obj)] } - - let defaultType - for (let t in editor.furniture_types) { - defaultType = t - break + const defKey = function(obj) { + for (let i in obj) { + return i + } } + editor.finishAction() + let p + let id + let params + if (typeof pointOrID === "string") { + id = pointOrID + params = editor.backend.reqObj(id) + let fp = editor.backend.reqObj(params.furniture_id) + for (let k in fp) { + params[k] = fp[k] + } + } else { + if (pointOrID == null) { + p = { x: 0, y: 0 } + } else if (typeof pointOrID === "object") { + p = pointOrID + } + let type = defKey(editor.furniture_types) + console.log(type, editor.furniture_types) + let v = def(editor.furniture_types[type].varieties) + params = { + x: p.x, + y: p.y, + type, + width: v.width ?? 9600, + depth: v.height ?? 9600, + name: null + + } + id = editor.addMappedFurniture(params) + } let items = [ menuItem("name", "Name"), - menuItem("type", "Type", { break: false, enum: editor.furniture_types, attributes: { required: true } }), - menuItem("variety", "Variety", { enum: editor.furniture_types[defaultType].varieties }), - menuItem("width", "Width", { attributes: { required: true } }), - menuItem("depth", "Depth", { attributes: { required: true } }), - menuItem("angle", "Angle", { attributes: { min: 0, max: 359, type: "number", value: 0, required: true } }), - menuItem("add", null, { attributes: { value: "Add", type: "Submit" } }) + menuItem("type", "Type", { break: false, enum: editor.furniture_types, attributes: { value: params.type, required: true } }), + menuItem("variety", "Variety", { enum: editor.furniture_types[params.type].varieties }), + menuItem("width", "Width", { attributes: { value: params.width, required: true } }), + menuItem("depth", "Depth", { attributes: { value: params.height, required: true } }), + menuItem("angle", "Angle", { attributes: { value: params.angle, min: 0, max: 359, type: "number", value: 0, required: true } }), + menuItem("cancel", null, { attributes: { value: "Cancel", type: "button" }}), + menuItem("done", null, { attributes: { value: "Done", type: "submit" }}) ] let keys = {} for (let i in items) { @@ -658,14 +737,16 @@ function furnitureMenu(editor, p) { } const fromVariety = function(type, variety) { + console.log("Loading width & depth from variety") let v if (variety == null) { v = { width: null, depth: null } } else { v = editor.furniture_types[type].varieties[variety] } - items[keys.width].input.value = v.width - items[keys.depth].input.value = v.depth + params.width = items[keys.width].input.value = v.width + params.depth = items[keys.depth].input.value = v.depth + editor.addMappedFurniture(params, id) } const newVariety = function() { let vars = editor.furniture_types[items[keys.type].input.value].varieties @@ -689,29 +770,26 @@ function furnitureMenu(editor, p) { items[keys.type].input.addEventListener("change", function(ev) { newVariety() }) - menu.addEventListener("submit", function(ev) { + items[keys.cancel].input.addEventListener("click", function(ev) { + ev.preventDefault() + editor.undo() + menu.remove() + }) + menu.addEventListener("change", function(ev) { ev.preventDefault() try { - let name = items[keys.name].input.value - if (name.length === 0) { - name = null - } - let params = { - type: items[keys.type].input.value, - x: p.x, - y: p.y, - width: Number(items[keys.width].input.value), - depth: Number(items[keys.depth].input.value), - angle: Number(items[keys.angle].input.value), - name - } - editor.addMappedFurniture(params) - editor.finishAction() + params[event.target.name] = event.target.value + console.log("Change", event.target.name, event.target.value) + editor.addMappedFurniture(params, id) } catch(err) { etc.error(err, menu) throw err } + }) + menu.addEventListener("submit", function(ev) { + ev.preventDefault() + editor.finishAction() menu.remove() }) @@ -782,7 +860,7 @@ function menuItem(name, label, options) { if (options.value != undefined) { attributes.value = options.value } - if (attributes.title == undefined) { + if (attributes.title == undefined && label != undefined) { attributes.title = label } diff --git a/files/lib/etc.js b/files/lib/etc.js @@ -84,7 +84,7 @@ export function error(message, on) { err_elem.append(msg) err_elem.append(ui.button("Dismiss", "Dismiss error", "close", { handlers: { click: { function() { err_elem.remove() } } } })) - on.before(err_elem) + on.prepend(err_elem) } }