editor.js (23564B)
1 import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js" 2 import * as backend from "./backend.js" 3 import { Vector2 } from "/lib/github.com/mrdoob/three.js/math/Vector2.js" 4 import * as geometry from "./geometry.js" 5 6 const selectEvent = new Event("select") 7 const unselectEvent = new Event("unselect") 8 9 SVG.extend(SVG.Svg, { 10 unselect: function(list) { 11 console.debug("Svg.unselect", list) 12 13 let selected 14 if (list) { 15 selected = list 16 } else { 17 selected = this.find(".selected") 18 } 19 20 // Should also test for selected class 21 if (!selected) { 22 console.debug("Nothing to unselect") 23 return 24 } 25 26 this.find(".last_selected") 27 .removeClass("last_selected") 28 29 let unselected = selected 30 .removeClass("selected") 31 .addClass("last_selected") 32 33 // NOTE: Could fire an event, but then I'd have to handle 34 // deletions, so I'll leave it until it's needed. 35 return unselected 36 }, 37 38 select: function(list) { 39 console.debug("Svg.select", list) 40 41 if (list && list.sameElements(this.find(".selected"))) { 42 this.fire("reselect", { selected: this.find(".selected") }) 43 return list 44 } 45 46 this.unselect() 47 48 if (list) { 49 list.addClass("selected") 50 } 51 this.fire("select", { selected: list }) 52 return list 53 }, 54 55 reselect: function() { 56 this.fire("select", { selected: this.find(".selected") }) 57 } 58 }) 59 60 SVG.extend(SVG.List, { 61 selectList: function() { 62 let root 63 this.each(function(item) { 64 if (!root) { 65 root = item.root() 66 } else if (root != item.root()) 67 throw new Error("Cannot select from different documents") 68 }) 69 70 return root.select(this) 71 }, 72 73 array: function() { 74 let a = [] 75 this.each(function(item) { 76 a.push(item) 77 }) 78 return a 79 }, 80 81 sameElements: function(list2) { 82 const cmp = function(a, b) { 83 return a.attr("id") < b.attr("id") 84 } 85 86 let a = this.array() 87 let b = list2.array() 88 if (a.length != b.length) { 89 return false 90 } 91 92 a = a.sort(cmp) 93 b = b.sort(cmp) 94 for (let i = 0; i < a.length; ++i) { 95 if (a[i].attr("id") !== b[i].attr("id")) { 96 return false 97 } 98 } 99 return true 100 } 101 102 }) 103 104 SVG.extend(SVG.Element, { 105 select: function() { 106 return new SVG.List([this]).selectList()[0] 107 }, 108 109 findOneMax: function(selector) { 110 let results = this.find(selector) 111 if (results.length > 1) { 112 throw new Error("Found more than one element") 113 } 114 if (results.length == 1) 115 return results[0] 116 return undefined 117 }, 118 119 findExactlyOne: function(selector) { 120 let r = this.findOneMax(selector) 121 if (!r) { 122 throw new Error("Didn't find " + selector) 123 } 124 return r 125 }, 126 127 touching: function(x, y, minsize) { 128 let b = this.bbox() 129 let d = minsize - b.width 130 if (d > 0) { 131 b.x -= d / 2 132 b.width = minsize 133 } 134 d = minsize - b.height 135 if (d > 0) { 136 b.y -= d / 2 137 b.height = minsize 138 } 139 return (x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height) 140 } 141 }) 142 143 // May not be needed anymore 144 SVG.extend(SVG.Circle, { 145 // Maybe this already exists? 146 pos: function() { 147 let attrs = this.attr(["cx", "cy"]) 148 return { x: attrs.cx, y: attrs.cy } 149 } 150 }) 151 152 class Units { 153 constructor() { 154 this.data = {} 155 this.systems = {} 156 this.symbols = {} 157 } 158 159 add(name, factor, options) { 160 options = options ?? {} 161 162 if (!name || !factor) { 163 throw new Error("Requires name and factor") 164 } 165 if (this.data[name]) { 166 throw new Error("Already exists") 167 } 168 if (options.base && (!this.data[options.base] || this.data[options.base].next)) { 169 throw new Error("Invalid base (already used or does not exist)") 170 } 171 if (options.base && options.system) { 172 throw new Error("Class may only be set on base units") 173 } 174 if (options.system && this.systems[options.system]) { 175 throw new Error("Unit system already exists") 176 } 177 if (options.symbol && this.symbols[options.symbol]) { 178 throw new Error("Symbol already exists") 179 } 180 181 this.data[name] = { 182 name: name, 183 factor: factor, 184 } 185 if (options.system) { 186 this.data[name].system = options.system 187 this.systems[options.system] = name 188 } 189 if (options.symbol) { 190 this.data[name].symbol = options.symbol 191 this.symbols[options.symbol] = name 192 } 193 if (options.base) { 194 this.data[options.base].next = name 195 this.data[name].base = options.base 196 } 197 } 198 199 get(name, num) { 200 if (!name || !this.data[name]) { 201 throw new Error("Invalid unit") 202 } 203 let n = this.data[name].factor 204 if (this.data[name].base) { 205 n *= this.get(this.data[name].base) 206 } 207 return n * (num ?? 1) 208 } 209 210 system(name) { 211 return this.data[this.smallest(name)].system 212 } 213 214 smallest(name) { 215 return this.walk(name, "base") 216 } 217 218 biggest(name) { 219 return this.walk(name, "next") 220 } 221 222 walk(name, key) { 223 while (this.data[name][key]) { 224 name = this.data[name][key] 225 } 226 return name 227 } 228 229 separate(units, system, options) { 230 options = options ?? {} 231 let parts = [] 232 let unit = this.biggest(this.systems[system]) 233 234 do { 235 let n = this.get(unit) 236 if (units >= n) { 237 let amount = units / n 238 if (this.data[unit].base || options.whole) { 239 amount = Math.floor(amount) 240 } 241 units -= amount * n // not sure about floating mod in js 242 parts.push({ unit: unit, symbol: this.data[unit].symbol, amount: amount }) 243 } 244 } while (units > 0 && (unit = this.data[unit].base)) 245 if (units > 0) { 246 parts.push({ "amount": units }) 247 } 248 return parts 249 } 250 251 combine(parts) { 252 let t = 0 253 for (let i in parts) { 254 if (!parts[i].unit) { 255 if (!parts[i].symbol) { 256 throw new Error("Requires unit or symbol") 257 } 258 parts[i].unit = this.symbols[parts[i].symbol] 259 } 260 t += this.get(parts[i].unit, parts[i].amount) 261 } 262 return t 263 } 264 265 snapTo(x, unit) { 266 let n = this.get(unit) 267 let f = function(x) { 268 x = Math.round(x) 269 return x - (x % n) 270 } 271 272 if (typeof x === "number") { 273 return f(x) 274 } else if (Array.isArray(x)) { 275 for (let i in x) { 276 x[i] = f(x[i]) 277 } 278 } else if (typeof x === "object") { 279 for (let i in x) { 280 if (typeof x[i] === "number") { 281 x[i] = f(x[i]) 282 } 283 } 284 } else { 285 throw new Error("Unable to snap that") 286 } 287 288 return x 289 } 290 } 291 292 export class FloorplanEditor { 293 constructor(svg, floorplan, options) { 294 if (!options) { 295 options = {} 296 } 297 298 let editor = this 299 300 this.draw = svg 301 this.mode 302 this.modes = {} 303 this.mode_states = {} 304 305 // Setup units 306 this.units = new Units() 307 this.units.add("inch", 96, { symbol: '"', system: "imperial" }) 308 this.units.add("foot", 12, { base: "inch", symbol: "'" }) 309 this.units.add("centimeter", this.units.get("inch") / 2.54, { symbol: "cm", system: "metric" }) 310 this.units.add("meter", 100, { symbol: "m", base: "centimeter" }) 311 312 if (!options.backend) { 313 options.backend = {} 314 } 315 if (!options.backend.callbacks) { 316 options.backend.callbacks = {} 317 } 318 options.backend.callbacks["patch"] = function(diff) { editor.applyOp(diff) } 319 320 this.backend = new backend.FloorplanBackend(floorplan, options.backend) 321 322 this.grids = {} 323 for (let system in this.units.systems) { 324 this.grids[system] = gridSystem(this, system) 325 } 326 327 this.draw.rect().attr({ id: "grid" }) 328 this.useGrid() 329 330 this.ui = {} 331 this.ui.bottom = this.draw.group().attr({ id: "bottom" }) 332 333 let data = this.draw.group().attr({ id: "floorplan" }) 334 this.doorSwings = data.group().attr({ id: "door_swings" }) 335 data.group().attr({ id: "pointmaps" }) // lines 336 data.group().attr({ id: "points" }) // circles 337 this.layouts = data.group().attr({ id: "furniture_layouts" }) // g of furniture 338 this.layout = "1" 339 340 this.ui.top = this.draw.group().attr({ id: "top" }) 341 this.ui.labels = this.draw.group().attr({ id: "labels" }) 342 343 // Resize grid when appropriate 344 this.draw.on("zoom", function(event) { 345 editor.updateGrid(event.detail.box) 346 }) 347 this.draw.on("panning", function(event) { 348 editor.updateGrid(event.detail.box) 349 }) 350 let resize = new ResizeObserver(function(entries) { 351 if (entries[0].target != editor.draw.node) { 352 throw new Error("Expected draw node") 353 } 354 console.debug("Editor resized") 355 editor.updateGrid() 356 }) 357 resize.observe(editor.draw.node) 358 359 let selectionRemoval = new MutationObserver(function(mutations) { 360 for (const m of mutations) { 361 if (m.type === "childList" && m.removedNodes) { 362 m.removedNodes.forEach(function(node) { 363 if (node.classList && node.classList.contains("selected")) { 364 console.debug("selectionRemoval", 365 "Detected selected node being removed") 366 editor.draw.reselect() 367 return 368 } 369 }) 370 } 371 } 372 }) 373 selectionRemoval.observe(this.draw.node, { childList: true, subtree: true }) 374 375 this.draw.on("select", function(event) { 376 editor.selection = event.detail.selection 377 }) 378 379 this.initialized = this.backend.initialized 380 } 381 382 useUnits(system) { 383 if (!this.units.systems[system]) { 384 throw new Error("No such system") 385 } 386 this.unitSystem = system 387 this.useGrid(system) 388 } 389 390 get unit() { 391 return this.units.systems[this.unitSystem] 392 } 393 394 addMode(name, mode) { 395 if (this.modes[name]) { 396 throw new Error("Mode already exists") 397 } 398 if (!mode) { 399 throw new Error("No mode") 400 } 401 402 this.modes[name] = {} 403 for (let key in mode) { 404 if (key !== "handlers") { 405 this.modes[name][key] = mode[key] 406 } 407 } 408 409 const getHandler = function(editor, handler) { 410 editor.mode_states[handler] = {} 411 return function(event) { 412 return handler(event, state, editor.mode_states[handler]) 413 } 414 } 415 416 // to pass use in another function 417 let state = this 418 this.modes[name].handlers = {} 419 this.mode_states[name] = {} 420 for (let type in mode.handlers) { 421 this.modes[name]["handlers"][type] = [] 422 423 let a = mode.handlers[type] 424 if (typeof a === "function") { 425 a = [ a ] 426 } else if (typeof a !== "object") { 427 delete this.modes[name] 428 throw new Error("Expected function or object") 429 } 430 431 for (let i in a) { 432 console.debug("Create mode handler", name, type, a[i]) 433 this["modes"][name]["handlers"][type] 434 .push(getHandler(this, a[i])) 435 } 436 } 437 438 console.log("Add mode", mode) 439 return this 440 } 441 442 useMode(newmode) { 443 if (newmode && !this.modes[newmode]) { 444 throw new Error("'" + newmode + "': Invalid mode") 445 } 446 447 if (newmode === this.mode) { 448 return this 449 } 450 451 if (this.mode) { 452 remove_mode_handlers(this.draw, this.modes[this.mode].handlers) 453 } 454 455 if (newmode) { 456 let points = this.draw.findOne("#points") 457 if (this.modes[newmode].points) { 458 points.attr("visibility", null) 459 } else { 460 points.attr("visibility", "hidden") 461 } 462 add_mode_handlers(this.draw, this.modes[newmode].handlers) 463 } 464 465 this.mode = newmode 466 console.log("Mode", this.mode) 467 return this 468 } 469 470 useGrid(system) { 471 let grid = this.draw.findExactlyOne("#grid") 472 if (!system) { 473 grid.attr("visibility", "hidden") 474 } else { 475 grid.fill(this.grids[system].url()).attr("visibility", null) 476 } 477 } 478 479 furnitureLabels(show) { 480 console.debug("Editor.furnitureLabels", show) 481 const l = this.ui.labels 482 if (show === undefined) { 483 return !l.hasClass("hidden") 484 } 485 if (show) { 486 l.removeClass("hidden") 487 } else { 488 l.addClass("hidden") 489 } 490 } 491 492 updateGrid(box) { 493 let grid = this.draw.findExactlyOne("#grid") 494 if (!box) { 495 box = this.draw.viewbox() 496 } 497 498 const swap = { x: "y", y: "x", width: "height", height: "width" } 499 const map = { width: "x", height: "y" } 500 501 // Reads easy, right? 502 let real = this.draw.node.getBoundingClientRect() 503 let base = (real.width > real.height) ? "height" : "width" 504 let val = real[swap[base]] * (box[base] / real[base]) 505 let diff = val - box[swap[base]] 506 box[map[swap[base]]] -= diff / 2 507 box[swap[base]] = val 508 const margin = 10000 509 grid.size(box.width + margin, box.height + margin).move(box.x - margin / 2, box.y - margin / 2) 510 } 511 512 fitToView() { 513 const adj = function(ns, t, pos, siz) { 514 t[pos] += (t[siz] - ns) / 2 515 t[siz] = ns 516 } 517 const add = function(d, t, pos, siz) { 518 return adj(t[siz] + d, t, pos, siz) 519 } 520 521 let bbox = this.draw.findOne("#floorplan").bbox() 522 let ft = this.units.get("foot") 523 let min = ft * 20 524 add(ft * 2, bbox, "x", "width") 525 if (bbox.width < min) { 526 adj(min, bbox, "x", "width") 527 } 528 add(ft * 2, bbox, "y", "height") 529 if (bbox.height < min) { 530 adj(min, bbox, "y", "height") 531 } 532 this.draw.viewbox(bbox) 533 this.updateGrid() 534 } 535 536 // Should be called after each user "action" 537 finishAction() { 538 this.backend.history.mark() 539 return this.backend.push() 540 } 541 542 undo() { 543 this.backend.undo() 544 return this.backend.push() 545 } 546 547 redo() { 548 this.backend.redo() 549 return this.backend.push() 550 } 551 552 addPoint(point, force) { 553 if (!force) { 554 let already = this.pointAt(point) 555 if (already != null) { 556 return already 557 } 558 } 559 return this.backend.addPoint(point) 560 } 561 562 movePoint(point, coordinate) { 563 return this.backend.addPoint(coordinate, getID(point, "points")) 564 } 565 566 remove(...elements) { 567 let later = [] 568 569 for (let i in elements) { 570 let id = getID(elements[i]) 571 if (backend.idType(id) === "pntmap") { 572 this.backend.unmapPoints(id) 573 } else if (backend.idType(id) === "furmap") { 574 // For now, just remove the furniture too 575 later.push(this.backend.reqObj(id).furniture_id) 576 this.backend.unmapFurniture(id) 577 } else { 578 later.push(id) 579 } 580 } 581 582 let editor = this 583 const pointsAngle = function(a, b) { 584 a = editor.backend.reqObj(a) 585 b = editor.backend.reqObj(b) 586 return geometry.lineAngle(new Vector2(a.x, a.y), new Vector2(b.x, b.y)) 587 } 588 589 for (let i in later) { 590 let t = backend.idType(later[i]) 591 if (t === "pnt") { 592 let p = later[i] 593 for (let o in this.backend.mappedPoints[p]) { 594 let a1 = pointsAngle(p, o) 595 for (let o2 in this.backend.mappedPoints[p]) { 596 if (o2 === o) { 597 continue 598 } 599 600 let a2 = pointsAngle(p, o2) 601 if ((geometry.deg(Math.abs(a1 - a2)) % 180) < 1) { 602 editor.backend.mapPoints({ a: o, b: o2 }) 603 } 604 } 605 } 606 this.backend.removePoint(later[i], { unmap: true }) 607 } else if (t === "fur") { 608 this.backend.removeFurniture(later[i]) 609 } else { 610 throw new Error(backend.idType(later[i]) + ": Unsupported type") 611 } 612 } 613 614 this.backend.removeOrphans() 615 } 616 617 pointAt(point) { 618 return this.thingAt(point, "#points") 619 } 620 621 thingAt(point, selector, options) { 622 options = options ?? {} 623 options.max = 1 624 return this.thingsAt(point, selector, options)[0] 625 } 626 627 thingsAt(point, selector, options) { 628 options = options ?? {} 629 630 let children = this.draw.find(selector ?? "*") 631 .children() 632 .toArray() 633 634 let done = {} 635 let inside = [] 636 for (let i = 0; i < children.length; ++i) { 637 if (children[i][options.method ?? "insideT"](point.x, point.y, options.minsize)) { 638 if (inside.push(children[i]) >= options.max) { 639 return inside 640 } 641 children[i] = null 642 } 643 } 644 645 return inside 646 } 647 648 mapPoints(params, id) { 649 if (params.a) { 650 params.a = getID(params.a, "points") 651 } 652 if (params.b) { 653 params.b = getID(params.b, "points") 654 } 655 return this.backend.mapPoints(params, id) 656 } 657 658 addFurniture(params, id) { 659 return this.backend.addFurniture(params, id) 660 } 661 662 mapFurniture(params, id) { 663 return this.backend.mapFurniture(params, id) 664 } 665 666 addMappedFurniture(params, id) { 667 return this.backend.addMappedFurniture(params, id) 668 } 669 670 selected(type) { 671 if (type === "furniture_maps") { 672 type = "furniture_layouts > * >" 673 } 674 return this.draw.find(`#${type} > .selected`) 675 } 676 677 selectedOne(type) { 678 let sel = this.selected(type) 679 if (sel.length != 1) { 680 return null 681 } 682 return sel[0] 683 } 684 685 applyDiffs(diffs) { 686 for (let op in diffs) { 687 this.applyOp(diffs[op]) 688 } 689 } 690 691 applyOp(diff) { 692 console.debug("Editor.applyOp", diff) 693 let editor = this 694 695 let ops = { 696 add: { 697 points: function(id, value) { 698 let cur = editor.draw.findOneMax(byId(id)) 699 // Update pointmaps 700 if (cur) { 701 cur.cx(value.x).cy(value.y) 702 .select() 703 } else { 704 editor.draw.findOne("#points") 705 .circle(400) 706 .cx(value.x).cy(value.y) 707 .attr({ id }) 708 .addClass("point") 709 .select() 710 711 } 712 for (let oth in editor.backend.mappedPoints[id]) { 713 let map = editor.backend.mappedPoints[id][oth] 714 ops.add.pointmaps(map, editor.backend.obj(map)) 715 } 716 }, 717 pointmaps: function(id, value) { 718 let a = editor.backend.obj(value.a) 719 let b = editor.backend.obj(value.b) 720 let wall = editor.draw.findOneMax(byId(id)) 721 if (wall) { 722 wall.plot(a.x, a.y, b.x, b.y) 723 .removeClass(wall.data("type")) 724 .addClass(value.type) 725 .data("type", value.type) 726 } else { 727 wall = editor.draw.findExactlyOne("#pointmaps") 728 .line(a.x, a.y, b.x, b.y) 729 .stroke({ color: "black", width: 350 }) 730 .attr({ id }) 731 .addClass(value.type) 732 .data("type", value.type) 733 } 734 735 let sid = swingID(id) 736 if (value.type !== "door" || !value.door_swing) { 737 let s = editor.draw.findOne(byId(sid)) 738 if (s != null) { 739 s.remove() 740 } 741 } else { 742 a = new Vector2(a.x, a.y) 743 b = new Vector2(b.x, b.y) 744 if (value.door_swing.at(0) === "b") { 745 let t = a 746 a = b 747 b = t 748 } 749 750 let deg = 90 751 if (value.door_swing.at(1) === "-") { 752 deg = -deg 753 } 754 let e = b.clone().rotateAround(a, geometry.rad(deg)) 755 let r = a.distanceTo(b) 756 let d = `M ${b.x} ${b.y} A ${r} ${r} ${deg} 0 ${deg < 0 ? 0 : 1} ${e.x} ${e.y} L ${a.x} ${a.y} Z` 757 758 let swing = editor.draw.findOne(byId(sid)) 759 if (swing != null) { 760 swing.plot(d) 761 } else { 762 swing = editor.doorSwings.path(d) 763 .fill("rgba(0,0,0,.05)").stroke({ width: 100, color: "#AAA", dasharray: "400 100" }) 764 .attr({ id: sid }) 765 } 766 } 767 }, 768 furniture: function(id, value) { 769 let maps = editor.backend.cache.furniture_maps 770 for (let mid in maps) { 771 if (maps[mid].furniture_id == id) { 772 let m = editor.draw.findOneMax(byId(mid)) 773 if (m == null) { 774 ops.add.furniture_maps(id, editor.backend.cache[id]) 775 m = editor.draw.findOneMax(byId(mid)) 776 } 777 m.size(value.width, value.depth) 778 let t = m.findOne("title") 779 if (t == null) { 780 t = m.element("title") 781 } 782 t.words(furniture_name(value)) 783 m.load(furnitureImage(value)) 784 } 785 } 786 }, 787 furniture_maps: function(id, value) { 788 let f = editor.backend.reqObj(value.furniture_id) 789 let fm = editor.draw.findOneMax(byId(id)) 790 if (!fm) { 791 fm = editor.layoutG().image(furnitureImage(f)) 792 .size(f.width, f.depth) 793 .attr({ id, preserveAspectRatio: "none" }) 794 fm.on("error", function() { 795 if (this.attr("href") === "/furniture/any/default.svg") { 796 throw new Error("Unable to load furniture assets") 797 } 798 this.load("/furniture/any/default.svg") 799 }) 800 } 801 fm.cx(value.x).cy(value.y) 802 803 let tid = id + "_text" 804 let text = editor.draw.findOne(byId(tid)) 805 if (!f.name) { 806 if (text) { 807 text.remove() 808 } 809 } else { 810 if (!text) { 811 text = editor.ui.labels.text(f.name) 812 .attr({ id: tid }) 813 .font('size', '8in') 814 } else { 815 text.plain(f.name) 816 } 817 text.cx(value.x).cy(value.y) 818 } 819 820 fm.transform({ 821 rotate: value.angle 822 }) 823 } 824 }, 825 remove: { 826 points: function(id) { 827 // Remove pointmaps 828 editor.draw.findExactlyOne(byId(id)).remove() 829 }, 830 pointmaps: function(id) { 831 editor.draw.findExactlyOne(byId(id)).remove() 832 let s = editor.draw.findOne(byId(swingID(id))) 833 if (s != null) { 834 s.remove() 835 } 836 }, 837 furniture: function(name) {}, 838 furniture_maps: function(id) { 839 editor.draw.findExactlyOne(byId(id)).remove() 840 } 841 } 842 } 843 if (ops.replace) { 844 throw new Error("You messed up") 845 } 846 ops.replace = ops.add 847 ops.new = ops.add 848 849 if (!ops[diff.op]) { 850 throw new Error(diff.op + ": Unexpected patch operation") 851 } 852 853 let id = backend.parsePath(diff.path) 854 let t = backend.idTable(id) 855 if (!ops[diff.op][t]) { 856 throw new Error("Unhandled patch") 857 } 858 ops[diff.op][t](id, diff.value) 859 } 860 861 switchLayout(name) { 862 if (this.layout != null) { 863 this.layouts.findExactlyOne(byId(layoutID(this.layout))).hide() 864 } 865 this.layouts.findExactlyOne(byId(layoutID(name))).show() 866 this.layout = name 867 } 868 869 layoutG(name) { 870 if (name == null) { 871 name = this.layout 872 } 873 let id = layoutID(name) 874 let layout = this.layouts.findOneMax(byId(id)) 875 if (layout) { 876 return layout 877 } 878 layout = this.layouts.group().attr({id: id}) 879 return layout 880 } 881 882 findObj(id) { 883 return this.draw.findExactlyOne(byId(getID(id))) 884 } 885 886 variety(id) { 887 let t = backend.idType(id) 888 if (t === "furmap") { 889 id = this.backend.reqObj(id).furniture_id 890 } else if (t !== "fur") { 891 throw new Error(id + ": Unable to get furniture definition from that ID") 892 } 893 894 let f = structuredClone(this.backend.reqObj(id)) 895 let d = f.depth 896 return this.varietyFrom(f) 897 } 898 899 varietyFrom(params) { 900 if (this.backend.params.furniture[params.type] == null) { 901 throw new Error(params.type + ": Invalid furniture type") 902 } 903 let vars = this.backend.params.furniture[params.type].varieties 904 for (let v in vars) { 905 if (params.width == vars[v].width && params.depth == vars[v].depth) { 906 return v 907 } 908 } 909 return null 910 } 911 912 pointmapLength(map) { 913 map = this.backend.reqObj(getID(map)) 914 let a = this.backend.reqObj(map.a) 915 a = new Vector2(a.x, a.y) 916 let b = this.backend.reqObj(map.b) 917 b = new Vector2(b.x, b.y) 918 return a.distanceTo(b) 919 } 920 } 921 922 function layoutID(name) { 923 return "layout_" + name 924 } 925 926 function remove_mode_handlers(target, mode_handlers) { 927 for (let event in mode_handlers) { 928 for (let handler in mode_handlers[event]) { 929 console.debug("Remove mode handler", event, handler, "to", target) 930 let h = mode_handlers[event][handler] 931 if (event === "keydown" || event === "keyup") { 932 document.removeEventListener(event, h) 933 } else { 934 target.off(event, h) 935 } 936 } 937 } 938 } 939 940 function add_mode_handlers(target, mode_handlers) { 941 for (let event in mode_handlers) { 942 for (let handler in mode_handlers[event]) { 943 console.debug("Add mode handler", event, handler, "to", target) 944 let h = mode_handlers[event][handler] 945 if (event === "keydown" || event === "keyup") { 946 document.addEventListener(event, h) 947 } else { 948 target.on(event, h) 949 } 950 } 951 } 952 } 953 954 function gridPattern(editor, unit, using) { 955 let n = editor.units.get(unit) 956 return editor.draw.pattern(n, n, function(on) { 957 if (using) { 958 on.rect(n, n).fill(using.url()) 959 } 960 on.path(`M ${n} 0 L 0 0 0 ${n}`) 961 .fill("none") 962 .stroke({ width: n / 50, color: "grey" }) 963 }).attr({ id: "grid_" + unit + "_pattern", patternUnits: "userSpaceOnUse" }) 964 } 965 966 function gridSystem(editor, system) { 967 let unit = editor.units.systems[system] 968 let last 969 970 do { 971 last = gridPattern(editor, unit, last) 972 } while ((unit = editor.units.data[unit].next)); 973 return last 974 } 975 976 export function getID(thing, type) { 977 console.debug("getID", thing, type) 978 let id 979 if (typeof thing === "object") { 980 if (typeof thing.attr === "function") { 981 id = thing.attr("id") 982 } else if (typeof thing.type === "string" && typeof thing.id === "string") { 983 id = thing.id 984 } 985 } else if (typeof thing === "string") { 986 id = thing 987 } 988 989 if (id == undefined) { 990 console.error("Couldn't get id from", thing) 991 throw new Error("Invalid id") 992 } 993 if (type && backend.idTable(id) != type) { 994 throw new Error(`${backend.idTable(id)}: Invalid table (wanted ${type})`) 995 } 996 return id 997 } 998 999 function byId(id) { 1000 return "#" + id 1001 } 1002 1003 function furniture_name(f) { 1004 return f.name ? `${f.name} (${f.type})` : f.type 1005 } 1006 1007 function swingID(id) { 1008 return id + "_swing" 1009 } 1010 1011 function furnitureImage(f) { 1012 return `/furniture/${f.type}/${f.style ?? "default"}.svg` 1013 }