feat(sim-builder): фаза 6 — раздача классу, клон, шаблоны, привязка к программе (custom_sims)
This commit is contained in:
@@ -715,6 +715,21 @@ const SIMS = [
|
||||
|
||||
var _EDIT_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>';
|
||||
var _DEL_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||
var _SHARE_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
|
||||
var _CLONE_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
var _PUB_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
||||
var _UNPUB_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M2 2l20 20"/><path d="M12 2a15.3 15.3 0 0 1 4 10c0 1.3-.2 2.6-.5 3.8M6.5 6.5A15.3 15.3 0 0 0 12 22a15.3 15.3 0 0 0 3.3-5"/><path d="M2 12h7m6 0h7"/></svg>';
|
||||
|
||||
function _isTeacherUser() {
|
||||
try { return typeof user !== 'undefined' && user && (user.role === 'teacher' || user.role === 'admin'); }
|
||||
catch (e) { return false; }
|
||||
}
|
||||
function _btn(act, id, html, extra, title) {
|
||||
return '<button type="button" data-act="' + act + '" data-id="' + _esc(id) + '" ' +
|
||||
(title ? 'title="' + _esc(title) + '" ' : '') +
|
||||
'style="display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:.78rem;font-weight:700;padding:7px 10px;border-radius:10px;cursor:pointer;' + (extra || '') + '">' +
|
||||
html + '</button>';
|
||||
}
|
||||
|
||||
function _cardHtml(m) {
|
||||
var owner = _isOwner(m);
|
||||
@@ -726,14 +741,26 @@ const SIMS = [
|
||||
else if (owner) badges += '<span style="display:inline-flex;align-items:center;gap:4px;font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:3px 9px;border-radius:99px;background:rgba(255,255,255,.06);color:var(--text-3);border:1px solid rgba(255,255,255,.14)">Черновик</span>';
|
||||
var actions = '';
|
||||
if (owner) {
|
||||
var STYLE_PRI = 'flex:1;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)';
|
||||
var STYLE_GHOST = 'background:rgba(255,255,255,.05);color:var(--text-2);border:1px solid rgba(255,255,255,.16)';
|
||||
var STYLE_DEL = 'background:rgba(244,91,105,.1);color:#f45b69;border:1px solid rgba(244,91,105,.28)';
|
||||
var pubBtn = published
|
||||
? _btn('unpublish', m.id, _UNPUB_ICON, STYLE_GHOST, 'Снять с публикации')
|
||||
: _btn('publish', m.id, _PUB_ICON, STYLE_GHOST, 'Опубликовать');
|
||||
actions =
|
||||
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
||||
'<button type="button" data-act="edit" data-id="' + _esc(m.id) + '" ' +
|
||||
'style="flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:.78rem;font-weight:700;padding:7px 10px;border-radius:10px;cursor:pointer;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)">' +
|
||||
_EDIT_ICON + 'Редактировать</button>' +
|
||||
'<button type="button" data-act="del" data-id="' + _esc(m.id) + '" ' +
|
||||
'style="display:inline-flex;align-items:center;justify-content:center;padding:7px 11px;border-radius:10px;cursor:pointer;background:rgba(244,91,105,.1);color:#f45b69;border:1px solid rgba(244,91,105,.28)" title="Удалить">' +
|
||||
_DEL_ICON + '</button>' +
|
||||
_btn('edit', m.id, _EDIT_ICON + 'Редактировать', STYLE_PRI) +
|
||||
_btn('del', m.id, _DEL_ICON, STYLE_DEL, 'Удалить') +
|
||||
'</div>' +
|
||||
'<div style="display:flex;gap:8px;margin-top:8px">' +
|
||||
_btn('share', m.id, _SHARE_ICON + 'Раздать классу', STYLE_GHOST + ';flex:1') +
|
||||
pubBtn +
|
||||
'</div>';
|
||||
} else if (published && _isTeacherUser()) {
|
||||
actions =
|
||||
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
||||
_btn('clone', m.id, _CLONE_ICON + 'Клонировать к себе',
|
||||
'flex:1;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)') +
|
||||
'</div>';
|
||||
}
|
||||
var preview = '<svg class="sim-preview" viewBox="0 0 300 140" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">' +
|
||||
@@ -792,6 +819,14 @@ const SIMS = [
|
||||
location.href = '/sim-builder?id=' + encodeURIComponent(id);
|
||||
} else if (act === 'del') {
|
||||
del(id);
|
||||
} else if (act === 'share') {
|
||||
shareToClass(id);
|
||||
} else if (act === 'clone') {
|
||||
clone(id);
|
||||
} else if (act === 'publish') {
|
||||
setStatus(id, 'published');
|
||||
} else if (act === 'unpublish') {
|
||||
setStatus(id, 'draft');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -818,6 +853,69 @@ const SIMS = [
|
||||
});
|
||||
}
|
||||
|
||||
// Опубликовать / снять с публикации (владельцу). PUT status.
|
||||
function setStatus(dbid, status) {
|
||||
if (!window.LS || !LS.customSimUpdate) return;
|
||||
LS.customSimUpdate(dbid, { status: status }).then(function () {
|
||||
if (_meta[dbid]) _meta[dbid].status = status;
|
||||
renderSection(_catFilter);
|
||||
if (LS.toast) LS.toast(status === 'published' ? 'Опубликовано' : 'Снято с публикации', 'success');
|
||||
}).catch(function (e) {
|
||||
if (LS.toast) LS.toast((e && e.message) || 'Не удалось изменить статус', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Клонировать чужую (published) симуляцию к себе как черновик и открыть в билдере.
|
||||
function clone(dbid) {
|
||||
if (!window.LS || !LS.customSimClone) return;
|
||||
LS.customSimClone(dbid).then(function (res) {
|
||||
var newId = res && res.id;
|
||||
if (newId) {
|
||||
if (LS.toast) LS.toast('Скопировано в ваши черновики', 'success');
|
||||
location.href = '/sim-builder?id=' + encodeURIComponent(newId);
|
||||
}
|
||||
}).catch(function (e) {
|
||||
if (LS.toast) LS.toast((e && e.message) || 'Не удалось клонировать', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Раздать классу: модалка выбора класса -> LS.customSimShare (авто-публикует
|
||||
// и шлёт уведомление ученикам со ссылкой /lab?sim=custom:<id>).
|
||||
function shareToClass(dbid) {
|
||||
if (!window.LS || !LS.customSimShare || !LS.getClasses || !LS.modal) return;
|
||||
LS.getClasses().then(function (classes) {
|
||||
if (!Array.isArray(classes) || !classes.length) {
|
||||
if (LS.toast) LS.toast('Нет классов для раздачи', 'warn');
|
||||
return;
|
||||
}
|
||||
var opts = classes.map(function (c) {
|
||||
return '<option value="' + _esc(c.id) + '">' + _esc(c.name) + '</option>';
|
||||
}).join('');
|
||||
var content = '<div style="display:flex;flex-direction:column;gap:8px">' +
|
||||
'<label style="font-size:.8rem;color:var(--text-3)">Класс</label>' +
|
||||
'<select id="cs-share-class" style="width:100%;box-sizing:border-box;padding:9px 11px;border:1px solid var(--border);border-radius:9px;font:inherit;background:var(--surface);color:var(--text)">' + opts + '</select>' +
|
||||
'<div style="font-size:.78rem;color:var(--text-3)">Ученики класса получат уведомление со ссылкой. Симуляция будет автоматически опубликована.</div>' +
|
||||
'</div>';
|
||||
var m = LS.modal({ title: 'Раздать классу', content: content, size: 'sm', actions: [
|
||||
{ label: 'Отмена', onClick: function () { m.close(); } },
|
||||
{ label: 'Раздать', primary: true, onClick: function () {
|
||||
var sel = m.body.querySelector('#cs-share-class');
|
||||
var classId = sel ? Number(sel.value) : NaN;
|
||||
LS.customSimShare(dbid, { classId: classId }).then(function (r) {
|
||||
m.close();
|
||||
if (_meta[dbid]) _meta[dbid].status = 'published';
|
||||
renderSection(_catFilter);
|
||||
if (LS.toast) LS.toast('Отправлено ученикам: ' + ((r && r.sent) || 0), 'success');
|
||||
}).catch(function (e) {
|
||||
if (LS.toast) LS.toast((e && e.message) || 'Ошибка раздачи', 'error');
|
||||
});
|
||||
} }
|
||||
] });
|
||||
}).catch(function () {
|
||||
if (LS.toast) LS.toast('Не удалось загрузить классы', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузить список custom-sims, зарегистрировать ленивые манифесты, нарисовать секцию.
|
||||
function init() {
|
||||
if (_initPromise) return _initPromise;
|
||||
@@ -844,6 +942,9 @@ const SIMS = [
|
||||
resolveId: resolveId,
|
||||
renderSection: renderSection,
|
||||
ensureSpec: ensureSpec,
|
||||
del: del
|
||||
del: del,
|
||||
share: shareToClass,
|
||||
clone: clone,
|
||||
setStatus: setStatus
|
||||
};
|
||||
})();
|
||||
|
||||
+169
-2
@@ -320,16 +320,26 @@
|
||||
var statusBadge = this.status === 'published'
|
||||
? '<span class="sbu-badge sbu-badge-pub">Опубликовано</span>'
|
||||
: '<span class="sbu-badge">Черновик</span>';
|
||||
// Кнопка публикации: для опубликованной — «Снять с публикации»; иначе «Опубликовать».
|
||||
var pubBtn = this.status === 'published'
|
||||
? '<button class="btn-ghost sbu-tb-btn" data-a="unpublish" title="Вернуть в черновик">' + ICON.unpublish + ' Снять</button>'
|
||||
: '<button class="btn-primary sbu-tb-btn" data-a="publish">' + ICON.send + ' Опубликовать</button>';
|
||||
// «Раздать классу» доступна только для уже сохранённой симуляции.
|
||||
var shareBtn = this.simId
|
||||
? '<button class="btn-ghost sbu-tb-btn" data-a="share" title="Раздать классу">' + ICON.send + ' Раздать</button>'
|
||||
: '';
|
||||
t.innerHTML =
|
||||
'<div class="sbu-tb-left">' +
|
||||
'<span class="sbu-tb-title">' + (this.simId ? 'Редактор симуляции' : 'Новая симуляция') + '</span>' +
|
||||
statusBadge +
|
||||
'</div>' +
|
||||
'<div class="sbu-tb-right">' +
|
||||
'<button class="btn-ghost sbu-tb-btn" data-a="template" title="Создать из шаблона">' + ICON.template + ' Шаблон</button>' +
|
||||
'<button class="btn-ghost sbu-tb-btn" data-a="test" title="Запустить превью">' + ICON.play + ' Тест</button>' +
|
||||
'<button class="btn-ghost sbu-tb-btn" data-a="reset" title="Сброс превью">' + ICON.reset + ' Сброс</button>' +
|
||||
'<button class="btn-ghost sbu-tb-btn" data-a="save">' + ICON.save + ' Сохранить</button>' +
|
||||
'<button class="btn-primary sbu-tb-btn" data-a="publish">' + ICON.send + ' Опубликовать</button>' +
|
||||
shareBtn +
|
||||
pubBtn +
|
||||
'</div>';
|
||||
t.querySelectorAll('[data-a]').forEach(function (b) {
|
||||
b.addEventListener('click', function () { self.onToolbar(b.getAttribute('data-a')); });
|
||||
@@ -341,6 +351,96 @@
|
||||
if (action === 'reset') { if (this.inst && this.inst.reset) this.inst.reset(); return; }
|
||||
if (action === 'save') { this.save(false); return; }
|
||||
if (action === 'publish') { this.save(true); return; }
|
||||
if (action === 'unpublish') { this.setStatus('draft'); return; }
|
||||
if (action === 'share') { this.openShareModal(); return; }
|
||||
if (action === 'template') { this.openTemplateModal(); return; }
|
||||
};
|
||||
|
||||
/* Изменить статус публикации уже сохранённой симуляции (PUT status). */
|
||||
Builder.prototype.setStatus = function (status) {
|
||||
var self = this;
|
||||
if (!this.simId) { this.save(status === 'published'); return; }
|
||||
global.LS.customSimUpdate(this.simId, { status: status }).then(function () {
|
||||
self.status = status;
|
||||
self.renderToolbar();
|
||||
global.LS.toast(status === 'published' ? 'Опубликовано' : 'Снято с публикации', 'success');
|
||||
}).catch(function (e) {
|
||||
global.LS.toast((e && e.message) || 'Ошибка', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
/* Раздать классу: модалка выбора класса -> LS.customSimShare (авто-публикует +
|
||||
уведомляет учеников со ссылкой /lab?sim=custom:<id>). */
|
||||
Builder.prototype.openShareModal = function () {
|
||||
var self = this;
|
||||
if (!this.simId) { global.LS.toast('Сначала сохраните симуляцию', 'warn'); return; }
|
||||
global.LS.getClasses().then(function (classes) {
|
||||
if (!Array.isArray(classes) || !classes.length) { global.LS.toast('Нет классов для раздачи', 'warn'); return; }
|
||||
var opts = classes.map(function (c) {
|
||||
return '<option value="' + esc(c.id) + '">' + esc(c.name) + '</option>';
|
||||
}).join('');
|
||||
var content = '<div style="display:flex;flex-direction:column;gap:8px">' +
|
||||
'<label style="font-size:.8rem;color:var(--text-3)">Класс</label>' +
|
||||
'<select id="sbu-share-class" class="sbu-in">' + opts + '</select>' +
|
||||
'<div style="font-size:.78rem;color:var(--text-3)">Ученики класса получат уведомление со ссылкой. Симуляция будет автоматически опубликована.</div>' +
|
||||
'</div>';
|
||||
var m = global.LS.modal({ title: 'Раздать классу', content: content, size: 'sm', actions: [
|
||||
{ label: 'Отмена', onClick: function () { m.close(); } },
|
||||
{ label: 'Раздать', primary: true, onClick: function () {
|
||||
var sel = m.body.querySelector('#sbu-share-class');
|
||||
var classId = sel ? Number(sel.value) : NaN;
|
||||
global.LS.customSimShare(self.simId, { classId: classId }).then(function (r) {
|
||||
m.close();
|
||||
self.status = 'published';
|
||||
self.renderToolbar();
|
||||
global.LS.toast('Отправлено ученикам: ' + ((r && r.sent) || 0), 'success');
|
||||
}).catch(function (e) {
|
||||
global.LS.toast((e && e.message) || 'Ошибка раздачи', 'error');
|
||||
});
|
||||
} }
|
||||
] });
|
||||
}).catch(function () { global.LS.toast('Не удалось загрузить классы', 'error'); });
|
||||
};
|
||||
|
||||
/* Старт из шаблона: выбор готовой спеки -> загрузка в редактор как НОВАЯ
|
||||
симуляция (simId сбрасывается, чтобы первое «Сохранить» создало запись). */
|
||||
Builder.prototype.openTemplateModal = function () {
|
||||
var self = this;
|
||||
var cards = TEMPLATES.map(function (tpl, i) {
|
||||
return '<button type="button" data-tpl="' + i + '" style="text-align:left;display:flex;flex-direction:column;gap:4px;padding:11px 13px;border:1px solid var(--border);border-radius:10px;background:#fff;cursor:pointer">' +
|
||||
'<span style="font-weight:800;font-size:.84rem;color:var(--text)">' + esc(tpl.name) + '</span>' +
|
||||
'<span style="font-size:.74rem;color:var(--text-3)">' + esc(tpl.desc) + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
var content = '<div style="display:flex;flex-direction:column;gap:8px">' + cards +
|
||||
'<div style="font-size:.74rem;color:var(--text-3);margin-top:4px">Шаблон заменит текущую сцену и создаст новую симуляцию.</div></div>';
|
||||
var m = global.LS.modal({ title: 'Создать из шаблона', content: content, size: 'sm', actions: [
|
||||
{ label: 'Закрыть', onClick: function () { m.close(); } }
|
||||
] });
|
||||
m.body.querySelectorAll('[data-tpl]').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var tpl = TEMPLATES[Number(b.getAttribute('data-tpl'))];
|
||||
if (!tpl) return;
|
||||
var apply = function () {
|
||||
self.simId = null;
|
||||
try { global.history.replaceState({}, '', '/sim-builder'); } catch (e) {}
|
||||
// loadFromSim ждёт sim-объект; собираем синтетический из спеки шаблона.
|
||||
var spec = JSON.parse(JSON.stringify(tpl.spec));
|
||||
self.loadFromSim({
|
||||
id: null, status: 'draft', version: 1,
|
||||
title: (spec.meta && spec.meta.title) || tpl.name,
|
||||
description: (spec.meta && spec.meta.desc) || '',
|
||||
subject: spec.subject || '', grade: spec.grade != null ? spec.grade : '',
|
||||
cat: tpl.cat || spec.cat || '', spec: spec
|
||||
});
|
||||
m.close();
|
||||
global.LS.toast('Шаблон загружен', 'success');
|
||||
};
|
||||
var hasContent = self.st.params.length || self.st.objects.length || self.st.plots.length;
|
||||
if (hasContent && !global.confirm('Заменить текущую сцену шаблоном «' + tpl.name + '»?')) return;
|
||||
apply();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ════════════════════════ ВАЛИДАЦИЯ (клиент, до запроса) ════════════════════════ */
|
||||
@@ -1100,9 +1200,76 @@
|
||||
trash: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.1"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
||||
chev: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4"><polyline points="6 9 12 15 18 9"/></svg>',
|
||||
target: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.1"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5"/><line x1="12" y1="1" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="1" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="23" y2="12"/></svg>',
|
||||
cog: '<svg viewBox="0 0 24 24" width="13" height="13" style="vertical-align:-2px" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>'
|
||||
cog: '<svg viewBox="0 0 24 24" width="13" height="13" style="vertical-align:-2px" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
||||
template: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.1"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>',
|
||||
unpublish: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.1"><path d="M3 3l18 18"/><path d="M10.5 5.1A15.3 15.3 0 0 1 12 5a15.3 15.3 0 0 1 4 7M6.3 6.3A15.3 15.3 0 0 0 12 19a15.3 15.3 0 0 0 3-4"/><path d="M3 12h7m5 0h6"/></svg>'
|
||||
};
|
||||
|
||||
/* ── Встроенные шаблоны стартовых спек (Фаза 6) ──────────────────────────
|
||||
Данные, не код. Каждый — полноценная валидная спека v1: «Создать из шаблона»
|
||||
загружает её через loadFromSim как новую симуляцию. */
|
||||
var TEMPLATES = [
|
||||
{
|
||||
name: 'Пустая сцена', cat: 'phys',
|
||||
desc: 'Чистый холст с осями и сеткой — начать с нуля.',
|
||||
spec: {
|
||||
specVersion: 1, meta: { title: 'Новая симуляция', desc: '' },
|
||||
viewport: { xmin: -1, xmax: 10, ymin: -1, ymax: 10, grid: true, axes: true },
|
||||
time: { autoplay: false, loop: true, speed: 1 }, params: [], objects: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Математический маятник', cat: 'phys',
|
||||
desc: 'Груз на нити: угол колеблется по гармоническому закону.',
|
||||
spec: {
|
||||
specVersion: 1, meta: { title: 'Маятник', desc: 'Колебания груза на нити' },
|
||||
viewport: { xmin: -3, xmax: 3, ymin: -3.4, ymax: 0.6, grid: true, axes: true },
|
||||
time: { autoplay: true, loop: true, speed: 1 },
|
||||
params: [
|
||||
{ name: 'L', label: 'Длина нити', min: 0.5, max: 3, step: 0.1, value: 2.4, unit: 'м' },
|
||||
{ name: 'A', label: 'Амплитуда', min: 0.1, max: 1, step: 0.05, value: 0.5, unit: 'рад' }
|
||||
],
|
||||
objects: [
|
||||
{ type: 'segment', x1: 0, y1: 0, x2: 'L*sin(A*cos(2.2*t))', y2: '-L*cos(A*cos(2.2*t))', color: '#94a3b8', width: 2 },
|
||||
{ id: 'bob', type: 'circle', x: 'L*sin(A*cos(2.2*t))', y: '-L*cos(A*cos(2.2*t))', r: 0.18, color: '#9B5DE5' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'График y = f(x)', cat: 'math',
|
||||
desc: 'Параметрический график функции с настраиваемыми коэффициентами.',
|
||||
spec: {
|
||||
specVersion: 1, meta: { title: 'График функции', desc: 'y = a*sin(b*x)' },
|
||||
viewport: { xmin: -6.5, xmax: 6.5, ymin: -3.5, ymax: 3.5, grid: true, axes: true },
|
||||
time: { autoplay: false, loop: true, speed: 1 },
|
||||
params: [
|
||||
{ name: 'a', label: 'Амплитуда a', min: -3, max: 3, step: 0.1, value: 2 },
|
||||
{ name: 'b', label: 'Частота b', min: 0.2, max: 4, step: 0.1, value: 1 }
|
||||
],
|
||||
objects: [
|
||||
{ type: 'plot', expr: 'a*sin(b*x)', var: 'x', range: [-6.5, 6.5], samples: 200, color: '#06D6E0', width: 2 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Бросок тела', cat: 'phys',
|
||||
desc: 'Траектория тела под углом к горизонту (кинематика).',
|
||||
spec: {
|
||||
specVersion: 1, meta: { title: 'Бросок тела', desc: 'Движение в поле тяжести' },
|
||||
viewport: { xmin: -1, xmax: 22, ymin: -1, ymax: 12, grid: true, axes: true },
|
||||
time: { autoplay: true, loop: true, speed: 1 },
|
||||
params: [
|
||||
{ name: 'v', label: 'Скорость', min: 5, max: 25, step: 0.5, value: 16, unit: 'м/с' },
|
||||
{ name: 'ang', label: 'Угол', min: 10, max: 80, step: 1, value: 45, unit: 'град' }
|
||||
],
|
||||
objects: [
|
||||
{ id: 'b', type: 'circle', x: 'v*cos(ang*pi/180)*t', y: 'v*sin(ang*pi/180)*t - 4.9*t*t', r: 0.25, color: '#9B5DE5' },
|
||||
{ type: 'plot', expr: '(x*tan(ang*pi/180)) - (4.9*x*x)/((v*cos(ang*pi/180))^2)', var: 'x', range: [0, 22], samples: 150, color: 'rgba(6,214,224,0.5)', width: 1.5 }
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/* plot-объект сериализуется особым путём (range_a/range_b -> range, без UI-полей);
|
||||
все прочие типы — через _stripObjOrig (удаление _uid и пустых полей). */
|
||||
var _stripObjOrig = stripObj;
|
||||
|
||||
Reference in New Issue
Block a user