main.js (44672B)
1 import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js" 2 import "/lib/github.com/svgdotjs/svg.panzoom.js/svg.panzoom.js" 3 import * as ui from "/lib/ui.js" 4 import * as etc from "/lib/etc.js" 5 import * as lib from "./editor.js" // Confusing, but I don't want to fix variable conflict 6 import { Vector2 } from "/lib/github.com/mrdoob/three.js/math/Vector2.js" 7 import * as geometry from "./geometry.js" 8 import * as backend from "./backend.js" 9 import * as api from "/lib/api.js" 10 11 const defaultMode = "Edit" 12 const messageTimeout = 4000 13 14 const buttons = { 15 left: 0, 16 middle: 1, 17 right: 2 18 } 19 20 const params = { 21 longpress: 350, 22 notMoveRadius: 20, 23 threshold: 400 24 } 25 26 const panBit = 1 27 const zoomBit = 2 28 29 const modes = { 30 View: { 31 handlers: { 32 contextmenu: preventDefaultHandler, 33 keydown: keyHandler 34 } 35 }, 36 Edit: { 37 points: true, 38 handlers: { 39 contextmenu: preventDefaultHandler, 40 pointerdown: [singlePointerHandler, selectionHandler, selectionBoxHandler, precisePointHandler, precisePointMapHandler, furnitureHandler, addFurnitureHandler], 41 pointermove: [singlePointerHandler, selectionBoxHandler, precisePointHandler, precisePointMapHandler, furnitureHandler, addFurnitureHandler], 42 pointerup: [singlePointerHandler, selectionBoxHandler, precisePointHandler, precisePointMapHandler, furnitureHandler, addFurnitureHandler], 43 pointercancel: [singlePointerHandler, selectionBoxHandler, precisePointHandler, precisePointMapHandler, furnitureHandler], 44 keydown: [keyHandler], 45 select: selectHandler, 46 reselect: selectHandler 47 } 48 }, 49 Select: { 50 points: true, 51 handlers: { 52 contextmenu: preventDefaultHandler, 53 pointerdown: [singlePointerHandler, selectionHandler, selectionBoxHandler], 54 pointermove: [singlePointerHandler, selectionBoxHandler], 55 pointerup: [singlePointerHandler, selectionBoxHandler], 56 pointercancel: [singlePointerHandler, selectionBoxHandler], 57 keydown: keyHandler, 58 select: selectHandler, 59 reselect: selectHandler 60 } 61 } 62 } 63 64 let State = { 65 panZoom: 0, 66 pointOp: 'Create', 67 snapAngle: true, 68 snapPoints: true, 69 lastClick: null, 70 furnRotationSnap: true 71 } 72 73 const debug = (new URLSearchParams(new URL(document.URL).search)).get("debug") != undefined 74 75 // turn off bubbling 76 const escapeEvent = new Event("escape") 77 const cancelEvent = new PointerEvent("pointercancel") 78 79 etc.handle_wrap(init) 80 81 function init() { 82 ui.wait("Loading data...") 83 84 let floorplan_id = (new URLSearchParams(new URL(document.URL).search)).get("id") 85 if (!floorplan_id) { 86 document.location.href = "/floorplans" 87 return 88 } 89 90 let floorplan 91 if (floorplan_id === "flp_demo") { 92 let f = document.body.appendChild(document.createElement("footer")) 93 f.id = "demo_footer" 94 f.append(document.createTextNode("Missing something? Click ")) 95 let a = f.appendChild(document.createElement("a")) 96 a.append(document.createTextNode("here")) 97 a.href = "/features/upcoming.html" 98 f.append(document.createTextNode(" to see upcoming features or put in a request")) 99 } else { 100 etc.authorize() 101 floorplan = { user: localStorage.getItem("username"), id: floorplan_id } 102 } 103 104 etc.bar() 105 106 let h1 = document.querySelector("h1") 107 let suffix = h1.appendChild(document.createTextNode("")) 108 if (!floorplan) { 109 h1.textContent = "Demo" 110 } else { 111 api.fetch("GET", `floorplans/:user/${floorplan.id}`) 112 .then(function(metadata) { 113 h1.textContent = metadata.name 114 }) 115 .catch(function(err) { 116 document.location.href = "/floorplans" 117 }) 118 } 119 120 let draw = SVG() 121 .addTo("#editor") 122 .panZoom({ 123 panButton: buttons.left, 124 // These need to be set using device size 125 zoomMin: .001, 126 zoomMax: .5, 127 zoomFactor: .5 128 }) 129 130 let editor = new lib.FloorplanEditor(draw, floorplan, 131 { backend: { 132 callbacks: { 133 pull: function() { 134 suffix.data = "" 135 }, 136 push: function() { 137 suffix.data = "" 138 status() 139 }, 140 pusherror: fetchError 141 } 142 } 143 }) 144 145 editor.initialized 146 .then(function() { run(editor) }) 147 .catch(etc.error) 148 } 149 150 function run(editor) { 151 ui.wait("Initializing editor...") 152 153 editor.useUnits("imperial") 154 editor.draw.hide() 155 156 for (let mode in modes) { 157 editor.addMode(mode, modes[mode]) 158 } 159 editor.useMode(defaultMode) 160 editor.furnitureLabels(false) 161 162 let toolbar = document.querySelector("header") 163 .appendChild(document.createElement("ul")) 164 toolbar.classList.add("toolbar") 165 166 let undoRedo = document.createElement("li") 167 // These could be hidden when they wouldn't have any effect 168 undoRedo.append(ui.button("Undo", 169 "Undo last action (you may also press control-z)", 170 "arrow-undo-circle", { 171 handlers: { 172 click: function() { 173 editor.undo() 174 } 175 } 176 }) 177 ) 178 undoRedo.append(ui.button("Redo", 179 "Redo next action (you may also press control-y)", 180 "arrow-redo-circle", { 181 handlers: { 182 click: function() { 183 editor.redo() 184 } 185 } 186 }) 187 ) 188 189 let addFurn = ui.button("Add Furniture", "Add furniture", null, { handlers: { 190 // TODO: Create it at the last clicked point if on screen, else somewhere reasonable on screen 191 click: function() { 192 let p 193 const v = editor.draw.viewbox() 194 const c = { x: v.x + v.width / 2, y: v.y + v.height / 2 } 195 if (State.lastClick != null) { 196 p = State.lastClick 197 if (p.x < v.x || p.x > v.x + v.width || p.y < p.height || p.y > p.y + p.height) { 198 p = c 199 } 200 } else { 201 p = c 202 } 203 furnitureMenu(editor, p) 204 } 205 }}) 206 207 const newGroup = function(name) { 208 return toolbar.appendChild(item(toolGroup(name))).querySelector("ul") 209 } 210 211 let tg = newGroup("More...") 212 213 tg.append(item(ui.button("Fit to view", "Fit the floor plan into view", null, { 214 handlers: { click: function() { 215 editor.fitToView() 216 }} 217 }))) 218 tg.append(item(ui.button("Furniture labels", "Toggle furniture label visability", null, { 219 handlers: { click: function() { 220 editor.furnitureLabels(!editor.furnitureLabels()) 221 }} 222 }))) 223 tg.append(item( 224 selector(editor.modes, function(mode) { 225 if (mode === "View") { 226 editor.draw.unselect() 227 escape() 228 } 229 editor.useMode(mode) 230 }, 231 { current: editor.mode, text: "Mode:" } 232 ) 233 )) 234 tg.append(item(checkToggle("Angle snap", { 235 title: "Snap points to 45° angle", 236 off: function() { State.snapAngle = false }, 237 on: function() { State.snapAngle = true }, 238 value: State.snapAngle 239 }))) 240 tg.append(item(checkToggle("Point snap", { 241 title: "Snap points to other points", 242 off: function() { State.snapPoints = false }, 243 on: function() { State.snapPoints = true }, 244 value: State.snapPoints 245 }))) 246 tg.append(item(checkToggle("Furniture angle snap", { 247 title: "Snap furniture rotation at 45 degree angles", 248 off: function() { State.furnRotationSnap = false }, 249 on: function() { State.furnRotationSnap = true }, 250 value: State.furnRotationSnap 251 }))) 252 253 toolbar.append(undoRedo) 254 toolbar.append(item(addFurn)) 255 256 if (debug) { 257 toolbar.append(item( 258 selector(editor.units.systems, function(system) { editor.useUnits(system) }, 259 { current: editor.unitSystem, text: "Units:" } 260 ) 261 )) 262 } 263 264 editor.useGrid() 265 editor.draw.show() 266 editor.backend.pull() 267 .then(function() { 268 ui.wait() 269 if (editor.draw.findExactlyOne("#points").children().length === 0) { 270 editor.addPoint({ x: 0, y: 0 }) 271 } 272 editor.useGrid("imperial") 273 editor.fitToView() 274 }) 275 276 editor.draw.on("touchstart", function(e){ e.preventDefault() }); 277 editor.draw.on("touchmove", function(e){ e.preventDefault() }); 278 editor.draw.on("pinchZoomStart", function() { State.panZoom |= zoomBit }) 279 editor.draw.on("pinchZoomEnd", function() { State.panZoom &= ~zoomBit}) 280 editor.draw.on("panStart", function() { State.panZoom |= panBit }) 281 editor.draw.on("panEnd", function() { State.panZoom &= ~panBit }) 282 } 283 284 function checkToggle(name, params) { 285 if (!params || !params.off || !params.on) { 286 throw new Error("Requires on and off values") 287 } 288 289 const run = function(value) { 290 if (value) { 291 params.on() 292 } else { 293 params.off() 294 } 295 } 296 297 let c = document.createElement("form") 298 c.classList.add("check_toggle") 299 300 let label = c.appendChild(document.createElement("label")) 301 label.append(document.createTextNode(name + ": ")) 302 303 let input = c.appendChild(document.createElement("input")) 304 input.setAttribute("type", "checkbox") 305 input.setAttribute("checked", params.value ?? false) 306 input.addEventListener("change", function(ev) { run(ev.target.checked) }) 307 if (params.title) { 308 input.setAttribute("title", params.title) 309 } 310 311 if (params.value != undefined) { 312 input.checked = params.value 313 run(params.value) 314 } 315 return c 316 } 317 318 function selectHandler(event, editor, state) { 319 let old = document.getElementById("selOps") 320 if (!event.detail.selected) { 321 if (old) { 322 old.remove() 323 } 324 return 325 } 326 let a = event.detail.selected.array() 327 let c = document.createElement("li") 328 c.setAttribute("id", "selOps") 329 330 const refresh = function() { 331 selectHandler(event, editor, state) 332 } 333 334 c.append(ui.button("Unselect", "Unselect selection", null, { 335 handlers: { click: function() { editor.draw.select() } } 336 })) 337 338 c.append(ui.input("Delete", "Delete selected objects", { 339 attributes: { type: "button", value: "Delete" }, 340 handlers: { click: function() { 341 editor.remove(...a) 342 editor.finishAction() 343 }}, 344 }) 345 ) 346 347 let ids = [] 348 for (let i = 0; i < a.length; ++i) { 349 ids.push(lib.getID(a[i])) 350 } 351 352 let groups = {} 353 let cnt = 0 354 for (let i = 0; i < ids.length; ++i) { 355 let t = backend.idType(ids[i]) 356 if (groups[t] === undefined) { 357 groups[t] = [] 358 } 359 groups[t].push(ids[i]) 360 ++cnt 361 } 362 363 if (groups.pnt && groups.pnt.length === cnt) { 364 const pmode = function(mode) { State.pointOp = mode } 365 // NOTE: Not sure if this is the behavior I want. 366 //pmode("Create") 367 c.appendChild( 368 selector({ Create: true, Move: true }, pmode, { current: State.pointOp }) 369 ) 370 } 371 372 if (groups.pntmap !== undefined || groups.pnt !== undefined) { 373 const getMaps = function() { 374 let maps = {} 375 if (groups.pnt) { 376 for (let i = 0; i < groups.pnt.length; ++i) { 377 for (let id in editor.backend.mappedPoints[groups.pnt[i]]) { 378 id = editor.backend.mappedPoints[groups.pnt[i]][id] 379 maps[id] = editor.backend.reqObj(id) 380 } 381 } 382 } 383 if (groups.pntmap) { 384 for (let i = 0; i < groups.pntmap.length; ++i) { 385 maps[groups.pntmap[i]] = editor.backend.cache.pointmaps[groups.pntmap[i]] 386 } 387 } 388 return maps 389 } 390 const changeTypes = function(newvalue) { 391 editor.finishAction() 392 const maps = getMaps() 393 for (let id in maps) { 394 editor.mapPoints({ type: newvalue }, id) 395 } 396 editor.finishAction() 397 return refresh() 398 } 399 400 let current 401 const maps = getMaps() 402 for (let id in maps) { 403 if (current === undefined) { 404 current = maps[id].type 405 } else if (current !== maps[id].type) { 406 current = null 407 break; 408 } 409 } 410 c.appendChild( 411 selector(editor.backend.params.pointmaps.types, changeTypes, { current, text: "Type:" }) 412 ) 413 } 414 415 if (groups.pntmap && cnt === 1) { 416 c.appendChild(document.createElement("li")) 417 .appendChild(ui.button("Subdivide", "Subdivide pointmap", null, { 418 handlers: { 419 click: function(ev) { 420 handled(ev) 421 let pm = editor.findObj(groups.pntmap[0]) 422 let p 423 if (State.lastClick) { 424 // This will mostly work well, but if the user manages to click 425 // somewhere else and keep this selected it'll maybe not be where 426 // they want. 427 p = pm.closestPoint(new Vector2(State.lastClick.x, State.lastClick.y)) 428 } else { 429 p = pm.interpolatedPoint(0.5) 430 } 431 let pid = editor.addPoint(p) 432 pm = editor.backend.reqObj(groups.pntmap[0]) 433 editor.mapPoints({ a: pid, b: pm.a }) 434 editor.mapPoints({ a: pid, b: pm.b }) 435 editor.remove(groups.pntmap[0]) 436 } 437 } 438 })) 439 c.appendChild(document.createElement("li")) 440 .appendChild(document.createTextNode("Length: " + 441 userLength(editor, editor.pointmapLength(groups.pntmap[0])))) 442 443 const dolengths = function(m, point) { 444 let intr = m.closestLinearInterpolation(new Vector2(point.x, point.y)) 445 let points = m.vecs() 446 let len = geometry.length(points[0], points[1]) 447 let al = len * intr 448 let bl = len * (1.0 - intr) 449 if (!geometry.compareVecs(points[0], points[1])) { 450 let t = al 451 al = bl 452 bl = t 453 } 454 455 let it = c.appendChild(document.createElement("li")) 456 it.id = "pointmap_lengths" 457 it.append(document.createTextNode("(")) 458 let a = it.appendChild(document.createElement("span")) 459 it.append(document.createTextNode(" × ")) 460 let b = it.appendChild(document.createElement("span")) 461 it.append(document.createTextNode(")")) 462 463 a.textContent = userLength(editor, al) 464 b.textContent = userLength(editor, bl) 465 } 466 let m = editor.findObj(groups.pntmap[0]) 467 dolengths(m, State.lastClick) 468 /* 469 * In the future, I could change the function to update the values, 470 * but it wouldn't work well on touch devices anyway. 471 *m.on("pointermove", function(ev) { dolengths(m, editor.draw.point(ev.clientX, ev.clientY)) }) 472 */ 473 474 let pm = editor.backend.reqObj(groups.pntmap[0]) 475 if (pm.type === "door") { 476 const swingButton = function(backward) { 477 return ui.button(backward ? "prevswing" : "swing", 478 `Swing door${backward ? " backward" : ""} (you may also click, drag, and release on a door)`, 479 backward ? "arrow-back" : "arrow-forward", { 480 handlers: { 481 click: function() { 482 let pm = editor.backend.reqObj(groups.pntmap[0]) 483 editor.mapPoints({ door_swing: (backward ? prevSwing : nextSwing)(pm.door_swing) }, groups.pntmap[0]) 484 } 485 } 486 }) 487 } 488 let swingOps = c.appendChild(document.createElement("li")) 489 swingOps.append(document.createTextNode("Swing: ")) 490 swingOps.append(swingButton(true)) 491 swingOps.append(swingButton(false)) 492 swingOps.append(ui.button("Reset", "Reset door swing", null, { 493 handlers: { 494 click: function() { 495 editor.mapPoints({ door_swing: null }, groups.pntmap[0]) 496 } 497 } 498 })) 499 } 500 } 501 502 if (groups.furmap) { 503 if (groups.furmap.length !== 1) { 504 document.querySelectorAll(".furniture_menu").forEach( 505 function(e) { 506 e.remove() 507 } 508 ) 509 } else if (cnt === 1) { 510 furnitureMenu(editor, groups.furmap[0]) 511 } 512 } 513 514 if (old) { 515 old.replaceWith(c) 516 } else { 517 document.querySelector(".toolbar") 518 .appendChild(c) 519 } 520 } 521 522 function selector(things, select, options) { 523 options = options ?? {} 524 525 let form = document.createElement("form") 526 form.classList.add("selection") 527 528 if (options.text) { 529 form.appendChild(document.createTextNode(options.text + " ")) 530 } 531 532 let list = form.appendChild(document.createElement("select")) 533 list.addEventListener("change", function(event) { select(event.target.value) }) 534 535 let isArray = Array.isArray(things) 536 for (let thing in things) { 537 if (isArray) { 538 thing = things[thing] 539 } 540 541 console.debug("selector", options.text ?? "something", thing) 542 list.appendChild(document.createElement("option")) 543 .appendChild(document.createTextNode(thing)) 544 } 545 546 list.value = options.current 547 548 return form 549 } 550 551 // pointerdown 552 function selectionHandler(event, editor) { 553 let sel 554 555 if (event.pointerType === "mouse" && event.button === buttons.right) { 556 return 557 } 558 559 let p = editor.draw.point(event.clientX, event.clientY) 560 State.lastClick = structuredClone(p) 561 let order = [ "#" + editor.layoutG(), "#points", "#pointmaps" ] 562 for (let i = 0; !sel && i < order.length; ++i) { 563 sel = editor.thingAt(p, order[i]) 564 } 565 566 if (!sel) { 567 let close = editor.thingsAt(p, order.join(","), { method: "touching", minsize: 3500 }) 568 let dist 569 let closest 570 for (let i = 0; i < close.length; ++i) { 571 let tmp = close[i].distanceTo(p.x, p.y) 572 if (dist == null || tmp < dist) { 573 dist = tmp 574 closest = close[i] 575 } 576 } 577 sel = closest 578 } 579 580 if (sel != null) { 581 if (editor.mode !== "Select") { 582 sel.select() 583 } else { 584 let selection = addSelection(editor, sel, true) 585 if (selection.length === 0) { 586 editor.draw.select() 587 } else { 588 selection.selectList() 589 } 590 } 591 } else { 592 if (editor.mode !== "Select") { 593 editor.draw.select() 594 } 595 escape() 596 } 597 } 598 599 // pointerdown, pointercancel, pointermove, pointerup 600 function selectionBoxHandler(ev, editor, state) { 601 const cleanup = function() { 602 if (state.rect) { 603 state.rect.remove() 604 } 605 for (let k in state) { 606 delete state[k] 607 } 608 } 609 const useEv = function(ev) { 610 if (ev.pointerType === "mouse") { 611 if (ev.type === "pointermove") { 612 return true 613 } 614 let p = primaryPointer(ev) 615 return state.shiftStart ? p : !p 616 } else if (editor.mode === "Select") { 617 return primaryPointer(ev) 618 } else { 619 return false 620 } 621 } 622 623 if (ev.type === "pointercancel") { 624 cleanup() 625 return 626 } 627 628 if (ev.type === "pointerdown" && ev.shiftKey) { 629 state.shiftStart = true 630 } 631 632 if (ev.type === "pointerdown" && useEv(ev)) { 633 handled(ev) 634 let p = editor.draw.point(ev.clientX, ev.clientY) 635 if (state.rect) { 636 console.error("selectionBoxHandler state got messed up") 637 cleanup() 638 } 639 state.origin = structuredClone(p) 640 state.rect = editor.ui.top.rect(0, 0) 641 .move(p.x, p.y) 642 .stroke({ width: 50, dasharray: [350, 350], color: "blue" }) 643 .fill("rgba(0,0,1,.025)") 644 return 645 } 646 647 if (!state.rect) { 648 return 649 } 650 651 let p = editor.draw.point(ev.clientX, ev.clientY) 652 if (ev.type === "pointermove" && useEv(ev)) { 653 handled(ev) 654 let params = {} 655 if (state.origin.x > p.x) { 656 params.x = p.x 657 params.width = state.origin.x - p.x 658 } else { 659 params.x = state.origin.x 660 params.width = p.x - state.origin.x 661 } 662 if (state.origin.y > p.y) { 663 params.y = p.y 664 params.height = state.origin.y - p.y 665 } else { 666 params.y = state.origin.y 667 params.height = p.y - state.origin.y 668 } 669 state.rect.move(params.x, params.y).size(params.width, params.height) 670 } else if (ev.type === "pointerup" && useEv(ev)) { 671 handled(ev) 672 let objects = editor.draw.find("#floorplan > :not(#furniture_layouts, #door_swings) > *, #furniture_layouts > * > *") 673 let s = state.rect.rbox(state.rect.root()) 674 let selection = [] 675 objects.each(function() { 676 let b = this.rbox(this.root()) 677 if (((b.x >= s.x && b.x <= s.x2) || (b.x2 >= s.x && b.x2 <= s.x2)) && 678 ((b.y >= s.y && b.y <= s.y2) || (b.y2 >= s.y && b.y2 <= s.y2))) { 679 selection.push(this) 680 } 681 682 }) 683 if (selection.length > 0) { 684 addSelection(editor, selection).selectList() 685 } 686 cleanup() 687 } 688 } 689 690 function keyHandler(ev, editor) { 691 if (ev.key === "Escape") { 692 escape() 693 /*} else if (ev.key === "Backspace" || ev.key === "Delete") { 694 editor.remove(...editor.draw.find(".selected").array())*/ 695 } else if (ev.key === "+") { 696 editor.draw.zoom(editor.draw.zoom() * 1.25) 697 editor.updateGrid() 698 } else if (ev.key === "-" || ev.key === "_") { 699 editor.draw.zoom(editor.draw.zoom() / 1.25) 700 editor.updateGrid() 701 } else { 702 if (!event.ctrlKey) { 703 return 704 } 705 if (event.key === "z") { 706 editor.undo() 707 } else if (event.key === "y") { 708 editor.redo() 709 } else { 710 return 711 } 712 } 713 714 handled(ev) 715 } 716 717 function addSelection(editor, objects, flip) { 718 if (!Array.isArray(objects)) { 719 objects = [objects] 720 } 721 722 let sel = editor.draw.find(".selected") 723 for (let i = 0; i < objects.length; ++i) { 724 let si = sel.indexOf(objects[i]) 725 if (si >= 0) { 726 if (flip) { 727 sel.splice(si, 1) 728 } 729 } else { 730 sel.push(objects[i]) 731 } 732 } 733 return sel 734 } 735 736 function radioMenu(editor, key, values, initial, options) { 737 options = options ?? {} 738 options.callbacks = options.callbacks ?? {} 739 740 let menu = document.createElement("div") 741 menu.classList.add("menu") 742 743 menu.appendChild(document.createTextNode(key + ": ")) 744 745 let form = menu.appendChild(document.createElement("form")) 746 let container = form 747 748 if (options.legend) { 749 container.appendChild(document.createElement("legend")) 750 .appendChild(document.createTextNode(options.legend)) 751 } 752 753 let radios = radioInputs(key, values, initial) 754 for (let i in radios) { 755 if (options.callbacks.change) { 756 radios[i].addEventListener("change", function(event) { 757 options.callbacks.change(event.target.value) 758 handled(event) 759 }) 760 } 761 container.append(radios[i]) 762 } 763 764 if (options.nosubmit) { 765 return menu 766 } 767 768 container.appendChild(document.createTextNode(" ")) 769 let submit = container.appendChild(document.createElement("input")) 770 submit.setAttribute("type", "submit") 771 submit.setAttribute("value", "Change") 772 773 form.addEventListener("submit", function(event) { 774 if (options.callbacks.commit) { 775 options.callbacks.commit(event) 776 } 777 handled(event) 778 }) 779 780 return menu 781 } 782 783 function radioInputs(key, values, initial) { 784 let radios = [] 785 for (let i in values) { 786 let label = document.createElement("label") 787 let radio = label.appendChild(document.createElement("input")) 788 radio.setAttribute("type", "radio") 789 radio.setAttribute("name", key) 790 radio.setAttribute("value", values[i]) 791 if (values[i] === initial) { 792 radio.setAttribute("checked", true) 793 } 794 label.append(radio) 795 label.append(document.createTextNode(values[i])) 796 radios.push(label) 797 } 798 return radios 799 } 800 801 // pointerdown, pointermove, pointerup 802 function precisePointHandler(event, editor, state) { 803 const init = function() { 804 state.menu = document.body.querySelector(".toolbar") 805 .appendChild(document.createElement("li")) 806 state.menu.classList.add("menu") 807 state.menu.appendChild(document.createTextNode("Length: ")) 808 state.len = state.menu 809 .appendChild(document.createElement("input")) 810 state.len.value = 0 811 state.len.addEventListener("input", function(event) { 812 let vecs = state.line.vecs() 813 let len = editor.units.snapTo(unitInput(editor, event.target.value), editor.unit) 814 if (len == null) { 815 return 816 } 817 if (len> 0) { 818 vecs[1] = geometry.length(vecs[0], vecs[1], len) 819 updatePoint(vecs[1], { leave_input: true }) 820 } 821 }) 822 } 823 824 const cleanup = function() { 825 if (state.moveTimeout != null) { 826 clearTimeout(state.moveTimeout) 827 } 828 if (state.menu != undefined) { 829 state.menu.remove() 830 } 831 for (let i in state) { 832 delete state[i] 833 } 834 } 835 836 const updatePoint = function(p, options) { 837 options = options ?? {} 838 839 let points = editor.thingsAt(p, "#points") 840 let fid = !state.moveOp ? lib.getID(state.from) : null 841 let tid = lib.getID(state.to) 842 delete state.onPoint 843 for (let i in points) { 844 let id = lib.getID(points[i]) 845 if (id !== tid && (!fid || id !== fid)) { 846 state.onPoint = id 847 p = editor.backend.obj(id) 848 } 849 } 850 851 editor.movePoint(state.to, p) 852 853 if (!options.leave_input) { 854 unitInput(editor, state.len, 855 editor.units.snapTo(state.origin.distanceTo(p), editor.unit)) 856 } 857 } 858 859 const doMove = function() { 860 const ad = function(a, b) { 861 return Math.abs(a - b) 862 } 863 864 // This is racy 865 state.moveTimeout = null 866 if (state.nosnap) { 867 updatePoint(state.move) 868 return 869 } 870 871 let snapped = state.move 872 if (State.snapAngle) { 873 snapped = snap(editor.units.snapTo(state.move, editor.unit), state.origin, 8) 874 } 875 876 if (State.snapPoints) { 877 const updsnaps = function(snaps, k, from, test) { 878 let d = ad(from[k], test[k]) 879 if (d <= params.threshold) { 880 if (!snaps[k] || d < snaps[k].d) { 881 snaps[k] = { d, v: test[k] } 882 } 883 } 884 } 885 886 let points = editor.backend.cache.points 887 let snaps = {} 888 let exclude = lib.getID(state.to) 889 for (let p in points) { 890 if (p != exclude) { 891 updsnaps(snaps, "x", snapped, points[p]) 892 updsnaps(snaps, "y", snapped, points[p]) 893 } 894 } 895 if (snaps.x != null) { 896 snapped.x = snaps.x.v 897 } 898 if (snaps.y != null) { 899 snapped.y = snaps.y.v 900 } 901 } 902 updatePoint(snapped) 903 } 904 905 const revert = function() { 906 /* 907 * NOTE: WARNING: If allowing asyncronous edits this would be bad 908 * I should introduce a revert function which takes diffs and reverts 909 * them specifically, and I suppose pass a diff id with every single action. 910 * I think asyncronous actions would add very little in terms of value, 911 * and take time to implement. Better to disallow for now. 912 */ 913 editor.finishAction() 914 editor.undo() 915 cleanup() 916 } 917 918 const onMap = function(p) { 919 let maps = editor.thingsAt(p, "#pointmaps") 920 let toMaps = editor.backend.mappedPoints[state.to] 921 922 let map 923 for (let i = 0; !map && i < maps.length; ++i) { 924 let mid = lib.getID(maps[i]) 925 let good = true 926 for (let k in toMaps) { 927 if (toMaps[k] === mid) { 928 good = false 929 break 930 } 931 } 932 if (good) { 933 map = maps[i] 934 } 935 } 936 937 if (map == null) { 938 return null 939 } 940 return { point: map.whereIsPoint(p.x, p.y), map: map } 941 } 942 943 const commit = function() { 944 if (state.onPoint) { 945 let tid = lib.getID(state.to) 946 for (let oth in editor.backend.mappedPoints[tid]) { 947 if (oth !== state.onPoint) { 948 editor.mapPoints({ a: state.onPoint, b: oth }, editor.backend.mappedPoints[state.to][oth]) 949 } 950 } 951 editor.remove(tid) 952 state.to = editor.findObj(state.onPoint) 953 } 954 955 let on = onMap(state.to.vec()) 956 if (on !== null) { 957 let mapD = editor.backend.reqObj(lib.getID(on.map)) 958 editor.movePoint(state.to, on.point) 959 editor.mapPoints({ a: mapD.a, b: state.to }) 960 editor.mapPoints({ a: mapD.b, b: state.to }) 961 editor.remove(on.map) 962 } 963 editor.finishAction() 964 cleanup() 965 } 966 967 if (State.panZoom || event.type === "pointercancel") { 968 if (state.to) { 969 revert() 970 } 971 return 972 } 973 974 if (event.type === "pointermove") { 975 if (!primaryMove(event)) { 976 return 977 } 978 } else if (!truelyPrimary(event)) { 979 return 980 } 981 982 let cursor = editor.draw.point(event.clientX, event.clientY).vec() 983 if (state.to == undefined) { 984 if (event.type === "pointerdown") { 985 if (state.from != undefined) { 986 return 987 } 988 989 state.from = editor.selectedOne("points") 990 if (!state.from) { 991 return 992 } 993 994 if (State.pointOp === 'Move') { 995 state.to = state.from 996 state.from = null 997 state.moveOp = true 998 999 // I want the first pointmap defined, but this for now 1000 let m = editor.backend.mappedPoints[lib.getID(state.to)] 1001 for (let point in m) { 1002 state.from = editor.findObj(point) 1003 break 1004 } 1005 if (!state.from) { 1006 // I mean, there really shouldn't be an orphaned point, 1007 // and I see no reason to move the only point in the plan 1008 cleanup() 1009 throw new Error("Can't move unmapped points") 1010 } 1011 init() 1012 } 1013 1014 state.origin = cursor 1015 } else if (event.type === "pointerup") { 1016 if (state.from) { 1017 cleanup() 1018 } else { 1019 return 1020 } 1021 } else if (event.type === "pointermove" && state.origin != undefined && 1022 state.origin.distanceTo(cursor) > params.threshold) { 1023 state.to = editor.addPoint(cursor, true) 1024 editor.mapPoints({ type: "wall", a: state.from, b: state.to}) 1025 state.to = editor.findObj(state.to) 1026 init() 1027 } else { 1028 return 1029 } 1030 handled(event) 1031 return 1032 } 1033 1034 if (state.to == undefined) { 1035 return 1036 } 1037 if (!state.from) { 1038 throw new Error("Hmm") 1039 } 1040 1041 if (event.type === "pointermove") { 1042 // This is still far too expensive, it runs up my fans in seconds. 1043 state.move = cursor 1044 state.nosnap = event.shiftKey 1045 if (state.moveTimeout == null) { 1046 state.moveTimeout = setTimeout(doMove, 35) 1047 } 1048 } else if (event.type === "pointerup") { 1049 if (!state.moveOp && state.from && state.from.inside(cursor.x, cursor.y)) { 1050 revert() 1051 } else { 1052 // Not that it makes much difference, but should probably use 1053 // state.to's position 1054 if (state.to && state.origin.distanceTo(cursor) > 0) { 1055 commit() 1056 } else { 1057 cleanup() 1058 } 1059 } 1060 } else { 1061 console.warn("Bit of a state mismatch, not that big of a deal though") 1062 commit() 1063 } 1064 1065 handled(event) 1066 } 1067 1068 // pointerdown, pointermove, pointerup 1069 function precisePointMapHandler(event, editor, state) { 1070 const cleanup = function() { 1071 for (let i in state) { 1072 delete state[i] 1073 } 1074 } 1075 1076 if (event.type === "pointercancel") { 1077 cleanup() 1078 return 1079 } 1080 1081 let cursor = editor.draw.point(event.clientX, event.clientY).vec() 1082 if (editor.thingAt(cursor, "#points")) { 1083 return 1084 } 1085 1086 if (event.type === "pointermove") { 1087 if (!state.door) { 1088 return 1089 } 1090 handled(event) 1091 1092 let door = editor.findObj(state.doorID) 1093 if (state.doorSwingFrom.distanceTo(cursor) < params.threshold) { 1094 return 1095 } 1096 1097 let o = door.point_offset(cursor) 1098 if (state.hinge === "b") { 1099 o = -o 1100 } 1101 1102 let v = door.vecs() 1103 if (v[0].y < v[1].y) { 1104 o = -o 1105 } 1106 1107 let s = (o > 0 ? "+" : "-") 1108 editor.backend.mapPoints({ door_swing: state.hinge + s }, state.doorID) 1109 } 1110 1111 if (event.type === "pointerup") { 1112 if (!state.door) { 1113 return 1114 } 1115 handled(event) 1116 editor.finishAction() 1117 cleanup() 1118 return 1119 } 1120 1121 let map = editor.selectedOne("pointmaps") 1122 if (map == null) { 1123 return 1124 } 1125 1126 let id = lib.getID(map) 1127 let data = editor.backend.obj(id) 1128 if (data.type !== "door" || !truelyPrimary(event)) { 1129 return 1130 } 1131 1132 if (event.type === "pointerdown") { 1133 handled(event) 1134 state.door = data 1135 state.doorID = id 1136 state.doorSwingFrom = cursor.clone() 1137 state.hinge = Math.round(editor.findObj(id).closestLinearInterpolation(cursor)) ? "a" : "b" 1138 } else { 1139 console.log("Hmm", event) 1140 } 1141 } 1142 1143 // pointerdown, pointerup 1144 function addFurnitureHandler(ev, editor, state) { 1145 const cleanup = function() { 1146 if (state.timeout) { 1147 clearTimeout(state.timeout) 1148 } 1149 for (let k in state) { 1150 delete state[k] 1151 } 1152 } 1153 1154 let p = new Vector2(ev.clientX, ev.clientY) 1155 1156 if (!state.down) { 1157 if (ev.pointerType === "mouse" && ev.type === "pointerdown") { 1158 state.down = p 1159 state.time = ev.timeStamp 1160 state.timeout = setTimeout(function() { 1161 furnitureMenu(editor, editor.draw.point(state.down.x, state.down.y).vec()) 1162 cleanup() 1163 }, params.longpress) 1164 } 1165 return 1166 } 1167 1168 if (ev.type === "pointermove") { 1169 if (state.down.distanceTo(p) < params.notMoveRadius) { 1170 return 1171 } 1172 } 1173 1174 cleanup() 1175 } 1176 1177 // pointerdown, pointerup, pointermove 1178 function furnitureHandler(ev, editor, state) { 1179 const doMove = function() { 1180 // racy 1181 if (state.move) { 1182 let id = state.moving.attr("id") 1183 editor.mapFurniture({ x: state.move.x + state.offset.x, y: state.move.y + state.offset.y }, id) 1184 delete state.move 1185 state.moved = true 1186 } 1187 } 1188 const cleanup = function() { 1189 if (state.moved) { 1190 editor.finishAction() 1191 } 1192 for (let k in state) { 1193 delete state[k] 1194 } 1195 } 1196 1197 if (state.panZoom || ev.type === "pointercancel") { 1198 cleanup() 1199 return 1200 } 1201 1202 let press = editor.draw.point(ev.clientX, ev.clientY).vec() 1203 let sel = editor.draw.find("#furniture_layouts > * > .selected").array() 1204 if (sel.length !== 1) { 1205 return 1206 } 1207 1208 if (ev.type === "pointerdown" && truelyPrimary(ev)) { 1209 handled(ev) 1210 state.moving = sel[0] 1211 state.origin = press 1212 let m = editor.backend.reqObj(lib.getID(sel[0])) 1213 state.offset = { x: m.x - state.origin.x, y: m.y - state.origin.y } 1214 return 1215 } 1216 1217 if (!state.moving) { 1218 return 1219 } 1220 1221 if (ev.type === "pointermove" && primaryMove(ev)) { 1222 if (!state.moved && press.distanceTo(state.origin) < params.threshold) { 1223 return 1224 } 1225 state.moved = true 1226 handled(ev) 1227 if (state.move) { 1228 state.move = press 1229 } else { 1230 state.move = press 1231 setTimeout(doMove, 60) 1232 } 1233 return 1234 } 1235 1236 if (ev.type === "pointerup" && truelyPrimary(ev)) { 1237 handled(ev) 1238 doMove() 1239 cleanup() 1240 return 1241 } 1242 } 1243 1244 function enumSelection(input, values, selected) { 1245 let a = typeof(values.keys) === "function" 1246 for (let i in values) { 1247 let opt = input.appendChild(document.createElement("option")) 1248 opt.appendChild(document.createTextNode(a ? values[i] : i)) 1249 } 1250 } 1251 1252 function furnitureMenu(editor, pointOrID) { 1253 let id 1254 if (typeof pointOrID === "string") { 1255 id = pointOrID 1256 } else { 1257 id = newFurniture(editor, pointOrID) 1258 } 1259 1260 let menu = document.createElement("div") 1261 menu.id = "furniture_menu" 1262 menu.classList.add("escapable") 1263 1264 let subs = [] 1265 menu.addEventListener("escape", function() { 1266 editor.finishAction() 1267 menu.remove() 1268 }) 1269 1270 subs.push(menu.appendChild(furnitureTools(editor, id))) 1271 menu.append(document.createElement("hr")) 1272 subs.push(menu.appendChild(furnitureParamsMenu(editor, id))) 1273 1274 let oldMenu = document.getElementById("furniture_menu") 1275 if (oldMenu) { 1276 oldMenu.replaceWith(menu) 1277 } else { 1278 document.body.append(menu) 1279 } 1280 } 1281 1282 function furnitureTools(editor, id) { 1283 let c = document.createElement("div") 1284 1285 c.append(ui.button("Duplicate", "Duplicate furniture parameters into a new piece", null, { 1286 handlers: { click: 1287 function() { 1288 editor.finishAction() 1289 let params = allFurnitureParams(editor, id) 1290 params.x += params.width * .6 1291 params.y -= params.depth * .6 1292 delete params.name 1293 delete params.furniture_id 1294 editor.finishAction() 1295 id = editor.addMappedFurniture(params) 1296 editor.findObj(id).select() 1297 } 1298 } 1299 })) 1300 1301 return c 1302 } 1303 1304 function furnitureParamsMenu(editor, id) { 1305 const styles = function(type) { 1306 let styles = ['default'] 1307 if (editor.backend.params.furniture[type].styles == null) { 1308 return styles 1309 } 1310 return styles.concat(editor.backend.params.furniture[type].styles) 1311 } 1312 1313 editor.finishAction() 1314 1315 let params = allFurnitureParams(editor, id) 1316 delete params.x 1317 delete params.y 1318 1319 let items = [ 1320 menuItem("name", "Name", { attributes: { value: params.name ?? "" } }), 1321 menuItem("type", "Type", { break: false, enum: editor.backend.params.furniture, attributes: { value: params.type, required: true } }), 1322 menuItem("style", "Style"), 1323 menuItem("variety", "Variety"), 1324 menuItem("width", "Width", { attributes: { value: userLength(editor, params.width), required: true } }), 1325 menuItem("depth", "Depth", { attributes: { value: userLength(editor, params.depth), required: true } }), 1326 menuItem("angle", "Angle", { attributes: { value: params.angle ?? 0, min: 0, max: 359, step: 1, type: "range", required: true } }) 1327 ] 1328 let keys = {} 1329 for (let i in items) { 1330 keys[items[i].attributes.name] = i 1331 } 1332 1333 const fromVariety = function(type, variety) { 1334 console.log(`Setting with and depth to ${variety} ${type}`) 1335 if (variety == null) { 1336 return 1337 } 1338 1339 let v = editor.backend.params.furniture[type].varieties[variety] 1340 params.width = v.width 1341 items[keys.width].input.value = userLength(editor, v.width) 1342 params.depth = v.depth 1343 items[keys.depth].input.value = userLength(editor, v.depth) 1344 tryUpdate() 1345 } 1346 const newVariety = function(init) { 1347 let vars = editor.backend.params.furniture[items[keys.type].input.value].varieties 1348 if (vars == undefined) { 1349 items[keys.variety].container.classList.add("hidden") 1350 fromVariety() 1351 return 1352 } 1353 1354 let cnt = 0 1355 for (let k in vars) { 1356 if (++cnt > 1) { 1357 break 1358 } 1359 } 1360 1361 let v 1362 if (cnt === 1) { 1363 v = menuItem("variety", "Variety", { attributes: { type: "button", value: "Reset" } }) 1364 } else { 1365 v = menuItem("variety", "Variety", { enum: vars }) 1366 } 1367 let c = makeItem(v) 1368 items[keys.variety].container.replaceWith(c) 1369 items[keys.variety] = v 1370 updateVariety() 1371 fromVariety(items[keys.type].input.value, init ? null : defKey(vars)) 1372 if (cnt > 1) { 1373 c.addEventListener("input", function(ev) { 1374 fromVariety(items[keys.type].input.value, ev.target.value) 1375 }) 1376 } else { 1377 c.addEventListener("click", function() { 1378 fromVariety(items[keys.type].input.value, defKey(vars)) 1379 updateVariety() 1380 }) 1381 } 1382 } 1383 const newStyle = function() { 1384 let typeStyles = styles(params.type) 1385 if (typeStyles.length === 1) { 1386 items[keys.style].container.classList.add("hidden") 1387 } else { 1388 let s = menuItem("style", "Style", { enum: typeStyles }) 1389 items[keys.style].container.replaceWith(makeItem(s)) 1390 items[keys.style] = s 1391 if (params.style != null) { 1392 items[keys.style].input.value = params.style 1393 } 1394 } 1395 } 1396 const updateVariety = function() { 1397 let vars = editor.backend.params.furniture[params.type].varieties 1398 let cnt = 0 1399 for (let k in vars) { 1400 if (++cnt > 1) { 1401 break 1402 } 1403 } 1404 if (cnt > 1) { 1405 items[keys.variety].input.value = editor.varietyFrom(params) 1406 } else if (cnt === 1) { 1407 if (editor.varietyFrom(params)) { 1408 items[keys.variety].input.setAttribute("disabled", true) 1409 } else { 1410 items[keys.variety].input.removeAttribute("disabled") 1411 } 1412 } 1413 } 1414 const tryUpdate = function() { 1415 let err = menu.querySelector(".error") 1416 if (err) { 1417 err.remove() 1418 } 1419 for (let i in items) { 1420 // If invalid, don't even try 1421 if (!items[i].input.validity.valid) { 1422 return 1423 } 1424 } 1425 editor.addMappedFurniture(params, id) 1426 } 1427 1428 let menu = makeMenu(items) 1429 menu.classList.add("furniture_params_menu") 1430 items[keys.type].input.value = params.type 1431 newVariety(true) 1432 newStyle() 1433 menu.addEventListener("input", function(ev) { 1434 handled(ev) 1435 try { 1436 console.debug("furnitureMenu.input(ev)", ev.target.name, ev.target.value) 1437 if (ev.target.name === "width" || ev.target.name === "depth") { 1438 let u = unitInput(editor, ev.target) 1439 if (u == undefined) { 1440 return 1441 } 1442 if (u <= 0) { 1443 ev.target.setCustomValidity(ui.capitalize(ev.target.name) + " must be greater than zero") 1444 } else { 1445 ev.target.setCustomValidity("") 1446 } 1447 ev.target.reportValidity() 1448 params[ev.target.name] = u 1449 updateVariety() 1450 } else { 1451 if (ev.target.name === "style" && ev.target.value === "default") { 1452 params[ev.target.name] = null 1453 } else { 1454 if (ev.target.name === "angle") { 1455 let a 1456 if (ev.target.value.length === 0) { 1457 a = 0 1458 } else if (!State.furnRotationSnap) { 1459 a = ev.target.value 1460 } else { 1461 const snapOn = 45 1462 const snapAt = 15 1463 a = ev.target.value 1464 let d = (a % snapOn) 1465 if (d < snapAt) { 1466 a -= d 1467 } else if (d > (snapOn - snapAt)) { 1468 a -= d - snapOn 1469 } 1470 a %= 360 1471 } 1472 params[ev.target.name] = a 1473 } else { 1474 params[ev.target.name] = ev.target.value.length === 0 ? null : ev.target.value 1475 } 1476 } 1477 if (ev.target.name === "type") { 1478 newVariety() 1479 newStyle() 1480 } 1481 } 1482 tryUpdate() 1483 } 1484 catch(err) { 1485 etc.error(err, menu) 1486 throw err 1487 } 1488 }) 1489 1490 return menu 1491 } 1492 1493 function allFurnitureParams(editor, id) { 1494 let params = structuredClone(editor.backend.reqObj(id)) 1495 let fp = editor.backend.reqObj(params.furniture_id) 1496 for (let k in fp) { 1497 params[k] = fp[k] 1498 } 1499 return params 1500 } 1501 1502 function newFurniture(editor, point) { 1503 if (point == null) { 1504 point = { x: 0, y: 0 } 1505 } 1506 1507 let type = "any" 1508 let vars = editor.backend.params.furniture[type].varieties 1509 let v 1510 if (def(vars)) { 1511 v = def(vars) 1512 } else { 1513 let s = editor.units.get("inch", 32) 1514 v = { width: s, depth: s } 1515 } 1516 let params = { 1517 x: point.x, 1518 y: point.y, 1519 type, 1520 width: v.width, 1521 depth: v.depth, 1522 name: null 1523 1524 } 1525 let id = editor.addMappedFurniture(params) 1526 1527 editor.finishAction() 1528 editor.findObj(id).select() 1529 1530 return id 1531 } 1532 1533 function makeMenu(items) { 1534 let c = document.createElement("form") 1535 1536 // In case I make c != form later 1537 let form = c 1538 1539 for (let i in items) { 1540 form.append(makeItem(items[i])) 1541 if (items[i].break) { 1542 form.appendChild(document.createElement("br")) 1543 } 1544 } 1545 1546 return c 1547 } 1548 1549 function makeItem(item) { 1550 if (item.label != null) { 1551 item.container = document.createElement("label") 1552 let label = item.container 1553 label.appendChild(document.createTextNode(item.label + ": ")) 1554 } 1555 1556 item.input = document.createElement(item.enum ? "select" : "input") 1557 if (!item.container) { 1558 item.container = item.input 1559 } else { 1560 item.container.appendChild(item.input) 1561 } 1562 1563 if (item.enum != null) { 1564 enumSelection(item.input, item.enum) 1565 } 1566 for (let a in item.attributes ?? {}) { 1567 item.input.setAttribute(a, item.attributes[a]) 1568 } 1569 for (let c in item.classes ?? {}) { 1570 item.container.classList.add(item.classes[c]) 1571 } 1572 /* 1573 if (item.break) { 1574 // NOTE: Want this to be done in CSS instead of with <br>s 1575 item.container.classList.add("break") 1576 } 1577 */ 1578 return item.container 1579 } 1580 1581 function menuItem(name, label, options) { 1582 options = options ?? {} 1583 let attributes = options.attributes ?? {} 1584 1585 if (options.enum != null && typeof options.enum != "object") { 1586 throw new Error("Expected object for menuItem enum") 1587 } 1588 if (name == undefined) { 1589 throw new Error("Must have name") 1590 } 1591 1592 attributes.name = name 1593 if (options.value != undefined) { 1594 attributes.value = options.value 1595 } 1596 if (attributes.title == undefined && label != undefined) { 1597 attributes.title = label 1598 } 1599 1600 return { 1601 label, 1602 attributes, 1603 enum: options.enum, 1604 break: options.break == null ? true : options.break 1605 } 1606 } 1607 1608 function unitInput(editor, input, value) { 1609 if (value != null) { 1610 input.value = userLength(editor, value) 1611 return 1612 } 1613 1614 try { 1615 return parseUserLength(editor, input.value) 1616 } 1617 catch(err) { 1618 input.setCustomValidity(err) 1619 input.reportValidity() 1620 console.warn(err) 1621 } 1622 } 1623 1624 1625 function parseUserLength(editor, length) { 1626 let a = length 1627 .replaceAll(" ", "") 1628 .replaceAll("‘", "'") 1629 .replaceAll("’", "'") 1630 .replaceAll("“", '"') 1631 .replaceAll("”", '"') 1632 .split(/([0-9.]+)/) 1633 1634 let amount 1635 let rebuilt = [] 1636 for (let i in a) { 1637 if (a[i].length === 0) { 1638 ; 1639 } else if (!amount) { 1640 amount = Number(a[i]) 1641 if (amount === NaN) { 1642 throw new Error("Invalid number") 1643 } 1644 } else { 1645 if (!editor.units.symbols[a[i]]) { 1646 throw new Error("Invalid user length") 1647 } 1648 rebuilt.push({ symbol: a[i], amount: amount }) 1649 amount = null 1650 } 1651 } 1652 if (amount) { 1653 rebuilt.push({ unit: editor.unit, amount: amount }) 1654 } 1655 1656 return editor.units.combine(rebuilt) 1657 } 1658 1659 function userLength(editor, units) { 1660 let a = editor.units.separate(units, editor.unitSystem, { whole: true }) 1661 let words = [] 1662 for (let i in a) { 1663 if (!a[i].unit) { 1664 // We don't allow anything smaller than smallest defined unit, 1665 // though maybe this should be an error condition 1666 continue 1667 } 1668 1669 words.push(a[i].amount + (a[i].symbol ?? a[i].name ?? "")) 1670 } 1671 return words.join(" ") 1672 } 1673 1674 // I suppose this is why math is important... 1675 // and the internet: 1676 // <https://stackoverflow.com/questions/42510144/calculate-coordinates-for-45-degree-snap> 1677 // UPDATE: Probably find an easy way using threejs-math now 1678 function snap(point, on, directions) { 1679 let factor = (directions ?? 4) / 2 1680 let dx = point.x - on.x 1681 let dy = point.y - on.y 1682 let dist = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) 1683 let angle = Math.atan2(dy, dx) 1684 angle = Math.round(angle / Math.PI * factor) / factor * Math.PI 1685 return new Vector2( 1686 on.x + dist * Math.cos(angle), 1687 on.y + dist * Math.sin(angle) 1688 ) 1689 } 1690 1691 function preventDefaultHandler(event) { 1692 handled(event) 1693 } 1694 1695 function singlePointerHandler(ev, editor, state) { 1696 if (ev.type === "pointerdown") { 1697 state[ev.pointerId] = true 1698 } else if (ev.type === "pointerup" || ev.type === "pointercancel") { 1699 delete state[ev.pointerId] 1700 } 1701 1702 // Send all events but pointerdown on to the other handlers 1703 if (ev.type !== "pointerdown" && ev.type !== "pointermove") { 1704 return 1705 } 1706 1707 let cnt = 0 1708 for (let k in state) { 1709 if (++cnt > 1) { 1710 editor.draw.fire(cancelEvent); 1711 handled(ev) 1712 return 1713 } 1714 } 1715 } 1716 1717 function notify(message, id) { 1718 console.log("Notify", message) 1719 1720 let e = document.createElement("aside") 1721 e.id = id 1722 e.classList.add("message") 1723 e.textContent = message 1724 1725 let old 1726 if (id) { 1727 old = document.getElementById(id) 1728 } 1729 1730 if (old) { 1731 old.replaceWith(e) 1732 } else { 1733 document.body.prepend(e) 1734 } 1735 setTimeout(function() { e.remove() }, messageTimeout) 1736 } 1737 1738 function item(node) { 1739 let i = document.createElement("li") 1740 i.append(node) 1741 return i 1742 } 1743 1744 function elapsed(since) { 1745 return Date.now() - since 1746 } 1747 1748 function handled(event) { 1749 event.stopImmediatePropagation() 1750 event.preventDefault() 1751 } 1752 1753 function escape() { 1754 document.body.querySelectorAll(".escapable").forEach(function(e) { 1755 console.debug("Escape", e) 1756 e.dispatchEvent(escapeEvent) 1757 }) 1758 } 1759 1760 function primaryPointer(ev) { 1761 if (ev.type === "pointermove") { 1762 return primaryMove(ev) 1763 } else { 1764 return truelyPrimary(ev) 1765 } 1766 } 1767 1768 function truelyPrimary(ev) { 1769 if (ev.pointerType === "mouse") { 1770 return ev.button === buttons.left 1771 } 1772 return ev.isPrimary 1773 } 1774 1775 function primaryMove(ev) { 1776 if (ev.pointerType === "mouse") { 1777 return true 1778 } 1779 return ev.isPrimary 1780 } 1781 1782 function precision(a) { 1783 if (!isFinite(a)) { 1784 return 0; 1785 } 1786 let e = 1 1787 let p = 0 1788 while (Math.round(a * e) / e !== a) { 1789 e *= 10 1790 p++ 1791 } 1792 return p 1793 } 1794 1795 function nextSwing(swing) { 1796 let next 1797 if (!swing) { 1798 next = 'a-' 1799 } else if (swing[1] === '-') { 1800 next = (swing[0] === 'a' ? 'b' : 'a') + '+' 1801 } else { 1802 next = swing[0] + '-' 1803 } 1804 console.debug("nextSwing", `${swing} -> ${next}`) 1805 return next 1806 } 1807 1808 function prevSwing(swing) { 1809 let prev 1810 if (!swing) { 1811 prev = 'a-' 1812 } else if (swing[1] === '+') { 1813 prev = (swing[0] === 'a' ? 'b' : 'a') + '-' 1814 } else { 1815 prev = swing[0] + '+' 1816 } 1817 console.debug("prevSwing", `${swing} -> ${prev}`) 1818 return prev 1819 } 1820 1821 function def(obj) { 1822 return obj[defKey(obj)] 1823 } 1824 1825 function defKey(obj) { 1826 for (let i in obj) { 1827 return i 1828 } 1829 } 1830 1831 function status(msg) { 1832 let s = document.getElementById("status") 1833 s.classList[msg instanceof Error ? "add" : "remove"]("error") 1834 s.textContent = msg ?? "" 1835 } 1836 1837 function fetchError(err) { 1838 if (err instanceof api.FetchError) { 1839 status("Network error: " + err) 1840 } else { 1841 status(err) 1842 } 1843 } 1844 1845 function toolGroup(name) { 1846 let g = document.createElement("div") 1847 g.classList.add("toolgroup") 1848 let n = g.appendChild(document.createElement("button")) 1849 n.textContent = name 1850 g.append(document.createElement("ul")) 1851 return g 1852 }