feat(dashboard): UGC details in the property 3D view and model list

Selecting a brick-built model in the 3D view shows its UGC data under
Selected: icons now and before, state, waiting until, UGC id, owner,
name and description, bricks, triangles before and after hidden faces
were removed, timings, and links to the UGC viewer, NIF and LXFML, with
Make again for UGC managers. The property page's model rows have the
same block, opened on demand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 15:23:40 -05:00
parent 19097c13c0
commit 55ea44e2e8
3 changed files with 106 additions and 4 deletions

View File

@@ -217,7 +217,22 @@ function showSelection(model, index) {
'<dt class="col-4">Model ID</dt><dd class="col-8 text-break">' + esc(model.id) + '</dd>' +
'<dt class="col-4">Position</dt><dd class="col-8">' + model.position.map((v) => esc(v.toFixed(2))).join(', ') + '</dd>' +
'<dt class="col-4">Rotation</dt><dd class="col-8">' + esc(yaw) + '&deg;</dd>' + ugcInfo(model) + '</dl>' +
(showcase ? '' : '<a class="btn btn-sm btn-outline-secondary" href="/api/property_models/' + esc(model.id) + '/lxfml">Download LXFML</a>');
(showcase ? '' : brickBuilt(model) ? '<h6 class="mt-2 mb-1">UGC</h6><div id="ugcDetails" class="small text-body-secondary">Loading…</div>'
: '<a class="btn btn-sm btn-outline-secondary" href="/api/property_models/' + esc(model.id) + '/lxfml">Download LXFML</a>');
if (!showcase && brickBuilt(model)) {
const item = ugc[model.id];
const placed = property && property.models.find((m) => String(m.id) === String(model.id));
UgcLinks.details(item ? item.ugcId : placed && placed.ugc_id, 'property=' + propertyId, model.id).then((html) => {
const box = document.getElementById('ugcDetails');
if (box && selectedIndex === index) { box.className = ''; box.innerHTML = html; }
});
}
}
// Brick-built models (their blueprint is on the UGC server); cars and rockets are modular and drawn by the client
function brickBuilt(model) {
const item = ugc[model.id];
return item ? item.kind === 'model' : model.lot === 14;
}
// A player-built model's UGC server state, what it is drawn from, and a link to it in the UGC viewer

View File

@@ -217,6 +217,12 @@ function ugcState(m) {
return '<div class="small mt-1">UGC server: ' + UgcLinks.state(m.ugc) + (m.ugc.link ? ' <a href="' + esc(m.ugc.link) + '">Open in UGC viewer</a>' : '') + '</div>';
}
// A brick-built model's full UGC data (icons, stats, links), loaded when opened (ugc-links.js)
function ugcDetails(m) {
return '<details class="mt-1" data-ugc-details="' + esc(m.ugc_id || '') + '" data-model="' + esc(m.id) + '" data-via="property=' + esc(propertyId) + '">' +
'<summary class="small">UGC details</summary><div class="ugc-details-body mt-1 small text-body-secondary">Loading…</div></details>';
}
const labels = $('#modelsTable thead th').map(function () { return $(this).text().trim(); }).get();
const columns = [
{ data: null, render: (m, type) => {
@@ -236,13 +242,13 @@ const columns = [
{ data: null, render: (m, type) => {
if (m.lxfml_size === null || m.lxfml_size === undefined) {
if (type !== 'display') return m.ldf_config || '';
return m.ldf_config ? '<span class="small">Modular: <code>' + esc(m.ldf_config) + '</code></span>' + ugcState(m) : muted(m.lot === UGC_LOT ? 'Missing' : 'Prebuilt');
return m.ldf_config ? '<span class="small">Modular: <code>' + esc(m.ldf_config) + '</code></span>' + ugcState(m) : (m.lot === UGC_LOT ? muted('Missing') + ugcDetails(m) : muted('Prebuilt'));
}
if (type !== 'display') return m.lxfml_size;
return '<span class="small">' + esc(bytes(m.lxfml_size)) + '</span> ' +
(m.is_optimized ? fmt.badge('Optimized', 'info') + ' ' : '') + (m.bake_ao ? fmt.badge('Baked AO', 'secondary') + ' ' : '') +
(m.ldf_config ? '<div class="small">Modular: <code>' + esc(m.ldf_config) + '</code></div>' : '') +
'<div class="small text-body-secondary">UGC ' + esc(m.ugc_id) + (m.filename ? ' &middot; ' + esc(m.filename) : '') + '</div>' + ugcState(m);
'<div class="small text-body-secondary">UGC ' + esc(m.ugc_id) + (m.filename ? ' &middot; ' + esc(m.filename) : '') + '</div>' + ugcState(m) + (!m.ugc || m.ugc.kind === 'model' ? ugcDetails(m) : '');
} },
{ data: 'position', orderable: false, render: (d) => '<span class="small text-nowrap">' + d.map((v) => v.toFixed(1)).join(', ') + '</span>' },
{ data: 'id', orderable: false, className: 'actions', render: (d) =>