janusweb 1.7.2 → 1.7.3
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.
- package/README.md +1 -1
- package/media/assets/webui/apps/comms/chat.html +6 -7
- package/media/assets/webui/apps/comms/comms.css +282 -84
- package/media/assets/webui/apps/comms/comms.html +6 -5
- package/media/assets/webui/apps/comms/comms.js +30 -14
- package/media/assets/webui/apps/comms/userlist.html +10 -17
- package/media/assets/webui/apps/comms/voip.js +111 -1
- package/media/assets/webui/apps/editor/editor-properties.js +34 -2
- package/media/assets/webui/apps/editor/editor.css +27 -0
- package/media/assets/webui/apps/editor/editor.js +326 -49
- package/media/assets/webui/apps/editor/objecticons-small.png +0 -0
- package/media/assets/webui/apps/inventory/inventory.css +71 -0
- package/media/assets/webui/apps/inventory/inventory.html +16 -61
- package/media/assets/webui/apps/inventory/inventory.js +342 -72
- package/media/assets/webui/apps/inventory/inventory.json +1 -4
- package/media/assets/webui/apps/inventory/objecticons-small.png +0 -0
- package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +7 -5
- package/media/assets/webui/apps/xrfragment/xrfragment.json +1 -2
- package/package.json +1 -1
- package/scripts/client.js +4 -27
- package/scripts/config.js +7 -2
- package/scripts/janusbase.js +21 -3
- package/scripts/janusweb.js +24 -3
- package/scripts/object.js +67 -7
- package/scripts/room.js +113 -7
- package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +0 -15
|
@@ -1,62 +1,17 @@
|
|
|
1
1
|
<h3>Inventory</h3>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (room.assetpack
|
|
72
|
-
|
|
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"],
|
|
Binary file
|
|
@@ -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(
|
|
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
|
|
230
|
+
if( !room || !room?.objects?.scene?.modelasset?.loaded ) {
|
|
229
231
|
return setTimeout( xrf_install_hyperlinks, 300 )
|
|
230
232
|
}
|
|
231
233
|
if( !room.hyperlink ){
|
package/package.json
CHANGED
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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) =>
|
|
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
|