janusweb 1.7.2 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,17 @@
1
1
  <h3>Inventory</h3>
2
- <collection-jsonapi id="models"
3
- host="https://poly.googleapis.com"
4
- endpoint="/v1/assets"
5
- itempath="assets"
6
- apiargs.keywords=""
7
- apiargs.format="gltf2"
8
- apiargs.key="AIzaSyC9ngd4hqQ9YmaBpUAQP4R9KJEY3dIZ4hM">
9
- </collection-jsonapi>
10
-
11
- <ui-tabs>
12
- <ui-tab label="Primitives">
13
- <ui-grid name="primitives" class="models" draggable="true" selectable="true">
14
- <ui-item><a href="janus:object/id=cube">cube</a></ui-item>
15
- <ui-item><a href="janus:object/id=sphere">sphere</a></ui-item>
16
- <ui-item><a href="janus:object/id=cone">cone</a></ui-item>
17
- <ui-item><a href="janus:object/id=cylinder">cylinder</a></ui-item>
18
- <ui-item><a href="janus:object/id=pyramid">pyramid</a></ui-item>
19
- <ui-item><a href="janus:object/id=torus">torus</a></ui-item>
20
- <ui-item><a href="janus:object/id=capsule">capsule</a></ui-item>
21
- <ui-item><a href="janus:object/id=pipe">pipe</a></ui-item>
22
- <ui-item><a href="janus:object/id=plane">plane</a></ui-item>
23
-
24
- <ui-item><a href="janus:image">image</a></ui-item>
25
- <ui-item><a href="janus:link">link</a></ui-item>
26
- <ui-item><a href="janus:paragraph">paragraph</a></ui-item>
27
- <ui-item><a href="janus:particle">particle</a></ui-item>
28
- <ui-item><a href="janus:sound">sound</a></ui-item>
29
- <ui-item><a href="janus:text/text=Text/col=red/roughness=0.3/metalness=0">text</a></ui-item>
30
- <ui-item><a href="janus:video">video</a></ui-item>
31
-
32
- <ui-item><a href="janus:light/light_cone_angle=0">point light</a></ui-item>
33
- <ui-item><a href="janus:light/light_cone_angle=0.8">spot light</a></ui-item>
34
- <ui-item><a href="janus:light/light_cone_angle=1">directional light</a></ui-item>
35
- </ui-grid>
36
- </ui-tab>
37
- <ui-tab label="Room Assets">
38
- <janus-ui-inventory-roomassets></janus-ui-inventory-roomassets>
39
- </ui-tab>
40
- <!-- Disabled Google Poly - maybe one day ICOSA Gallery or someone else will offer the old Google Blocks API again
41
- <ui-tab label="Google Poly">
42
- <ui-flexpanel id="poly" vertical>
43
- <ui-input noflex placeholder="Search for Google Poly models" onaccept="let models = document.getElementById('models'); models.apiargs.keywords = this.value; delete models.apiargs.pageToken; document.getElementById('itemlist').clear(); models.load(); pagination.disabled = false;"></ui-input>
44
- <ui-grid name="polyobjects" scrollable-y flex="1" class="models" id="itemlist" itemcomponent="janus.ui.inventory.item.poly" collection="models" selectable="true"></ui-grid>
45
- <ui-button noflex id="pagination" onclick="let models = document.getElementById('models'); if (models.rawdata.nextPageToken) { models.apiargs.pageToken = models.rawdata.nextPageToken; models.append(); } else { this.disabled = true; }">Load More</ui-button>
46
- </ui-flexpanel>
47
- </ui-tab>
48
- -->
49
- <ui-tab label="Vesta">
50
- <h3>Vesta integration coming soon!</h3>
51
- <p>When it's ready, you'll be able to access assets you've stored on your Vesta account, and bring them with you anywhere you go.</p>
52
- <p><a href="https://vesta.janusvr.com/">Sign up for a vesta account today</a></p>
53
- </ui-tab>
54
- <ui-tab label="Import">
55
- <div>
56
- <h3>Did you know!?</h3>
57
- <p>You can drag 3d models, images, videos, and sounds into Janus rooms from other websites, or even your local hard drive!</p>
58
- <p>Just select the files, drag them over, and drop them into the world. Then use our in-world editing tools to place it where you want it to be.</p>
59
- </div>
60
- </ui-tab>
61
- </ui-tabs>
62
-
2
+ <div class="inv-controls">
3
+ <input type="text" name="search" class="inv-search" placeholder="Search assets…" />
4
+ <select name="sortselect" class="inv-sort">
5
+ <option value="title">Name</option>
6
+ <option value="type">Type</option>
7
+ <option value="source">Source</option>
8
+ </select>
9
+ </div>
10
+ <div class="inv-chips" name="sourcefilters"></div>
11
+ <div class="inv-chips" name="typefilters"></div>
12
+ <ui-grid name="assetgrid" class="models inv-grid" selectable="true" draggable="true"
13
+ itemcomponent="janus.ui.inventory.item.asset"></ui-grid>
14
+ <div class="inv-empty" name="empty">
15
+ No assets yet. Drag 3D models, images, videos or sounds in from another site or
16
+ your desktop, or pick a primitive above.
17
+ </div>
@@ -1,82 +1,352 @@
1
- elation.elements.define('janus.ui.inventory', class extends elation.elements.base {
2
- create() {
3
- //this.innerHTML = elation.template.get('janus.ui.inventory');
4
- let elements = elation.elements.fromTemplate('janus.ui.inventory', this);
5
-
6
- console.log('INVENTORY ELEMENTS', elements);
7
- if (elements.primitives) {
8
- elation.events.add(elements.primitives, 'select', (ev) => this.handlePrimitiveSelect(ev));
9
- //elation.events.add(elements.polyobjects, 'select', (ev) => this.handleAssetSelect(ev));
10
- }
11
- }
12
- addSource(name, collection) {
1
+ /* janus.ui.inventory — a unified, source-aggregated asset browser.
2
+ *
3
+ * Sources (built-in primitives, the room's own markup assets, and any
4
+ * host-registered source) each contribute items. The inventory merges items
5
+ * that refer to the same underlying asset (by key), so one card can carry
6
+ * several provenance badges (e.g. an asset that is both in the room and from a
7
+ * host source), and presents a single searchable / filterable / sortable grid.
8
+ * Selecting or dragging a card places the asset into the scene via the editor.
9
+ */
10
+ (function () {
11
+ // ---- source registry (module-private, shared across inventory instances) --
12
+ var SOURCES = []; // [{ name, label, items, refreshOn }]
13
+ var INSTANCES = []; // live janus.ui.inventory elements
14
+ var SOURCE_LABEL = {}; // name -> label (for badges/chips)
15
+
16
+ var TYPE_ICON = {
17
+ object: '▣', image: '\u{1F5BC}', video: '\u{1F39E}',
18
+ sound: '\u{1F50A}', light: '\u{1F4A1}', text: '\u{1F1F9}', link: '\u{1F517}',
19
+ particle: '✨', shader: '\u{1F308}', script: '\u{1F4DC}'
20
+ };
21
+
22
+ function notify() { for (var i = 0; i < INSTANCES.length; i++) INSTANCES[i].rebuild(); }
23
+
24
+ // Register (or replace, by name) an asset source. Safe to call before any
25
+ // inventory exists — live inventories rebuild immediately.
26
+ // src = { name, label, refreshOn?, items }
27
+ // items: array | () => array | () => Promise<array>
28
+ // each raw item: { key?, type?, title?/name?, thumbnail?/src?, tags?,
29
+ // url?/href? | getInsertURL() }
30
+ function addSource(src) {
31
+ if (!src || !src.name) return;
32
+ for (var i = 0; i < SOURCES.length; i++) { if (SOURCES[i].name === src.name) { SOURCES.splice(i, 1); break; } }
33
+ SOURCES.push(src);
34
+ SOURCE_LABEL[src.name] = src.label || src.name;
35
+ notify();
13
36
  }
14
- handlePrimitiveSelect(ev) {
15
- let item = ev.target;
16
- elation.events.fire({type: 'assetselect', data: item.value.href, element: this});
17
- ev.preventDefault();
18
- ev.stopPropagation();
37
+ function removeSource(name) {
38
+ for (var i = 0; i < SOURCES.length; i++) { if (SOURCES[i].name === name) { SOURCES.splice(i, 1); notify(); return; } }
19
39
  }
20
- handleAssetSelect(ev) {
21
- let item = ev.target;
22
- elation.events.fire({type: 'assetselect', data: item.model.root.url, element: this});
23
- ev.preventDefault();
24
- ev.stopPropagation();
40
+
41
+ function normalizeItem(raw, srcname) {
42
+ var url = (typeof raw.getInsertURL === 'function') ? raw.getInsertURL() : (raw.url || raw.href || '');
43
+ // 'model' (uploaded 3D files) and 'object' (primitives, room objects) are the
44
+ // same kind of placeable asset; collapse to one type so they share a filter.
45
+ var type = raw.type === 'model' ? 'object' : (raw.type || 'object');
46
+ var rec = {
47
+ key: raw.key || url || raw.title || raw.name,
48
+ type: type,
49
+ title: raw.title || raw.name || raw.key || '',
50
+ thumbnail: raw.thumbnail || raw.src || null,
51
+ icon: raw.icon || null,
52
+ tags: raw.tags ? raw.tags.slice() : [],
53
+ url: url,
54
+ sources: [srcname],
55
+ _bySource: {}
56
+ };
57
+ rec._bySource[srcname] = url;
58
+ return rec;
25
59
  }
26
- });
27
- elation.elements.define('janus.ui.inventory.item.poly', class extends elation.elements.ui.item {
28
- create() {
29
- let item = this.value;
30
-
31
- for (let i = 0; i < item.formats.length; i++) {
32
- if (item.formats[i].formatType == 'GLTF2') {
33
- this.model = item.formats[i];
34
- }
60
+
61
+ // Merge records sharing a key: union the badges; an asset already in the room
62
+ // keeps the room insert URL (reference the existing <asset>) over a source URL
63
+ // (which would add a new asset def).
64
+ function mergeAssets(records) {
65
+ var byKey = {}, order = [];
66
+ for (var i = 0; i < records.length; i++) {
67
+ var r = records[i], k = r.key, m = byKey[k];
68
+ if (!m) { byKey[k] = r; order.push(k); continue; }
69
+ for (var s = 0; s < r.sources.length; s++) if (m.sources.indexOf(r.sources[s]) === -1) m.sources.push(r.sources[s]);
70
+ for (var sk in r._bySource) m._bySource[sk] = r._bySource[sk];
71
+ if (!m.thumbnail && r.thumbnail) m.thumbnail = r.thumbnail;
72
+ if ((!m.title || m.title === m.key) && r.title) m.title = r.title;
73
+ for (var t = 0; t < r.tags.length; t++) if (m.tags.indexOf(r.tags[t]) === -1) m.tags.push(r.tags[t]);
35
74
  }
36
- if (!this.model) {
37
- this.model = item.formats[0];
75
+ var out = [];
76
+ for (var o = 0; o < order.length; o++) {
77
+ var rec = byKey[order[o]];
78
+ rec.url = rec._bySource['room'] || rec.url; // prefer reference-existing
79
+ out.push(rec);
38
80
  }
39
-
40
- let elements = elation.elements.fromTemplate('janus.ui.inventory.item.poly', this);
41
- elation.events.add(this, 'click', (ev) => this.click(ev));
42
- }
43
- });
44
- elation.elements.define('janus.ui.inventory.roomassets', class extends elation.elements.base {
45
- create() {
46
- this.elements = elation.elements.fromTemplate('janus.ui.inventory.roomassets', this);
81
+ return out;
47
82
  }
48
- });
49
- elation.elements.define('janus.ui.inventory.roomassets.list', class extends elation.elements.base {
50
- constructor() {
51
- super();
52
- this.defineAttribute('assettype', { type: 'string' });
53
- }
54
- create() {
55
- this.list = elation.elements.create('ui-grid', {
56
- append: this,
57
- class: 'models',
58
- selectable: 1,
59
- draggable: 1,
60
- itemcomponent: 'janus.ui.inventory.item.roomasset',
83
+
84
+ function activeKeys(state) { var k = [], n; for (n in state) if (state[n]) k.push(n); return k; }
85
+
86
+ // ---- built-in sources ------------------------------------------------------
87
+ var PRIMITIVE_DEFS = [
88
+ { id: 'cube', label: 'Cube', type: 'object', url: 'janus:object/id=cube' },
89
+ { id: 'sphere', label: 'Sphere', type: 'object', url: 'janus:object/id=sphere' },
90
+ { id: 'cone', label: 'Cone', type: 'object', url: 'janus:object/id=cone' },
91
+ { id: 'cylinder', label: 'Cylinder', type: 'object', url: 'janus:object/id=cylinder' },
92
+ { id: 'pyramid', label: 'Pyramid', type: 'object', url: 'janus:object/id=pyramid' },
93
+ { id: 'torus', label: 'Torus', type: 'object', url: 'janus:object/id=torus' },
94
+ { id: 'capsule', label: 'Capsule', type: 'object', url: 'janus:object/id=capsule' },
95
+ { id: 'pipe', label: 'Pipe', type: 'object', url: 'janus:object/id=pipe' },
96
+ { id: 'plane', label: 'Plane', type: 'object', url: 'janus:object/id=plane' },
97
+ { id: 'image', label: 'Image', type: 'image', url: 'janus:image' },
98
+ { id: 'link', label: 'Link', type: 'link', url: 'janus:link' },
99
+ { id: 'paragraph', label: 'Paragraph', type: 'text', url: 'janus:paragraph' },
100
+ { id: 'particle', label: 'Particle', type: 'particle', url: 'janus:particle' },
101
+ { id: 'sound', label: 'Sound', type: 'sound', url: 'janus:sound' },
102
+ { id: 'text', label: 'Text', type: 'text', url: 'janus:text/text=Text/col=red/roughness=0.3/metalness=0' },
103
+ { id: 'video', label: 'Video', type: 'video', url: 'janus:video' },
104
+ { id: 'pointlight', label: 'Point light', type: 'light', url: 'janus:light/light_cone_angle=0' },
105
+ { id: 'spotlight', label: 'Spot light', type: 'light', url: 'janus:light/light_cone_angle=0.8' },
106
+ { id: 'dirlight', label: 'Directional light', type: 'light', url: 'janus:light/light_cone_angle=1' }
107
+ ];
108
+ // Shape primitives that have a glyph in the engine object-icon sprite sheet
109
+ // (see .inv-prim-* in inventory.css). Others fall back to their type emoji.
110
+ var PRIM_ICON = { cube:1, sphere:1, cone:1, cylinder:1, pyramid:1, torus:1, capsule:1, pipe:1, plane:1 };
111
+ function primitiveItems() {
112
+ return PRIMITIVE_DEFS.map(function (p) {
113
+ return { key: 'primitive:' + p.id, type: p.type, title: p.label, url: p.url, tags: ['primitive'],
114
+ icon: PRIM_ICON[p.id] ? p.id : null };
61
115
  });
62
- this.updateList();
63
- elation.events.add(janus._target, 'room_change', (ev) => this.updateList());
64
116
  }
65
- updateList() {
66
- this.list.clear();
67
- this.list.innerHTML = '';
68
- this.list.setItems(this.getAssetList());
69
- }
70
- getAssetList() {
71
- if (room.assetpack && room.assetpack.assetmap[this.assettype]) {
72
- return room.assetpack.assetmap[this.assettype];
117
+
118
+ // Per-type insert URL for an existing room asset (references it by id, so no
119
+ // duplicate asset def is created).
120
+ var ROOM_REF = { object: 'object/id', image: 'image/image_id', sound: 'sound/sound_id', video: 'video/video_id' };
121
+ function roomItems() {
122
+ var out = [], room = window.room;
123
+ if (!room || !room.assetpack || !room.assetpack.assetmap) return out;
124
+ var map = room.assetpack.assetmap, type, name, asset, src, ref, parts;
125
+ for (type in map) {
126
+ for (name in map[type]) {
127
+ asset = map[type][name];
128
+ src = (asset && (asset.src || (typeof asset.getFullURL === 'function' && asset.getFullURL()))) || '';
129
+ ref = ROOM_REF[type] || 'object/id';
130
+ parts = ref.split('/');
131
+ out.push({
132
+ key: src || (type + ':' + name),
133
+ type: type,
134
+ title: name,
135
+ thumbnail: (type === 'image' ? src : null),
136
+ url: 'janus:' + parts[0] + '/' + parts[1] + '=' + encodeURIComponent(name),
137
+ tags: ['room']
138
+ });
139
+ }
73
140
  }
74
- return {};
75
- }
76
- });
77
- elation.elements.define('janus.ui.inventory.item.roomasset', class extends elation.elements.ui.item {
78
- create() {
79
- let elements = elation.elements.fromTemplate('janus.ui.inventory.item.roomasset', this);
80
- elation.events.add(this, 'click', (ev) => this.click(ev));
141
+ return out;
81
142
  }
82
- });
143
+
144
+ addSource({ name: 'primitive', label: 'Primitive', items: primitiveItems });
145
+ addSource({ name: 'room', label: 'Room', items: roomItems, refreshOn: ['room_change'] });
146
+
147
+ // ---- the inventory element -------------------------------------------------
148
+ elation.elements.define('janus.ui.inventory', class extends elation.elements.base {
149
+ create() {
150
+ this.assets = [];
151
+ this.q = '';
152
+ this.activeSources = {};
153
+ this.activeTypes = {};
154
+ this.sortKey = 'title';
155
+ this._rebuildToken = 0;
156
+
157
+ elation.elements.fromTemplate('janus.ui.inventory', this);
158
+ this.grid = this.querySelector('[name=assetgrid]');
159
+ this.searchEl = this.querySelector('[name=search]');
160
+ this.sortEl = this.querySelector('[name=sortselect]');
161
+ this.sourceChips = this.querySelector('[name=sourcefilters]');
162
+ this.typeChips = this.querySelector('[name=typefilters]');
163
+ this.emptyEl = this.querySelector('[name=empty]');
164
+
165
+ if (this.searchEl) elation.events.add(this.searchEl, 'input,change', (ev) => { this.q = (this.searchEl.value || '').toLowerCase(); this.applyFilters(); });
166
+ if (this.sortEl) elation.events.add(this.sortEl, 'change', (ev) => { this.sortKey = this.sortEl.value || 'title'; this.applyFilters(); });
167
+ if (this.sourceChips) elation.events.add(this.sourceChips, 'click', (ev) => this.toggleChip(ev, this.activeSources));
168
+ if (this.typeChips) elation.events.add(this.typeChips, 'click', (ev) => this.toggleChip(ev, this.activeTypes));
169
+
170
+ // Activation: a full click places via the editor (NOT the list's `select`,
171
+ // which fires on mousedown — that spawned the object mid-press). dragstart
172
+ // carries the insert URL so dropping into the scene works (independent of
173
+ // reorder).
174
+ if (this.grid) {
175
+ elation.events.add(this.grid, 'click', (ev) => this.handleActivate(ev));
176
+ elation.events.add(this.grid, 'dragstart', (ev) => this.handleDragStart(ev));
177
+ }
178
+
179
+ // Route selection to the editor controller, so the inventory works whether
180
+ // it's inside the editor panel or placed standalone (decomposed).
181
+ var c = (typeof window.getEditorController === 'function') ? window.getEditorController() : null;
182
+ if (c) { this.controller = c; c.inventory = this; elation.events.add(this, 'assetselect', (ev) => c.handleInventorySelect(ev)); }
183
+
184
+ try { elation.events.add(janus._target, 'room_change', () => this.rebuild()); } catch (e) {}
185
+
186
+ INSTANCES.push(this);
187
+ this.rebuild();
188
+ }
189
+
190
+ // Public per-instance hooks (mirror the static API for convenience).
191
+ addSource(src) { addSource(src); }
192
+ removeSource(name) { removeSource(name); }
193
+
194
+ rebuild() {
195
+ var token = ++this._rebuildToken;
196
+ var collected = [], pending = 0, self = this;
197
+ var finish = function () {
198
+ if (pending !== 0 || token !== self._rebuildToken) return;
199
+ self.assets = mergeAssets(collected);
200
+ self.updateChips();
201
+ self.applyFilters();
202
+ };
203
+ for (var i = 0; i < SOURCES.length; i++) {
204
+ (function (src) {
205
+ var items;
206
+ try { items = (typeof src.items === 'function') ? src.items() : src.items; } catch (e) { items = []; }
207
+ var push = function (list) { for (var j = 0; j < (list || []).length; j++) collected.push(normalizeItem(list[j], src.name)); };
208
+ if (items && typeof items.then === 'function') {
209
+ pending++;
210
+ items.then(function (list) { push(list); pending--; finish(); }, function () { pending--; finish(); });
211
+ } else {
212
+ push(items);
213
+ }
214
+ })(SOURCES[i]);
215
+ }
216
+ finish();
217
+ }
218
+
219
+ updateChips() {
220
+ // Source chips come from registered sources; type chips from present types.
221
+ if (this.sourceChips) {
222
+ var names = SOURCES.map(function (s) { return s.name; });
223
+ this.renderChips(this.sourceChips, names, function (n) { return SOURCE_LABEL[n] || n; }, this.activeSources, 'src');
224
+ }
225
+ if (this.typeChips) {
226
+ var types = [], seen = {}, i;
227
+ for (i = 0; i < this.assets.length; i++) { var t = this.assets[i].type; if (!seen[t]) { seen[t] = 1; types.push(t); } }
228
+ types.sort();
229
+ this.renderChips(this.typeChips, types, function (t) { return t; }, this.activeTypes, 'type');
230
+ }
231
+ }
232
+
233
+ renderChips(container, keys, labelfn, state, kind) {
234
+ container.innerHTML = '';
235
+ for (var i = 0; i < keys.length; i++) {
236
+ var k = keys[i];
237
+ var chip = document.createElement('button');
238
+ chip.type = 'button';
239
+ chip.className = 'inv-chip' + (state[k] ? ' active' : '') + (kind === 'src' ? ' inv-badge-' + k : '');
240
+ chip.setAttribute('data-key', k);
241
+ chip.textContent = labelfn(k);
242
+ container.appendChild(chip);
243
+ }
244
+ }
245
+
246
+ toggleChip(ev, state) {
247
+ var chip = ev.target && ev.target.closest ? ev.target.closest('[data-key]') : null;
248
+ if (!chip) return;
249
+ var k = chip.getAttribute('data-key');
250
+ state[k] = !state[k];
251
+ chip.classList.toggle('active', !!state[k]);
252
+ this.applyFilters();
253
+ }
254
+
255
+ applyFilters() {
256
+ var q = this.q, srcs = activeKeys(this.activeSources), types = activeKeys(this.activeTypes);
257
+ var view = this.assets.filter(function (a) {
258
+ if (q) {
259
+ var hit = (a.title || '').toLowerCase().indexOf(q) !== -1;
260
+ if (!hit) for (var i = 0; i < a.tags.length; i++) if (String(a.tags[i]).toLowerCase().indexOf(q) !== -1) { hit = true; break; }
261
+ if (!hit) return false;
262
+ }
263
+ if (srcs.length) { var ok = false; for (var s = 0; s < a.sources.length; s++) if (srcs.indexOf(a.sources[s]) !== -1) { ok = true; break; } if (!ok) return false; }
264
+ if (types.length && types.indexOf(a.type) === -1) return false;
265
+ return true;
266
+ });
267
+ var key = this.sortKey;
268
+ view.sort(function (a, b) {
269
+ var av = key === 'source' ? (a.sources[0] || '') : (a[key] || '');
270
+ var bv = key === 'source' ? (b.sources[0] || '') : (b[key] || '');
271
+ return String(av).localeCompare(String(bv));
272
+ });
273
+ if (this.grid) this.grid.setItems(view);
274
+ if (this.emptyEl) this.emptyEl.style.display = view.length ? 'none' : '';
275
+ }
276
+
277
+ handleActivate(ev) {
278
+ // Native click: resolve the card from the event target (the anchor/img/title
279
+ // inside it), so placement happens on click-release, not on mousedown.
280
+ var item = ev.target && ev.target.closest ? ev.target.closest('.inv-asset-item') : null;
281
+ var rec = item && item.value;
282
+ if (!rec || !rec.url) return;
283
+ elation.events.fire({ type: 'assetselect', element: this, data: rec.url });
284
+ if (ev.preventDefault) ev.preventDefault();
285
+ if (ev.stopPropagation) ev.stopPropagation();
286
+ }
287
+
288
+ handleDragStart(ev) {
289
+ var item = ev.target && ev.target.closest ? ev.target.closest('.inv-asset-item') : null;
290
+ var rec = item && item.value;
291
+ if (!rec || !rec.url || !ev.dataTransfer) return;
292
+ try {
293
+ ev.dataTransfer.effectAllowed = 'copy';
294
+ ev.dataTransfer.setData('text/uri-list', rec.url);
295
+ ev.dataTransfer.setData('text/plain', rec.url);
296
+ } catch (e) {}
297
+ }
298
+ });
299
+
300
+ // Card renderer for one merged asset record (this.value). Build in render()
301
+ // (and on every value assignment) rather than create(): the list assigns
302
+ // `value` around create time, so a one-shot create() read produced blank cards.
303
+ elation.elements.define('janus.ui.inventory.item.asset', class extends elation.elements.ui.item {
304
+ init() {
305
+ super.init();
306
+ // Re-render whenever the value is (re)assigned, regardless of set/create order.
307
+ this.defineAttributes({ value: { type: 'object', set: () => this.render() } });
308
+ }
309
+ render() {
310
+ var v = this.value;
311
+ this.innerHTML = '';
312
+ if (v == null || typeof v !== 'object') return;
313
+ this.addclass('inv-asset-item');
314
+ var a = document.createElement('a');
315
+ a.setAttribute('href', v.url || '#');
316
+ a.setAttribute('draggable', 'true');
317
+ a.setAttribute('onclick', 'return false');
318
+ if (v.thumbnail) {
319
+ var img = document.createElement('img'); img.src = v.thumbnail; a.appendChild(img);
320
+ } else if (v.icon) {
321
+ var ic = document.createElement('span'); ic.className = 'inv-type-icon';
322
+ var sprite = document.createElement('span'); sprite.className = 'inv-prim-icon inv-prim-' + v.icon;
323
+ ic.appendChild(sprite); a.appendChild(ic);
324
+ } else {
325
+ var ic = document.createElement('span'); ic.className = 'inv-type-icon'; ic.textContent = TYPE_ICON[v.type] || '▩'; a.appendChild(ic);
326
+ }
327
+ var h = document.createElement('h2'); h.textContent = v.title || ''; a.appendChild(h);
328
+ this.appendChild(a);
329
+ if (v.sources && v.sources.length) {
330
+ var badges = document.createElement('span'); badges.className = 'inv-badges';
331
+ for (var i = 0; i < v.sources.length; i++) {
332
+ var b = document.createElement('span');
333
+ b.className = 'inv-badge inv-badge-' + v.sources[i];
334
+ b.textContent = SOURCE_LABEL[v.sources[i]] || v.sources[i];
335
+ b.title = b.textContent;
336
+ badges.appendChild(b);
337
+ }
338
+ this.appendChild(badges);
339
+ }
340
+ }
341
+ });
342
+
343
+ // Host registration hook (engine-generic; no downstream coupling). A host can
344
+ // register an asset source at any time: window.JanusInventory.addSource({...})
345
+ // If a host registered before this script loaded (pushing onto a `sources`
346
+ // queue), drain it now.
347
+ var hook = (window.JanusInventory = window.JanusInventory || {});
348
+ if (hook.sources && hook.sources.length) hook.sources.forEach(addSource);
349
+ hook.sources = null;
350
+ hook.addSource = addSource;
351
+ hook.removeSource = removeSource;
352
+ })();
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "templates": {
3
- "janus.ui.inventory": "./inventory.html",
4
- "janus.ui.inventory.roomassets": "./roomassets.html",
5
- "janus.ui.inventory.item.poly": "./inventory-item-poly.html",
6
- "janus.ui.inventory.item.roomasset": "./inventory-item-roomasset.html"
3
+ "janus.ui.inventory": "./inventory.html"
7
4
  },
8
5
  "scripts": ["./inventory.js"],
9
6
  "css": ["./inventory.css"],
@@ -96,7 +96,9 @@ elation.require([], function() {
96
96
  opts = opts ? {...opts, url, hash} : {url,hash}
97
97
  console.log("hyperlink: "+href)
98
98
  elation.events.fire({element: this, type: 'href', data: {href,opts}});
99
- if( room.url != url.origin+url.pathname ) return this.executeExternal(href,opts)
99
+ if( String(url).replace(/#.*/,'') != room.getFullRoomURL(room.url) ){
100
+ return this.executeExternal(href,opts)
101
+ }
100
102
  hash.forEach( (v,k) => {
101
103
  const {operator,param} = this.getOperators(k)
102
104
  switch( param ){
@@ -166,11 +168,11 @@ elation.require([], function() {
166
168
 
167
169
  getUrlObject = function(href){
168
170
  const urlExpanded = href[0] == '#'
169
- ? room.url+href
170
- : href.match(/:\//) ? href : room.baseurl+href
171
+ ? room.getFullRoomURL( room.url )+href
172
+ : href.match(/:\//) ? href : room.getFullRoomURL(room.baseurl)+href
171
173
  const url = new URL( urlExpanded )
172
174
  const hash = new URLSearchParams( String(url.hash).substr(1) )
173
- return {url,hash}
175
+ return {url,hash,href}
174
176
  }
175
177
 
176
178
  getOperators = function(k){
@@ -225,7 +227,7 @@ elation.require([], function() {
225
227
  });
226
228
 
227
229
  xrf_install_hyperlinks = function(){
228
- if( !room.objects?.scene?.modelasset?.loaded ) {
230
+ if( !room || !room?.objects?.scene?.modelasset?.loaded ) {
229
231
  return setTimeout( xrf_install_hyperlinks, 300 )
230
232
  }
231
233
  if( !room.hyperlink ){
@@ -5,8 +5,7 @@
5
5
  "./level2-hyperlink.js",
6
6
  "./level0-sidecarfile.js",
7
7
  "./level7-engine-prefixes.import.js",
8
- "./level7-engine-prefixes.export.js",
9
- "./patch/metaquest-fix-flickering.js"
8
+ "./level7-engine-prefixes.export.js"
10
9
  ],
11
10
  "css": []
12
11
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "janusweb",
3
3
  "description": "Web client for JanusVR worlds",
4
- "version": "1.7.2",
4
+ "version": "1.7.4",
5
5
  "main": "scripts/janusweb.js",
6
6
  "author": "James Baicoianu",
7
7
  "license": "MIT",
package/scripts/client.js CHANGED
@@ -152,30 +152,6 @@ elation.require(['elements.elements', 'elements', 'engine.engine', 'engine.asset
152
152
  }
153
153
  }
154
154
  });
155
- this.initProxies()
156
- }
157
- initProxies() {
158
- elation.engine.assets = new Proxy(elation.engine.assets,{
159
- get(me,k){
160
- if( k == "corsproxy" && me[k] ){
161
- const p = elation.config.get("engine.assets.corsproxies")
162
- if( ! p?.length ){
163
- return me[k] // nothing to roundrobin
164
- }else{ // roundrobin proxies
165
- if( !p.index ) p.index = 0
166
- const corsproxy = p[ p.index ]
167
- elation.config.set("engine.assets.corsproxy", corsproxy )
168
- p.index = (p.index + 1) % p.length
169
- return corsproxy
170
- }
171
- }
172
- return me[k]
173
- },
174
- set(me,k,v){
175
- me[k] = v
176
- return true
177
- }
178
- })
179
155
  }
180
156
  initButtons() {
181
157
  this.sharebutton = elation.ui.button({classname: 'janusweb_sharing', label: 'Share'});
@@ -386,9 +362,10 @@ elation.require(['elements.elements', 'elements', 'engine.engine', 'engine.asset
386
362
  this.style.overflow = 'hidden';
387
363
  }
388
364
  getClientArgs() {
389
- var fullsize = this.fullsize,
390
- width = (this.fullsize ? window.innerWidth : this.width),
391
- height = (this.fullsize ? window.innerHeight : this.height);
365
+ // Resolution hint is the configured width/height. When fullsize, the engine
366
+ // view sizes itself to its container, so we don't assume the window here.
367
+ var width = this.width,
368
+ height = this.height;
392
369
  var args = {
393
370
  url: this.getRoomURL(),
394
371
  homepage: this.homepage || this.src,
package/scripts/config.js CHANGED
@@ -6,9 +6,14 @@ proxies = [
6
6
  ]
7
7
  elation.config.set('engine.assets.corsproxy', proxies[0]); // CORS proxy URL
8
8
  elation.config.set('engine.assets.corsproxies', [] ); // rotate **online** proxies only
9
- proxies.map( (url) =>
9
+ proxies.map( (url) =>
10
10
  fetch(url,{method:'HEAD'})
11
- .then( (res) => res.ok && elation.config.get("engine.assets.corsproxies").push(url) )
11
+ .then( (res) => {
12
+ // online defaults join the rotation unless an explicit set was configured
13
+ // (e.g. the viewer's corsproxy attribute)
14
+ let corsproxies = elation.config.get("engine.assets.corsproxies");
15
+ if (res.ok && !corsproxies.explicit) corsproxies.push(url);
16
+ })
12
17
  )
13
18
 
14
19
  elation.config.set('engine.assets.workers', 'auto'); // Number of workers to use for asset parsing