BUSSID Maker + 3D Model Viewer
Buat livery BUSSID versimu sendiri dengan
BUSSID Livery Maker, editor livery Bus Simulator Indonesia tanpa aplikasi tambahan dan tanpa keahlian editing. Pilih jenis bus, warnai
tiap bagian bodi, tempel logo dan stiker, lalu ekspor PNG siap pakai.
Tool ini dilengkapi
3D Mod BUSSID Viewer
. Kamu bisa melihat livery di model 3D tiap bus, membuka file mod BUSSID, mengekstrak
template UV mesh
, lalu mengirimnya ke editor sebagai panduan desain.
Aplikasi Buat Livery Bussid
Pick Color
#FF0000
Default Swatches
User Colors
Klik spektrum & hue untuk pilih warna. Edit RGB/HEX juga sinkron.
Bagian Livery
—Reload?
Mask tiap part berubah sesuai pilihan.
Pilih part, lalu pilih warna atau gunakan Brush.
Range: 300–5000 px. Kosongkan = pakai default.
Brush
Brush hanya menggambar pada part aktif dan otomatis termasking.
Logo Layers
Tip: Klik logo untuk pilih, drag untuk pindah. Segitiga = uniform scale, kotak sudut = stretch, lingkaran = rotate.
100%
Koleksi Livery Bussid Sudah Jadi
Apa Itu BUSSID Livery Maker?
BUSSID Livery Maker adalah editor livery 2D berbasis browser untuk game Bus Simulator Indonesia. Kamu mewarnai bagian
bodi sesuai mask bus yang dipilih, menambah logo dan goresan brush, lalu mengekspor hasilnya sebagai gambar PNG yang
dipasang lewat menu livery di game. Ada tombol
3D Preview
untuk melihat hasilnya di model 3D bus yang sama.
Apa Itu 3D Mod BUSSID Viewer?
3D Mod BUSSID Viewer adalah penampil model 3D di browser yang dirender real-time dengan WebGL. Ia membuka file mod
BUSSID dan model 3D lain, menampilkan statistik model, dan menguji livery pada model itu. Fitur utamanya adalah
ekstraksi template UV mesh
: koordinat UV model diubah menjadi gambar PNG yang bisa diunduh atau dikirim ke editor sebagai panduan menggambar
livery. Viewer
tidak mengubah file mod-nya
. Yang kamu edit adalah gambar livery atau tekstur dengan bantuan template UV.
Fitur Utama
6 jenis bus:
Yudistira HD, Sadewa SHD, Nakula SHD, Srikandi SHD, Arjuna XHD, Bimasena SDD.
7 bagian livery:
Body Kanan, Body Kiri, Body Depan, Body Belakang, Atap, Kaca, Aksesoris.
Color picker, brush, dan logo layer
(pindah, ubah ukuran, putar, opacity, mask per bagian).
Efek 3D dan template bus
untuk hasil yang rapi.
Preview 3D untuk tiap bus,
dirender langsung di browser.
Viewer file mod:
.bussidmod, .modbussid, .glb, .gltf, dan .obj.
Template UV
(White Base dan UV Wireframe) 1024 sampai 4096 px.
Save/Load project (.nmt), Export PNG
300 sampai 5000 px, dan
upload ke ImgBB
.
Tutorial 1: Cara Menggunakan BUSSID Livery Maker (Editor 2D dan Preview 3D)
Pilih jenis bus.
Di panel
Bagian Livery
, buka dropdown
Jenis Bus
dan pilih model yang kamu pakai di game. Mask bagian bodi, template, dan efek 3D otomatis menyesuaikan.
Pilih bagian yang diwarnai.
Ketuk salah satu tombol bagian (Body Kanan, Body Kiri, Body Depan, Body Belakang, Atap, Kaca, atau Aksesoris).
Bagian aktif diberi cincin biru.
Warnai bagian.
Klik
spektrum warna
dan
bar hue
, atau ketik nilai di kolom
RGB
. Tombol
Mode
beralih ke kolom
HEX
.
Pakai
Default Swatches
, atau simpan warna favoritmu dengan
Tambah ke Swatches
.
Dengan
Auto-apply ke part aktif
menyala, warna langsung masuk ke bagian aktif. Kalau mati, gunakan
Kirim ke part aktif
atau
Apply ke part aktif
.
Lock color
mengunci spektrum, hue, dan kolom RGB/HEX supaya warna tidak bergeser tanpa sengaja saat menggulir halaman.
Atur kaca.
Centang
Warnai Kaca
, pilih warna, lalu atur
Opacity
. Kalau tidak dicentang, kaca dibiarkan transparan.
Gambar dengan brush.
Centang
Aktifkan Brush
, pilih warna lewat picker atau kolom HEX, atur
Ukuran
(1 sampai 128 px), lalu goreskan di kanvas. Brush hanya menggambar di bagian aktif dan otomatis dipotong sesuai
mask.
Tambahkan logo dan stiker.
Di
Logo Layers
, pilih satu atau beberapa gambar (PNG transparan paling rapi).
Pilih
Mask
:
Part Aktif
atau bagian tertentu, lalu klik
Tambah Logo
.
Seret isi logo untuk
memindahkan
, tarik
sudut bingkai
untuk mengubah ukuran proporsional, dan tarik
lingkaran di atas bingkai
untuk memutar.
Gunakan slider
Scale (Uniform)
,
Scale X/Y
(matikan
Lock Aspect
untuk meregangkan),
Lebar(px)
,
Rotate
, dan
Opacity
.
Snap Rotate
mengunci putaran per 15°, supaya logo lurus.
Atur susunan dengan
Bring Forward
dan
Send Backward
. Di daftar layer ada
Lock
,
Select
,
UP
,
DOWN
, dan
Copy
. Hapus logo dengan
Delete
.
Zoom dan geser kanvas.
Pakai tombol
−
,
+
,
Reset
, atau scroll mouse. Aktifkan
Hand
untuk menggeser kanvas, lalu matikan lagi sebelum menggeser logo.
Tampilkan template dan efek 3D.Show Template
menumpuk garis panduan bus di atas desain. Dropdown
3D Effect
dan centang
Show
menambah efek bayangan dan highlight. Tombol
Tambah...
menerima efek buatanmu sendiri: nama, URL gambar preview (512 px), dan URL gambar export (resolusi tinggi).
Lihat di Preview 3D.
Klik
3D Preview
di toolbar untuk membuka jendela modal (tutup dengan ✕ atau tombol Esc), atau buka tab
3D Model Viewer
. Livery terbaru dikirim ke model 3D bus yang aktif setiap kali preview dibuka. Mengganti jenis bus ikut mengganti
model 3D-nya.
Undo, Redo, dan simpan project.
Gunakan
Undo/Redo
(atau Ctrl+Z / Ctrl+Y).
Save(.nmt)
menyimpan jenis bus, warna, goresan brush, dan posisi logo.
Load .nmt
melanjutkan project nanti.
Export PNG.
Isi
Export Size
(300 sampai 5000 px, default 1628), lalu klik
Export PNG
. File diberi nama
namatin.com_bussid_[ukuran].png
.
Upload dan bagikan.
Klik
Upload to ImgBB
untuk mendapat link langsung, lalu
Copy
. Tombol ini mengirim versi 512 × 512 px. Untuk kualitas tinggi gunakan Export PNG.
Pasang di game.
Buka BUSSID, masuk Garasi, pilih bus yang sesuai template, buka menu livery, pilih berkas PNG hasil ekspor, lalu
pasang. Nama menu bisa sedikit berbeda antarversi game.
Tutorial 2: Cara Menggunakan 3D Mod BUSSID Viewer dan Ekstrak/Kirim Template UV
Buka tab 3D Model Viewer di bagian atas. Viewer otomatis memuat model preset sesuai bus aktif di editor. Label sumber model menampilkan
Preset bus
.
Muat file mod sendiri.
Klik
Pilih file model
atau seret file ke area viewer. Format yang didukung:
.bussidmod, .modbussid, .glb, .gltf, .obj
.
glTF
dengan tekstur terpisah: pilih semua file (gltf, bin, tekstur) sekaligus.
OBJ
: sertakan file .mtl dan teksturnya.
File diproses lokal di browser. Setelah selesai, statistik
mesh, vertex, segitiga, material, dan tekstur
muncul.
Navigasi model.
Seret untuk memutar, scroll atau cubit untuk zoom, geser dengan dua jari atau klik kanan, dan klik/ketuk dua kali
untuk memfokuskan ke satu titik. Tombol
Fit model
dan
Reset kamera
mengembalikan sudut pandang. Tombol di pojok viewer:
⛶
layar penuh,
◐
ganti latar,
☼
tambah cahaya.
Atur tampilan.
Nyalakan atau matikan
Grid
dan
Wireframe
. Centang
Bayangan
,
Dua sisi
,
Auto-putar
, atau
Balik tekstur V
bila tekstur terlihat terbalik. Panel
Pencahayaan & warna
punya slider Matahari, Lingkungan, Eksposur, serta warna Latar dan Tint cat.
Pilih bagian model.
Buka
Bagian model
, gunakan kolom filter, tombol
Semua / Tidak ada
, atau centang tiap bagian. Bagian yang disembunyikan tidak ikut dibuat menjadi template UV.
Uji livery di model.
Klik
Pilih livery PNG/JPG
(atau seret gambar ke viewer), lalu nyalakan
Pasang livery
.
Livery ke kaca
menerapkannya ke material kaca yang berlivery.
Livery dari Maker
menarik desain terbaru dari editor. Kecocokan hasil tergantung UV model.
Buat template UV.
Template dibuat otomatis setelah model dimuat. Atur
Resolusi
(1024, 2048, atau 4096 px),
Sertakan kaca
,
Balik UV vertikal
(bila posisi terbalik), dan warna garis, lalu klik
Buat ulang
untuk memperbarui.
Unduh hasilnya.
Ada dua hasil:
White Base
(area UV terisi putih di latar transparan) dan
UV Wireframe
(garis mesh). Klik
Download White Base
atau
Download UV Wireframe
untuk menyimpan PNG.
Kirim ke editor.
Send Template to Editor
menjadikan UV Wireframe sebagai template di editor dan mengaktifkan
Show Template
. Ganti jenis bus untuk memuat ulang template bawaan.
Send White Base as Layer
menambahkan siluet putih sebagai layer logo yang memenuhi kanvas. Layer ini mengikuti mask bagian aktif, jadi
ubah
Target Part
bila perlu.
Kembali ke model preset.
Klik
Kembali ke preset bus
untuk memakai model 3D bawaan sesuai jenis bus di editor.
CATATAN: Template akan menampilkan gambar putih saja dan UV Mesh menampilkan banyak coretan. Hal ini dikarenakan template dan uv saling bertumpuk satu sama lain, untuk kamu yang ingin mengedit template file modbussid dan khusus bagian body maka caranya adalah klik dibagian model dibawah pencahayaan dan warna yang ada dibawah sendiri, klik model lalu klik Tidak Ada.
Hal ini akan uncentang semua aset yang membentuk tampilan 3d. Nah, tinggal centang saja bagian khusus body u yang ingin membuat livery khusus mod tersebut. Nah, biasanya sebuah file mod memiliki aset dengan jumlah banyak dan namanya beda-beda, maka sebagai tips silahkan centang dan uji coba satu persatu.
Kan di tiap samping kanan aset kan ada angka yang beda-beda. Sebagai contoh misal aset yudistira hd di bagian model nanti akan ada nama aset HDBody dengan angka 1.3K sama dengan 1300. Kamu tinggal cari aja mana aset dengan angka terbanyak. Karena biasanya angka terbanyak itu adalah body livery. Silahkan centang satu persatu dan lihat di tampilan 3d. Kalau bentuk dan modelnya seperti kendaraan maka itu adalah aset livery. Silahkan edit dan kirim ke template editor.
Tips Agar Livery Rapi dan Tajam
Gunakan
PNG transparan
untuk logo dan stiker.
Aktifkan
Snap Rotate
agar logo lurus.
Gunakan
template UV
sebagai panduan agar elemen desain tidak terpotong di sambungan bodi.
Simpan project (.nmt)
sebelum menutup halaman.
Atur
Export Size
lebih besar untuk hasil yang lebih tajam.
FAQ
Apakah perlu memasang aplikasi tambahan?
Tidak. Editor dan viewer berjalan di browser. Viewer 3D memuat pustaka three.js dari CDN, jadi butuh koneksi internet.
Apakah file mod saya diunggah ke server?
Tidak. File model dibaca dan diproses di browser. Data hanya dikirim jika kamu menekan
Upload to ImgBB
untuk gambar livery.
Untuk apa template UV mesh?
Sebagai panduan posisi saat menggambar livery atau tekstur, agar desain pas di bodi model, terutama pada file mod yang
kamu miliki.
Kenapa livery terlihat terbalik atau tidak pas di model 3D?
Susunan UV tiap model berbeda. Coba
Balik tekstur V
di viewer atau
Balik UV vertikal
di bagian template UV.
Bus apa saja yang punya preview 3D?
Yudistira HD, Sadewa SHD, Nakula SHD, Srikandi SHD, Arjuna XHD, dan Bimasena SDD.
Bagaimana melanjutkan desain di lain waktu?
Simpan lewat
Save(.nmt)
, lalu buka lagi dengan
Load .nmt
.
3D Preview
Memuat viewer 3D…
600) {
busyEl.textContent = 'Viewer 3D gagal dimuat. Periksa koneksi (three.js dari CDN).'; setBadge('fail');
busyEl.classList.remove('hide');
try { fr.remove(); } catch (_) {}
frame = null; bvPromise = null;
reject(new Error('timeout viewer')); return;
}
setTimeout(poll, 100);
})();
});
return bvPromise;
}
/* salin livery Maker (tanpa overlay template & efek 3D) ke canvas khusus 3D */
function renderLive() {
const st = (typeof state !== 'undefined') ? state : null;
const hadTpl = !!(st && st.showTemplate);
try {
if (hadTpl) { st.showTemplate = false; drawAll(); }
const w = off.width, h = off.height;
if (liveCv.width !== w || liveCv.height !== h) { liveCv.width = w; liveCv.height = h; }
liveCtx.clearRect(0, 0, w, h);
liveCtx.drawImage(off, 0, 0);
} catch (e) { console.warn('renderLive', e); }
finally { if (hadTpl) { st.showTemplate = true; try { drawAll(); } catch (_) {} } }
}
function pushLivery(force) {
if (!bv) return;
renderLive();
try {
const recreated = bv.setLiveryCanvas(liveCv, !!force, fitSynced);
if (!recreated) bv.refreshLivery(); // unggah ulang tekstur di setiap pemicu
} catch (e) { console.warn(e); }
}
/* Auto-sync: model 3D selalu = preset bus aktif di Maker (kecuali model kustom diunggah) */
async function syncModel(force) {
const v = await ensureViewer();
const key = activeKey, p = PRESETS[key];
if (!p) return;
if (v.hasCustom() && !force) return;
if (!force && !v.hasCustom() && v.loadedKey() === key) return;
if (!force && syncInFlight && syncKey === key) return syncInFlight;
const pr = v.loadPreset(key, p.label, p.urls);
syncInFlight = pr; syncKey = key;
const clr = () => { if (syncInFlight === pr) syncInFlight = null; };
pr.then(clr, clr);
return pr;
}
function onBusChanged(key) {
if (!key || key === CUSTOM_KEY || key === activeKey) return;
activeKey = key;
if (bv) bv.clearCustom(); // memilih preset = kembali ke model preset
setBadge('loading', 0, 0);
ensureViewer()
.then(v => { if (key !== activeKey) return; if (!(modal || tab === '3d')) v.setActive(false); return syncModel(true); })
.then(() => { if (modal || tab === '3d') pushLivery(); })
.catch(() => setBadge('fail'));
if (modal) titleEl.textContent = '3D Preview \u00b7 ' + label();
}
function updateLock() { document.documentElement.classList.toggle('bm-lock', modal || tab === '3d'); }
function setHostMode(mode) { // 'off' | 'tab' | 'modal'
host.className = 'is-' + mode;
host.style.top = mode === 'tab' ? Math.max(0, Math.round(navEl.getBoundingClientRect().bottom)) + 'px' : '';
}
async function showViewer(mini) {
try {
const v = await ensureViewer();
v.setMini(mini); v.setActive(true);
pushLivery();
await syncModel();
pushLivery(); // render ulang tekstur setiap preview dibuka
} catch (e) { toast('Viewer 3D belum siap: ' + (e && e.message || e)); }
}
/* ---------- tab switcher (posisi scroll tidak diubah) ---------- */
function showTab(name) {
if (name === tab) { if (name === '3d') showViewer(false); return; }
if (tab === 'maker' && !modal) savedY = window.pageYOffset || 0;
tab = name;
const is3d = name === '3d';
tabMaker.setAttribute('aria-selected', String(!is3d));
tab3D.setAttribute('aria-selected', String(is3d));
paneMaker.classList.toggle('bm-off', is3d);
if (is3d) {
if (modal) closeModal(true);
setHostMode('tab'); updateLock();
showViewer(false);
} else {
setHostMode(modal ? 'modal' : 'off'); updateLock();
if (bv && !modal) bv.setActive(false);
if (!modal && Math.abs((window.pageYOffset || 0) - savedY) > 1) window.scrollTo(0, savedY); // jaga-jaga jika browser menggeser
setTimeout(() => window.dispatchEvent(new Event('resize')), 30);
}
}
tabMaker.addEventListener('click', () => showTab('maker'));
tab3D.addEventListener('click', () => showTab('3d'));
window.addEventListener('resize', () => { if (tab === '3d' && !modal) setHostMode('tab'); });
/* ---------- modal minimalis ---------- */
function openModal() {
if (modal) return;
modal = true; lastFocus = document.activeElement;
if (tab === 'maker') savedY = window.pageYOffset || 0;
titleEl.textContent = '3D Preview \u00b7 ' + label();
setHostMode('modal'); updateLock();
$('bmClose').focus({ preventScroll: true });
showViewer(true);
}
function closeModal(keepHost) {
if (!modal) return;
modal = false;
if (!keepHost) setHostMode(tab === '3d' ? 'tab' : 'off');
updateLock();
if (bv) { bv.setMini(false); bv.setActive(tab === '3d'); }
if (tab === 'maker' && Math.abs((window.pageYOffset || 0) - savedY) > 1) window.scrollTo(0, savedY);
if (lastFocus && lastFocus.focus) { try { lastFocus.focus({ preventScroll: true }); } catch (_) {} }
}
$('bmClose').addEventListener('click', () => closeModal());
window.addEventListener('keydown', e => { if (e.key === 'Escape' && modal) closeModal(); });
const prevBtn = $('bm3dPreviewBtn');
if (prevBtn) prevBtn.addEventListener('click', openModal);
/* ---------- Preset "Custom (Uploaded Mod)": satu template tunggal sebagai basis livery ---------- */
const CUSTOM_KEY = 'CUSTOM_MOD', CUSTOM_LABEL = 'Custom (Uploaded Mod)';
const PART_IDS = ['BODY_KANAN', 'BODY_KIRI', 'BODY_DEPAN', 'BODY_BELAKANG', 'ATAP', 'KACA', 'AKSESORIS'];
const PRIMARY_PART = 'BODY_KANAN';
let customOn = false, customImg = null, fitSynced = false;
const blankTpl = document.createElement('canvas'); blankTpl.width = blankTpl.height = 1;
function markActivePart() {
document.querySelectorAll('.parts button').forEach(x => x.classList.toggle('active', x.dataset.id === state.active));
}
function setGlassLock(lock) {
const g = $('enableGlassColor'); if (!g) return;
if (lock) g.checked = false;
g.disabled = !!lock;
}
function leaveCustom() { customOn = false; setGlassLock(false); }
/* Semua warna part diseragamkan ke part utama saat mode custom (satu layer template) */
if (typeof drawAll === 'function') {
const origDraw = drawAll;
window.drawAll = function () {
if (customOn) {
const c = state.colors[PRIMARY_PART] || '#FFFFFF';
PART_IDS.forEach(p => { state.colors[p] = c; });
state.glassColorEnabled = false;
}
return origDraw.apply(this, arguments);
};
}
/* Pemilih bagian: di mode custom semua pilihan diarahkan ke satu layer template */
const partsBox = $('parts');
if (partsBox) partsBox.addEventListener('click', () => {
if (!customOn) return;
state.active = PRIMARY_PART;
markActivePart();
try { ColorPicker.setHex(state.colors[PRIMARY_PART] || '#FFFFFF'); } catch (_) {}
});
async function applyCustom(img) {
customImg = img;
const W = (typeof baseCv !== 'undefined' && baseCv.width) || 512;
BUS_TYPES[CUSTOM_KEY] = { SIZE: W, MASKS: {}, __custom: true };
state.maskImgs = {};
PART_IDS.forEach(p => { state.maskImgs[p] = img; }); // satu mask dipakai semua bagian
state.tplImg = blankTpl; state.tplImgHi = null; state.creditImg = null; state.creditImgHi = null;
state.exportMaskImgs = null; state.exportMaskCanvas = null; state.exportMaskCanvasBus = null;
state.busType = CUSTOM_KEY; customOn = true;
state.showTemplate = false;
const tb = $('toggleTpl'); if (tb) tb.textContent = 'Show Template';
state.glassColorEnabled = false; setGlassLock(true);
PART_IDS.forEach(p => { state.colors[p] = '#FFFFFF'; });
state.active = PRIMARY_PART; markActivePart();
try { ColorPicker.setHex('#FFFFFF'); } catch (_) {}
const sel = $('busType');
if (sel) {
let opt = Array.from(sel.options).find(o => o.value === CUSTOM_KEY);
if (!opt) { opt = document.createElement('option'); opt.value = CUSTOM_KEY; opt.textContent = CUSTOM_LABEL; sel.appendChild(opt); }
sel.value = CUSTOM_KEY;
}
try { await set3DListForBus(CUSTOM_KEY); } catch (_) {}
drawAll(); commit();
}
/* ---------- hook pilih bus di Maker (tanpa mengubah kode Maker) ---------- */
const busSel = $('busType');
if (busSel) busSel.addEventListener('change', e => onBusChanged(e.target.value));
if (typeof loadBusType === 'function') {
const orig = loadBusType;
window.loadBusType = async function (key) {
if (key === CUSTOM_KEY) {
if (customImg) await applyCustom(customImg);
return;
}
const wasCustom = customOn;
if (wasCustom) leaveCustom();
const r = await orig.apply(this, arguments);
fitSynced = false;
try { if (wasCustom) activeKey = null; onBusChanged(currentBus()); } catch (_) {}
return r;
};
}
/* ---------- kirim template dari viewer ke editor ---------- */
function canvasToBlob(c) { return new Promise((res, rej) => c.toBlob(b => b ? res(b) : rej(new Error('Gagal membuat PNG')), 'image/png')); }
async function canvasToImage(src, S, stack) {
const c = document.createElement('canvas'); c.width = c.height = S;
const x = c.getContext('2d'); x.imageSmoothingQuality = 'high';
for (let i = 0; i < (stack || 1); i++) x.drawImage(src, 0, 0, S, S);
const url = URL.createObjectURL(await canvasToBlob(c));
return new Promise((res, rej) => {
const im = new Image();
im.onload = () => { URL.revokeObjectURL(url); res(im); };
im.onerror = () => { URL.revokeObjectURL(url); rej(new Error('Gagal memuat gambar template')); };
im.src = url;
});
}
async function receiveTemplate(base, wire) {
const S = (typeof baseCv !== 'undefined' && baseCv.width) || 512;
const hiS = Math.min(wire.width, 2048);
const stack = Math.max(1, Math.min(4, Math.round(wire.width / S))); // garis tipis tetap terlihat saat diperkecil
const lo = await canvasToImage(wire, S, stack);
const hi = await canvasToImage(wire, hiS, 1);
state.tplImg = lo; state.tplImgHi = hi; state.showTemplate = true; fitSynced = true;
const tb = $('toggleTpl'); if (tb) tb.textContent = 'Hide Template';
drawAll();
showTab('maker');
toast('Template UV dimuat ke editor (Show Template aktif).');
}
async function receiveWhiteBase(base) {
const hiS = Math.min(base.width, 2048);
const img = await canvasToImage(base, hiS, 1);
await applyCustom(img);
fitSynced = true;
showTab('maker');
toast('White Base menjadi template dasar: Custom (Uploaded Mod).');
}
/* ---------- indikator unduh preset + Reload (Yes/No) ---------- */
const badge = $('bmLoadBadge');
function setBadge(st, a, b) {
if (!badge) return;
badge.dataset.st = st;
let t = '\u2014';
if (st === 'loading') t = b > 0 ? Math.min(100, Math.round(a / b * 100)) + '%' : (a > 0 ? (a / 1048576).toFixed(1) + ' MB' : '0%');
else if (st === 'done') t = '100%';
else if (st === 'fail') t = 'Failed';
badge.textContent = t;
}
const rBtn = $('bmReloadBtn'), rAsk = $('bmReloadAsk'), rYes = $('bmReloadYes'), rNo = $('bmReloadNo');
let askT = 0;
function askReload(on) {
clearTimeout(askT);
if (rAsk) rAsk.hidden = !on;
if (rBtn) rBtn.hidden = !!on;
if (on) askT = setTimeout(() => askReload(false), 7000);
}
function doReload() {
if (!PRESETS[activeKey]) { toast('Pilih jenis bus preset untuk memuat ulang template.'); return; }
setBadge('loading', 0, 0);
toast('Memuat ulang model dan template UV\u2026');
ensureViewer()
.then(v => { if (!(modal || tab === '3d')) v.setActive(false); return syncModel(true); })
.then(() => { if (modal || tab === '3d') pushLivery(); })
.catch(() => setBadge('fail'));
}
if (rBtn) rBtn.addEventListener('click', () => askReload(true));
if (rNo) rNo.addEventListener('click', () => askReload(false));
if (rYes) rYes.addEventListener('click', () => { askReload(false); doReload(); });
/* prefetch model preset di latar belakang */
setTimeout(() => {
if (!PRESETS[activeKey] || bvPromise) return;
setBadge('loading', 0, 0);
ensureViewer().then(v => { if (!(modal || tab === '3d')) v.setActive(false); return syncModel(); }).catch(() => setBadge('fail'));
}, 1500);
window.BM = {
PRESETS, showTab, openModal, closeModal, syncModel, pushLivery,
receiveTemplate, receiveWhiteBase,
onCustomModel: name => { if (modal) titleEl.textContent = '3D Preview \u00b7 ' + name; },
loadStatus: (key, st, a, b) => { if (key && key !== activeKey) return; setBadge(st, a, b); }
};
})();