/* isokit-view.js: browser shell for isokit scenes (global IsoKit) */ /* * Plain ES2019, no dependencies. The Rust engine renders SVG; this file shows * it: levels, four rotations, drag pan, pinch / wheel zoom, tag declutter, * tooltips, selection, full screen. Port of the CNXIso / Mooban mount(). * * var view = IsoKit.mount(el, source, { onPick: function (info) {} }); * source = IsoKit.staticSource(data) // prerendered views (isokit build) * | IsoKit.httpSource("", meta) // the Studio / dev server * source.meta = { title, note, levels: [{id, label, title}], defaultLevel, defaultRotation } * source.render(level, rot, cfg) -> view data or a Promise of it: * { html, info, bbox: [x, y, w, h], focus: [x, y], north, boxes, warnings } * * view.update(cfg) view.refresh() view.setLevel(id) view.setRotation(r) view.rotate(d) * view.select(id) view.selectObj(id) view.focus(id | [x, y, z], zoom) view.fit() * view.toWorld(clientX, clientY, z) -> [x, y] view.ready() -> Promise * view.info(id) view.stats() view.fullscreen(on) view.destroy() */ (function (root) { "use strict"; var VERSION = "0.1.0"; var C = 4, CZ = 4; function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function r1(v) { return Math.round(v * 10) / 10; } function rotOf(r) { return ((r | 0) % 4 + 4) % 4; } // view coordinates -> world plan (inverse of the engine's Proj.r) function rInv(rot, vx, vy) { switch (rot) { case 0: return [vx, -vy]; case 1: return [-vy, -vx]; case 2: return [-vx, vy]; default: return [vy, vx]; } } function rFwd(rot, x, y) { switch (rot) { case 0: return [x, -y]; case 1: return [-y, -x]; case 2: return [-x, y]; default: return [y, x]; } } function project(rot, x, y, z) { var v = rFwd(rot, x, y); return [(v[0] - v[1]) * C, (v[0] + v[1]) * C / 2 - (z || 0) * CZ]; } function unproject(rot, sx, sy, z) { var a = sx / C, b = (sy + (z || 0) * CZ) * 2 / C; return rInv(rot, (a + b) / 2, (b - a) / 2); } function isThen(x) { return x && typeof x.then === "function"; } // ------------------------------------------------------------ sources function staticSource(data) { return { meta: data.meta, render: function (level, rot) { var v = data.views[level + "/" + rot]; return v || { error: { message: "view " + level + "/" + rot + " was not built" } }; } }; } function httpSource(base, meta) { base = base || ""; return { meta: meta, dev: true, render: function (level, rot, cfg) { var q = "level=" + encodeURIComponent(level) + "&rot=" + rot + "&dev=1" + (cfg ? "&cfg=" + encodeURIComponent(JSON.stringify(cfg)) : ""); return fetch(base + "/api/render?" + q, { cache: "no-store" }).then(function (r) { return r.json(); }); } }; } // ------------------------------------------------------------ mount var UID = 0; function mount(el, source, opts) { if (!el) throw new Error("IsoKit.mount: no container"); opts = opts || {}; var meta = source.meta || {}; var levels = meta.levels && meta.levels.length ? meta.levels : [{ id: "site", label: "Site" }]; var st = { cfg: Object.assign({}, opts.cfg || {}), level: opts.level || meta.defaultLevel || levels[0].id, rot: rotOf(opts.rotation === undefined ? (meta.defaultRotation === undefined ? 1 : meta.defaultRotation) : opts.rotation), sel: null, selObj: null, uid: ++UID }; var wrap = document.createElement("div"); wrap.className = "iso-wrap"; var segs = levels.length > 1 ? '' + levels.map(function (l) { return '"; }).join("") + "" : ""; wrap.innerHTML = '
' + '" + '
' + (opts.note === false || !meta.note ? "" : '

' + esc(meta.note) + "

"); el.appendChild(wrap); var stage = wrap.querySelector(".iso-stage"), svg = wrap.querySelector(".iso-svg"), tip = wrap.querySelector(".iso-tip"), bar = wrap.querySelector(".iso-bar"), errBox = wrap.querySelector(".iso-err"); var scn = null, vb = null, fitW = 1, atFit = true, metas = [], declT = 0, alive = true, lastSize = [0, 0], seq = 0, waiting = []; var touched = false, first = true; // ---------------------------------------------------------- full screen var fsEl = opts.fullscreen && opts.fullscreen.nodeType === 1 ? opts.fullscreen : wrap, fsBtn = bar.querySelector('[data-a="full"]'); function fsNative() { return (document.fullscreenElement || document.webkitFullscreenElement) === fsEl; } function isFull() { return fsNative() || fsEl.classList.contains("iso-fs-fake"); } function fsSync() { var on = isFull(); fsEl.classList.toggle("iso-fs", on); if (fsBtn) { fsBtn.setAttribute("aria-pressed", String(on)); fsBtn.title = on ? "Leave full screen (Esc)" : "Full screen (F)"; } call(opts.onFullscreen, on); } function fsFake(on) { fsEl.classList.toggle("iso-fs-fake", on); document.documentElement.classList.toggle("iso-fs-lock", on); fsSync(); } function setFull(on) { if (on === isFull()) return; if (!on) { if (fsEl.classList.contains("iso-fs-fake")) fsFake(false); else { var ex = document.exitFullscreen || document.webkitExitFullscreen; if (ex) ex.call(document); } return; } var req = fsEl.requestFullscreen || fsEl.webkitRequestFullscreen; if (!req) { fsFake(true); return; } try { var p = req.call(fsEl); if (p && p.catch) p.catch(function () { fsFake(true); }); } catch (e) { fsFake(true); } } function onFsKey(e) { if (e.key === "Escape" && fsEl.classList.contains("iso-fs-fake")) { fsFake(false); svg.focus(); } } document.addEventListener("fullscreenchange", fsSync); document.addEventListener("webkitfullscreenchange", fsSync); document.addEventListener("keydown", onFsKey); function call(fn) { if (typeof fn !== "function") return undefined; try { return fn.apply(null, Array.prototype.slice.call(arguments, 1)); } catch (e) { if (window.console) console.error(e); } return undefined; } // ---------------------------------------------------------- build function size() { var r = stage.getBoundingClientRect(); return [Math.max(1, r.width), Math.max(1, r.height)]; } function build() { var my = ++seq; wrap.classList.add("iso-loading"); var r; try { r = source.render(st.level, st.rot, st.cfg); } catch (e) { r = { error: { message: String(e) } }; } if (isThen(r)) { r.then(function (d) { if (my === seq) applyScene(d); }, function (e) { if (my === seq) applyScene({ error: { message: String(e) } }); }); } else applyScene(r); } function applyScene(d) { if (!alive) return; wrap.classList.remove("iso-loading"); if (!d || d.error) { var e = (d && d.error) || { message: "no data" }; errBox.innerHTML = "Scene error" + esc((e.file ? e.file + (e.line ? ":" + e.line : "") + ": " : "") + e.message) + "" + (e.trace && e.trace.length ? "" + e.trace.map(esc).join("
") + "
" : ""); errBox.hidden = false; call(opts.onRender, d); flush(); return; } errBox.hidden = true; var hadScene = !!scn; scn = d; svg.innerHTML = d.html; wrap.classList.toggle("iso-still", st.cfg.running === false); metas = Array.prototype.map.call(svg.querySelectorAll(".iso-tag"), function (g, i) { return { el: g, p: +g.getAttribute("data-p"), w: +g.getAttribute("data-w"), x: +g.getAttribute("data-x"), y: +g.getAttribute("data-y"), i: i, hid: false }; }).sort(function (a, b) { return b.p - a.p || a.i - b.i; }); Array.prototype.forEach.call(bar.querySelectorAll(".iso-seg button"), function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-level") === st.level)); }); wrap.querySelector(".iso-needle").setAttribute("transform", "rotate(" + (d.north || 0).toFixed(1) + ")"); if (st.sel) select(st.sel, true); if (st.selObj) selectObj(st.selObj); if (first || !hadScene) { first = false; lastSize = size(); focusDefault(); } else if (pendingFrame) { var f = pendingFrame; pendingFrame = null; f(); } else { var wasFit = atFit; computeFit(); if (wasFit) fit(); else { clampVB(); apply(); } } call(opts.onRender, d); flush(); } var pendingFrame = null; function flush() { var w = waiting; waiting = []; w.forEach(function (f) { f(); }); } function computeFit() { var b = scn.bbox, sz = size(), ar = sz[0] / sz[1], w = b[2] * 1.02, h = b[3] * 1.04; if (w / h > ar) h = w / ar; else w = h * ar; fitW = w; return { x: b[0] + b[2] / 2 - w / 2, y: b[1] + b[3] / 2 - h / 2, w: w, h: h }; } function apply() { if (!vb) return; svg.setAttribute("viewBox", r1(vb.x) + " " + r1(vb.y) + " " + r1(vb.w) + " " + r1(vb.h)); var sz = size(); svg.style.setProperty("--iso-k", (vb.w / sz[0]).toFixed(4)); atFit = vb.w >= fitW * 0.995; svg.style.touchAction = atFit ? "pan-y" : "none"; wrap.classList.toggle("iso-zoomed", !atFit); schedule(); } function fit() { if (!scn) return; vb = computeFit(); apply(); touched = false; } function clampVB() { var b = scn.bbox, cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2; cx = Math.max(b[0], Math.min(b[0] + b[2], cx)); cy = Math.max(b[1], Math.min(b[1] + b[3], cy)); vb.x = cx - vb.w / 2; vb.y = cy - vb.h / 2; } function zoomAt(f, px, py) { if (!vb) return; touched = true; var sz = size(), nw = Math.max(fitW / 20, Math.min(fitW, vb.w / f)), nh = nw * sz[1] / sz[0]; if (px === undefined) { px = 0.5; py = 0.5; } var ux = vb.x + px * vb.w, uy = vb.y + py * vb.h; vb = { x: ux - px * nw, y: uy - py * nh, w: nw, h: nh }; if (nw >= fitW * 0.999) vb = computeFit(); else clampVB(); apply(); } function zoomClient(f, cx, cy) { var r = stage.getBoundingClientRect(); zoomAt(f, (cx - r.left) / r.width, (cy - r.top) / r.height); } function panPx(dx, dy) { if (!vb) return; touched = true; var sz = size(), k = vb.w / sz[0]; vb.x -= dx * k; vb.y -= dy * k; clampVB(); apply(); } function focusDefault() { fit(); var sz = size(); if (sz[0] < 640 && scn && !isFull()) { var nw = fitW / 2.4, nh = nw * sz[1] / sz[0]; vb = { x: scn.focus[0] - nw / 2, y: scn.focus[1] - nh / 2, w: nw, h: nh }; clampVB(); apply(); } touched = false; } function schedule() { if (declT) return; declT = (window.requestAnimationFrame || setTimeout)(function () { declT = 0; declutter(); }); } function declutter() { if (!alive || !vb) return; var sz = size(), k = vb.w / sz[0], kept = [], i, j, t, x, y, r, vis; var bt = bar.offsetTop + bar.offsetHeight + 2, bw = bar.offsetLeft + bar.offsetWidth; for (i = 0; i < metas.length; i++) { t = metas[i]; x = (t.x - vb.x) / k; y = (t.y - vb.y) / k; r = [x - t.w / 2 - 2, y - 23, x + t.w / 2 + 2, y - 4]; vis = r[0] >= 2 && r[2] <= sz[0] - 2 && r[1] >= 2 && r[3] <= sz[1] - 2 && !(r[1] < bt && r[0] < bw); for (j = 0; vis && j < kept.length; j++) if (!(r[2] < kept[j][0] || r[0] > kept[j][2] || r[3] < kept[j][1] || r[1] > kept[j][3])) vis = false; if (vis) kept.push(r); if (t.hid === vis) { t.hid = !vis; t.el.classList.toggle("iso-hide", !vis); } } } // ---------------------------------------------------------- picking function infoOf(id) { return (scn && scn.info && scn.info[id]) || { id: id, name: id, detail: "" }; } function showTip(info, cx, cy) { var rows = info.rows && info.rows.length ? "" + info.rows.map(function (r) { return ""; }).join("") + "
" + esc(r[0]) + "" + esc(r[1]) + "
" : ""; tip.innerHTML = "" + esc(info.name) + "" + (info.detail ? "" + esc(info.detail) + "" : "") + rows; tip.hidden = false; var r = stage.getBoundingClientRect(), tw = tip.offsetWidth, th = tip.offsetHeight, x = cx - r.left + 14, y = cy - r.top + 14; if (x + tw > r.width - 6) x = Math.max(6, cx - r.left - tw - 14); if (y + th > r.height - 6) y = Math.max(6, cy - r.top - th - 14); tip.style.left = x + "px"; tip.style.top = y + "px"; } function hideTip() { tip.hidden = true; } function q(attr, id) { return svg.querySelectorAll("[" + attr + '="' + String(id).replace(/["\\]/g, "") + '"]'); } function select(id, quiet) { Array.prototype.forEach.call(svg.querySelectorAll(".iso-sel"), function (e) { e.classList.remove("iso-sel"); }); st.sel = id || null; if (st.selObj) selectObj(st.selObj); if (!id) return; Array.prototype.forEach.call(q("data-id", id), function (e) { if (!e.classList.contains("iso-tag")) e.classList.add("iso-sel"); }); } function selectObj(id) { Array.prototype.forEach.call(svg.querySelectorAll(".iso-osel"), function (e) { e.classList.remove("iso-osel"); }); st.selObj = id || null; if (id) Array.prototype.forEach.call(q("data-obj", id), function (e) { e.classList.add("iso-osel"); }); } function targetId(t) { var e = t && t.closest ? t.closest("[data-id]") : null; return e && svg.contains(e) ? e.getAttribute("data-id") : null; } function targetObj(t) { var e = t && t.closest ? t.closest("[data-obj]") : null; return e && svg.contains(e) ? e.getAttribute("data-obj") : null; } function targetSrc(t) { var e = t && t.closest ? t.closest("[data-src]") : null; return e && svg.contains(e) ? e.getAttribute("data-src") : null; } function pick(t, cx, cy) { var id = targetId(t), obj = targetObj(t); if (!id && !obj) { select(null); hideTip(); call(opts.onPick, null); return; } var info = infoOf(id || obj); if (!opts.noSelect) select(id); if (!opts.noTip) showTip(info, cx, cy); call(opts.onPick, { id: info.id || id, name: info.name, detail: info.detail, rows: info.rows || [], obj: obj, src: targetSrc(t) }); } // client point -> world plan at height z function toWorld(cx, cy, z) { if (!vb) return null; var r = stage.getBoundingClientRect(), sx = vb.x + (cx - r.left) / r.width * vb.w, sy = vb.y + (cy - r.top) / r.height * vb.h; return unproject(st.rot, sx, sy, z || 0); } function screenPx() { var sz = size(); return vb ? vb.w / sz[0] : 1; } // ---------------------------------------------------------- pointer var ptrs = {}, down = null, moved = false, pinch = null, hoverId = null, claimed = false; function npt() { return Object.keys(ptrs).length; } svg.addEventListener("pointerdown", function (e) { ptrs[e.pointerId] = { x: e.clientX, y: e.clientY }; if (npt() === 1) { down = { x: e.clientX, y: e.clientY, id: e.pointerId, t: e.target }; moved = false; claimed = !!(opts.interact && call(opts.interact.down, e, { obj: targetObj(e.target), id: targetId(e.target), world: toWorld(e.clientX, e.clientY, 0) })); if (claimed) { try { svg.setPointerCapture(e.pointerId); } catch (er) { /* ignore */ } } } if (npt() === 2) { if (claimed) { call(opts.interact.cancel); claimed = false; } var k = Object.keys(ptrs), a = ptrs[k[0]], b = ptrs[k[1]]; pinch = { d: Math.hypot(a.x - b.x, a.y - b.y) || 1 }; moved = true; } }); svg.addEventListener("pointermove", function (e) { if (opts.onHover) call(opts.onHover, toWorld(e.clientX, e.clientY, 0), e); var p = ptrs[e.pointerId]; if (p) { var dx = e.clientX - p.x, dy = e.clientY - p.y; p.x = e.clientX; p.y = e.clientY; if (pinch && npt() >= 2) { var k = Object.keys(ptrs), a = ptrs[k[0]], b = ptrs[k[1]], d = Math.hypot(a.x - b.x, a.y - b.y) || 1; zoomClient(d / pinch.d, (a.x + b.x) / 2, (a.y + b.y) / 2); pinch.d = d; return; } if (down && !moved && Math.hypot(e.clientX - down.x, e.clientY - down.y) > 4) { moved = true; hideTip(); wrap.classList.add("iso-dragging"); if (!claimed) { try { svg.setPointerCapture(e.pointerId); } catch (er) { /* ignore */ } } } if (moved) { if (claimed) call(opts.interact.move, e, { world: toWorld(e.clientX, e.clientY, 0), dx: e.clientX - down.x, dy: e.clientY - down.y, k: screenPx() }); else panPx(dx, dy); e.preventDefault(); } return; } if (e.pointerType === "mouse" && !opts.noTip) { var id = targetId(e.target); if (id) { if (id !== hoverId) { if (hoverId) setHover(hoverId, false); setHover(id, true); hoverId = id; } showTip(infoOf(id), e.clientX, e.clientY); } else if (hoverId) { setHover(hoverId, false); hoverId = null; if (!st.sel) hideTip(); } } }); function setHover(id, on) { Array.prototype.forEach.call(q("data-id", id), function (e) { if (!e.classList.contains("iso-tag")) e.classList.toggle("iso-hov", on); }); } function up(e) { var wasDown = down && down.id === e.pointerId; delete ptrs[e.pointerId]; if (npt() < 2) pinch = null; if (wasDown) { if (claimed) call(opts.interact.up, e, { moved: moved, world: toWorld(e.clientX, e.clientY, 0), cancel: e.type !== "pointerup" }); else if (!moved && e.type === "pointerup") pick(e.target, e.clientX, e.clientY); } if (npt() === 0) { down = null; claimed = false; wrap.classList.remove("iso-dragging"); } } svg.addEventListener("pointerup", up); svg.addEventListener("pointercancel", up); svg.addEventListener("pointerleave", function (e) { if (e.pointerType === "mouse" && hoverId) { setHover(hoverId, false); hoverId = null; if (!st.sel) hideTip(); } }); svg.addEventListener("wheel", function (e) { if (!(e.ctrlKey || e.metaKey || opts.wheelZoom)) return; e.preventDefault(); zoomClient(Math.exp(-e.deltaY * 0.004), e.clientX, e.clientY); }, { passive: false }); svg.addEventListener("dblclick", function (e) { e.preventDefault(); zoomClient(1.8, e.clientX, e.clientY); }); svg.addEventListener("keydown", function (e) { if (opts.onKey && call(opts.onKey, e)) { e.preventDefault(); return; } var k = e.key; if (k === "+" || k === "=") zoomAt(1.4); else if (k === "-") zoomAt(1 / 1.4); else if (k === "0") fit(); else if (k === "ArrowLeft") panPx(60, 0); else if (k === "ArrowRight") panPx(-60, 0); else if (k === "ArrowUp") panPx(0, 60); else if (k === "ArrowDown") panPx(0, -60); else if (k === "Escape") { select(null); hideTip(); } else if ((k === "f" || k === "F") && fsBtn && !e.ctrlKey && !e.metaKey && !e.altKey) setFull(!isFull()); else return; e.preventDefault(); }); bar.addEventListener("click", function (e) { var b = e.target.closest ? e.target.closest("button") : null; if (!b) return; var a = b.getAttribute("data-a"); if (a === "level") setLevel(b.getAttribute("data-level")); else if (a === "rotl") rotate(-1); else if (a === "rotr") rotate(1); else if (a === "zin") zoomAt(1.5); else if (a === "zout") zoomAt(1 / 1.5); else if (a === "fit") fit(); else if (a === "full") setFull(!isFull()); }); function setLevel(l) { if (!l || l === st.level || !levels.some(function (x) { return x.id === l; })) return; st.level = l; hideTip(); build(); call(opts.onLevel, l); } function rotate(d) { var keepZoom = vb ? vb.w / fitW : 1; st.rot = rotOf(st.rot + (d || 1)); hideTip(); pendingFrame = function () { if (keepZoom >= 0.995) { fit(); return; } computeFit(); var sz = size(), nw = fitW * keepZoom, nh = nw * sz[1] / sz[0], c = scn.focus; if (st.selObj && scn.boxes && scn.boxes[st.selObj]) { var b = scn.boxes[st.selObj]; c = project(st.rot, (b[0] + b[3]) / 2, (b[1] + b[4]) / 2, (b[2] + b[5]) / 2); } vb = { x: c[0] - nw / 2, y: c[1] - nh / 2, w: nw, h: nh }; clampVB(); apply(); }; build(); call(opts.onRotate, st.rot); } var ro = null, io = null; function onResize() { if (!scn) return; var sz = size(); if (Math.abs(sz[0] - lastSize[0]) < 0.5 && Math.abs(sz[1] - lastSize[1]) < 0.5) return; var old = lastSize; lastSize = sz; if (!vb || !old[0] || !touched) { focusDefault(); return; } var wasFit = atFit, k = vb.w / old[0], cx = vb.x + vb.w / 2, cy = vb.y + vb.h / 2; computeFit(); if (wasFit) { fit(); return; } var nw = Math.min(fitW, k * sz[0]), nh = nw * sz[1] / sz[0]; vb = { x: cx - nw / 2, y: cy - nh / 2, w: nw, h: nh }; clampVB(); apply(); } if (window.ResizeObserver) { ro = new ResizeObserver(onResize); ro.observe(stage); } else window.addEventListener("resize", onResize); if (window.IntersectionObserver) { io = new IntersectionObserver(function (es) { es.forEach(function (en) { wrap.classList.toggle("iso-off", !en.isIntersecting); }); }); io.observe(stage); } build(); var view = { el: wrap, svg: svg, stage: stage, source: source, update: function (c) { st.cfg = Object.assign({}, st.cfg, c || {}); build(); return view; }, refresh: function () { build(); return view; }, setSource: function (s) { source = s; build(); return view; }, setLevel: function (l) { setLevel(l); return view; }, getLevel: function () { return st.level; }, rotate: function (d) { rotate(d); return view; }, setRotation: function (r) { var d = rotOf(r - st.rot); if (d) rotate(d); return view; }, getRotation: function () { return st.rot; }, select: function (id) { select(id); return view; }, selectObj: function (id) { selectObj(id); return view; }, // centre on an object id / data-id or a world point [x, y, z] at a zoom relative to fit focus: function (target, f) { if (!scn) return view; var c = null; if (Array.isArray(target)) c = project(st.rot, target[0], target[1], target[2] || 0); else if (target && scn.boxes && scn.boxes[target]) { var b = scn.boxes[target]; c = project(st.rot, (b[0] + b[3]) / 2, (b[1] + b[4]) / 2, (b[2] + b[5]) / 2); } else if (target) { var bx = [1e9, 1e9, -1e9, -1e9]; Array.prototype.forEach.call(q("data-id", target), function (e) { if (e.classList.contains("iso-tag")) return; try { var b = e.getBBox(); if (b.width || b.height) { bx[0] = Math.min(bx[0], b.x); bx[1] = Math.min(bx[1], b.y); bx[2] = Math.max(bx[2], b.x + b.width); bx[3] = Math.max(bx[3], b.y + b.height); } } catch (er) { /* not rendered */ } }); if (bx[0] < 1e9) c = [(bx[0] + bx[2]) / 2, (bx[1] + bx[3]) / 2]; } if (!c) return view; computeFit(); var sz = size(), nw = fitW / Math.max(1, f || 3), nh = nw * sz[1] / sz[0]; vb = { x: c[0] - nw / 2, y: c[1] - nh / 2, w: nw, h: nh }; clampVB(); apply(); touched = true; return view; }, zoom: function (f) { zoomAt(f || 1.5); return view; }, fit: function () { fit(); return view; }, fullscreen: function (on) { setFull(on === undefined ? !isFull() : !!on); return view; }, isFullscreen: function () { return isFull(); }, toWorld: toWorld, screenOf: function (x, y, z) { return project(st.rot, x, y, z); }, screenPx: screenPx, scene: function () { return scn; }, ready: function () { return new Promise(function (res) { if (!wrap.classList.contains("iso-loading") && scn) res(view); else waiting.push(function () { res(view); }); }); }, info: function (id) { return id ? infoOf(id) : Object.keys((scn && scn.info) || {}).map(infoOf); }, config: function () { return Object.assign({}, st.cfg); }, stats: function () { return { nodes: svg.querySelectorAll("*").length, sprites: scn ? scn.sprites : 0, level: st.level, rotation: st.rot, ms: scn ? scn.ms : 0 }; }, destroy: function () { alive = false; if (ro) ro.disconnect(); else window.removeEventListener("resize", onResize); if (isFull()) setFull(false); document.removeEventListener("fullscreenchange", fsSync); document.removeEventListener("webkitfullscreenchange", fsSync); document.removeEventListener("keydown", onFsKey); if (io) io.disconnect(); if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } }; return view; } root.IsoKit = { mount: mount, staticSource: staticSource, httpSource: httpSource, project: project, unproject: unproject, version: VERSION }; })(typeof window !== "undefined" ? window : this);