feat(opticsbench): конструктор Фаза 4 — новые источники/элементы + улучшения
Источники: одиночный луч и лазер (узкий пучок) + угол прицеливания (point/single/laser/parallel наклоняются по ang). Новые элементы: - граница сред: Снеллиус на вертикальной плоскости + полное внутр. отражение (проверено: 30°→19.47°, ПВО при 50°) - стеклянная пластина: параллельный сдвиг (преломление вход/выход) Улучшения: - отсечение апертурой (лучи вне линзы/зеркала поглощаются — виньетирование) - метки F и 2F у собирающей линзы - числовые значения у слайдеров инспектора (без пересборки панели) bump opticsbench.js?v=5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+120
-50
@@ -2544,8 +2544,8 @@ class BenchSim {
|
||||
this.onUpdate = null;
|
||||
this._drag = null;
|
||||
this._nextId = 1;
|
||||
// source: object arrow by default
|
||||
this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9 };
|
||||
// source: object arrow by default. `ang` (deg) aims point/single/laser/parallel.
|
||||
this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9, ang: 0 };
|
||||
// elements along the bench, positioned by x-fraction; centred on the axis
|
||||
this.elements = [
|
||||
this._mk('lens', { xf: 0.40, f: 130, ap: 95 }),
|
||||
@@ -2565,6 +2565,8 @@ class BenchSim {
|
||||
if (type === 'aperture') return { ...base, gap: p.gap != null ? p.gap : 40 };
|
||||
if (type === 'screen') return { ...base };
|
||||
if (type === 'prism') return { ...base, apex: p.apex != null ? p.apex : 50, n: p.n != null ? p.n : 1.52, size: p.size || 90 };
|
||||
if (type === 'interface') return { ...base, n1: p.n1 != null ? p.n1 : 1, n2: p.n2 != null ? p.n2 : 1.5, ap: p.ap || 110 };
|
||||
if (type === 'slab') return { ...base, n: p.n != null ? p.n : 1.5, t: p.t != null ? p.t : 60, ap: p.ap || 95 };
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -2621,15 +2623,26 @@ class BenchSim {
|
||||
const push = (x, y, ang) => {
|
||||
for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, pts: [{ x, y }], alive: true, bounces: 0 });
|
||||
};
|
||||
if (this.source.kind === 'parallel') {
|
||||
const n = 9, hh = 90;
|
||||
const aim = (this.source.ang || 0) * Math.PI / 180;
|
||||
if (this.source.kind === 'single') {
|
||||
push(sx, ay, aim); // one aimable ray
|
||||
} else if (this.source.kind === 'laser') {
|
||||
const n = 3, hh = 7; // narrow collimated beam
|
||||
const px = -Math.sin(aim), py = Math.cos(aim); // perpendicular to aim
|
||||
for (let i = 0; i < n; i++) {
|
||||
const y = ay - hh + (2 * hh) * (i / (n - 1));
|
||||
push(sx, y, 0);
|
||||
const o = (i - (n - 1) / 2) * hh;
|
||||
push(sx + px * o, ay + py * o, aim);
|
||||
}
|
||||
} else if (this.source.kind === 'parallel') {
|
||||
const n = 9, hh = 90;
|
||||
const px = -Math.sin(aim), py = Math.cos(aim);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const o = -hh + (2 * hh) * (i / (n - 1));
|
||||
push(sx + px * o, ay + py * o, aim);
|
||||
}
|
||||
} else if (this.source.kind === 'point') {
|
||||
const n = this.source.rays, A = this.source.spread;
|
||||
for (let i = 0; i < n; i++) push(sx, ay, -A + 2 * A * (i / (n - 1)));
|
||||
for (let i = 0; i < n; i++) push(sx, ay, aim - A + 2 * A * (i / (n - 1)));
|
||||
} else { // object arrow: fan from tip and base
|
||||
const n = this.source.rays, A = this.source.spread;
|
||||
[ay - this.source.h, ay].forEach(y0 => {
|
||||
@@ -2682,14 +2695,38 @@ class BenchSim {
|
||||
_interact(ray, el, yRel) {
|
||||
const norm = (x, y) => { const l = Math.hypot(x, y) || 1; ray.dx = x / l; ray.dy = y / l; };
|
||||
if (el.type === 'lens') {
|
||||
if (Math.abs(yRel) > el.ap) return false; // outside aperture → pass
|
||||
if (Math.abs(yRel) > el.ap) { ray.alive = false; return true; } // mount blocks outside aperture
|
||||
const sgn = Math.sign(ray.dx) || 1;
|
||||
const w = ray.dy / Math.abs(ray.dx);
|
||||
norm(sgn, w - yRel / el.f);
|
||||
return true;
|
||||
}
|
||||
if (el.type === 'mirror') {
|
||||
if (el.type === 'interface') {
|
||||
if (Math.abs(yRel) > el.ap) return false;
|
||||
// Snell at a vertical plane (normal = x). Tangential (y) component scales by n_i/n_t.
|
||||
const goingRight = ray.dx > 0;
|
||||
const ni = goingRight ? el.n1 : el.n2;
|
||||
const nt = goingRight ? el.n2 : el.n1;
|
||||
const dyT = (ni / nt) * ray.dy; // sinθ_t (tangential preserved)
|
||||
if (Math.abs(dyT) >= 1) { ray.dx = -ray.dx; ray.bounces++; return true; } // total internal reflection
|
||||
const sgn = Math.sign(ray.dx) || 1;
|
||||
ray.dy = dyT; ray.dx = sgn * Math.sqrt(Math.max(0, 1 - dyT * dyT));
|
||||
return true;
|
||||
}
|
||||
if (el.type === 'slab') {
|
||||
if (Math.abs(yRel) > el.ap) return false;
|
||||
// parallel plate: ray exits parallel but laterally shifted (refract in, travel t, refract out)
|
||||
const sinI = ray.dy; // |d|=1 → y-comp is sinθ from axis
|
||||
const sinT = sinI / el.n;
|
||||
const tanT = sinT / Math.sqrt(Math.max(1e-6, 1 - sinT * sinT));
|
||||
const sgn = Math.sign(ray.dx) || 1;
|
||||
ray.pts.push({ x: ray.x, y: ray.y }); // entry on the front face
|
||||
ray.x += sgn * el.t; // emerge on the far face
|
||||
ray.y += tanT * el.t; // inside-the-glass vertical travel
|
||||
return true; // direction unchanged (parallel faces); tracer pushes exit
|
||||
}
|
||||
if (el.type === 'mirror') {
|
||||
if (Math.abs(yRel) > el.ap) { ray.alive = false; return true; }
|
||||
ray.dx = -ray.dx; // reflect about the vertical plane
|
||||
ray.bounces++;
|
||||
if (el.kind !== 'plane') {
|
||||
@@ -2784,15 +2821,22 @@ class BenchSim {
|
||||
|
||||
_drawSource(ctx, ay) {
|
||||
const sx = this.source.xf * this.W;
|
||||
const aim = (this.source.ang || 0) * Math.PI / 180;
|
||||
ctx.save();
|
||||
if (this.source.kind === 'object') {
|
||||
this._arrow(ctx, sx, ay, sx, ay - this.source.h, '#9B5DE5');
|
||||
} else {
|
||||
ctx.fillStyle = this.source.kind === 'point' ? '#FFD166' : '#9B5DE5';
|
||||
ctx.beginPath(); ctx.arc(sx, ay, 5, 0, Math.PI * 2); ctx.fill();
|
||||
if (this.source.kind === 'parallel') {
|
||||
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.moveTo(sx, ay - 90); ctx.lineTo(sx, ay + 90); ctx.stroke();
|
||||
const isLaser = this.source.kind === 'laser', isSingle = this.source.kind === 'single';
|
||||
ctx.fillStyle = (this.source.kind === 'point' || isSingle) ? '#FFD166' : '#9B5DE5';
|
||||
ctx.beginPath(); ctx.arc(sx, ay, isLaser ? 4 : 5, 0, Math.PI * 2); ctx.fill();
|
||||
if (this.source.kind === 'parallel' || isLaser) {
|
||||
const px = -Math.sin(aim), py = Math.cos(aim), hh = isLaser ? 10 : 90;
|
||||
ctx.strokeStyle = isLaser ? '#FF5B5B' : '#9B5DE5'; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.moveTo(sx - px * hh, ay - py * hh); ctx.lineTo(sx + px * hh, ay + py * hh); ctx.stroke();
|
||||
}
|
||||
// aim arrow for single / laser / point
|
||||
if (isSingle || isLaser || this.source.kind === 'point') {
|
||||
this._arrow(ctx, sx, ay, sx + 22 * Math.cos(aim), ay + 22 * Math.sin(aim), isLaser ? '#FF5B5B' : '#FFD166');
|
||||
}
|
||||
}
|
||||
const sel = this.selectedId === '__src';
|
||||
@@ -2816,7 +2860,26 @@ class BenchSim {
|
||||
[[ay - el.ap, 1], [ay + el.ap, -1]].forEach(([yy, s]) => {
|
||||
ctx.beginPath(); ctx.moveTo(x, yy); ctx.lineTo(x - tip, yy + s * 7); ctx.moveTo(x, yy); ctx.lineTo(x + tip, yy + s * 7); ctx.stroke();
|
||||
});
|
||||
// focal markers F and 2F on both sides of the lens
|
||||
if (conv) {
|
||||
ctx.fillStyle = 'rgba(6,214,224,0.6)';
|
||||
[el.f, -el.f, 2 * el.f, -2 * el.f].forEach((d, i) => {
|
||||
const fx = x + d; if (fx < 6 || fx > this.W - 6) return;
|
||||
ctx.beginPath(); ctx.arc(fx, ay, 3, 0, Math.PI * 2); ctx.fill();
|
||||
this._elLabel(ctx, fx, ay - 16, i < 2 ? 'F' : '2F');
|
||||
});
|
||||
}
|
||||
this._elLabel(ctx, x, ay + el.ap + 14, (conv ? 'линза +' : 'линза −') + Math.abs(el.f).toFixed(0));
|
||||
} else if (el.type === 'interface') {
|
||||
ctx.fillStyle = 'rgba(96,165,250,0.10)'; ctx.fillRect(x, ay - el.ap, this.W - x, 2 * el.ap);
|
||||
ctx.strokeStyle = sel ? '#fff' : '#60a5fa';
|
||||
ctx.beginPath(); ctx.moveTo(x, ay - el.ap); ctx.lineTo(x, ay + el.ap); ctx.stroke();
|
||||
this._elLabel(ctx, x, ay + el.ap + 14, 'граница ' + el.n1.toFixed(2) + ' | ' + el.n2.toFixed(2));
|
||||
} else if (el.type === 'slab') {
|
||||
ctx.fillStyle = 'rgba(123,245,164,0.10)'; ctx.fillRect(x, ay - el.ap, el.t, 2 * el.ap);
|
||||
ctx.strokeStyle = sel ? '#fff' : '#7BF5A4';
|
||||
ctx.strokeRect(x, ay - el.ap, el.t, 2 * el.ap);
|
||||
this._elLabel(ctx, x + el.t / 2, ay + el.ap + 14, 'пластина n=' + el.n.toFixed(2));
|
||||
} else if (el.type === 'mirror') {
|
||||
ctx.strokeStyle = sel ? '#fff' : '#A8E063';
|
||||
ctx.beginPath();
|
||||
@@ -4350,65 +4413,72 @@ function _freeUpdateUI(chain) {
|
||||
|
||||
/* ── Optical bench constructor (BenchSim) UI ── */
|
||||
function _benchElName(e) {
|
||||
if (e.type === 'lens') return (e.f >= 0 ? 'Линза +' : 'Линза −') + Math.abs(e.f).toFixed(0);
|
||||
if (e.type === 'mirror') return 'Зеркало ' + ({ plane: 'плоск', concave: 'вогн', convex: 'выпукл' }[e.kind] || '');
|
||||
if (e.type === 'aperture') return 'Диафрагма';
|
||||
if (e.type === 'screen') return 'Экран';
|
||||
if (e.type === 'prism') return 'Призма';
|
||||
if (e.type === 'lens') return (e.f >= 0 ? 'Линза +' : 'Линза −') + Math.abs(e.f).toFixed(0);
|
||||
if (e.type === 'mirror') return 'Зеркало ' + ({ plane: 'плоск', concave: 'вогн', convex: 'выпукл' }[e.kind] || '');
|
||||
if (e.type === 'aperture') return 'Диафрагма';
|
||||
if (e.type === 'screen') return 'Экран';
|
||||
if (e.type === 'prism') return 'Призма';
|
||||
if (e.type === 'interface') return 'Граница сред';
|
||||
if (e.type === 'slab') return 'Пластина';
|
||||
return e.type;
|
||||
}
|
||||
function _benchRow(label, html) {
|
||||
return '<div class="proj-slider-row" style="margin-bottom:6px"><label style="font-size:.74rem;color:#ccc;width:78px">' + label + '</label>' + html + '</div>';
|
||||
// Labelled slider with a live numeric value (no panel rebuild → drag stays smooth).
|
||||
function _benchCtl(label, id, key, min, max, step, val, isSource) {
|
||||
const vid = 'bv_' + (isSource ? 's' : id) + '_' + key;
|
||||
const call = isSource ? "benchSourceParam('" + key + "',this.value)" : "benchUpdate(" + id + ",'" + key + "',this.value)";
|
||||
return '<div class="proj-slider-row" style="margin-bottom:6px">' +
|
||||
'<label style="font-size:.72rem;color:#ccc;width:104px">' + label + ' <b id="' + vid + '" style="color:var(--cyan)">' + val + '</b></label>' +
|
||||
'<input type="range" min="' + min + '" max="' + max + '" step="' + step + '" value="' + val +
|
||||
'" oninput="' + call + ';var b=document.getElementById(\'' + vid + '\');if(b)b.textContent=this.value" style="flex:1"></div>';
|
||||
}
|
||||
function _benchSlider(id, key, min, max, step, val) {
|
||||
return '<input type="range" min="' + min + '" max="' + max + '" step="' + step + '" value="' + val +
|
||||
'" oninput="benchUpdate(' + id + ',\'' + key + '\',this.value)" style="flex:1">';
|
||||
function _benchBtnRow(opts, isActive, onClick) {
|
||||
return '<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:6px">' +
|
||||
opts.map(o => { const [k, lbl] = o.split(':');
|
||||
return '<button class="preset-btn' + (isActive(k) ? ' active' : '') + '" style="flex:1;font-size:.66rem" onclick="' + onClick(k) + '">' + lbl + '</button>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
function _benchPropsHTML() {
|
||||
if (!benchSim) return '';
|
||||
const sel = benchSim.selectedId;
|
||||
if (sel === '__src') {
|
||||
if (benchSim.selectedId === '__src') {
|
||||
const s = benchSim.source;
|
||||
let h = '<div class="gp-section-title" style="margin:4px 0 6px">Источник</div>';
|
||||
h += '<div style="display:flex;gap:3px;margin-bottom:6px">' +
|
||||
['object:Предмет', 'point:Точка', 'parallel:Параллель'].map(o => {
|
||||
const [k, lbl] = o.split(':');
|
||||
return '<button class="preset-btn' + (s.kind === k ? ' active' : '') + '" style="flex:1;font-size:.68rem" onclick="benchSourceKind(\'' + k + '\')">' + lbl + '</button>';
|
||||
}).join('') + '</div>';
|
||||
if (s.kind === 'object') h += _benchRow('Высота', _benchSourceSlider('h', 20, 120, 2, s.h));
|
||||
if (s.kind !== 'parallel') h += _benchRow('Раствор', _benchSourceSlider('spread', 0.1, 0.6, 0.02, s.spread));
|
||||
h += _benchBtnRow(['object:Предмет', 'point:Точка', 'parallel:Параллель', 'single:Луч', 'laser:Лазер'],
|
||||
k => s.kind === k, k => "benchSourceKind('" + k + "')");
|
||||
if (s.kind === 'object') h += _benchCtl('Высота', 0, 'h', 20, 120, 2, s.h, true);
|
||||
if (s.kind === 'point') h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true);
|
||||
if (s.kind !== 'object' && s.kind !== 'parallel') {} // no spread for single/laser
|
||||
if (s.kind !== 'object') h += _benchCtl('Угол°', 0, 'ang', -60, 60, 1, s.ang || 0, true);
|
||||
return h;
|
||||
}
|
||||
const e = benchSim.getSelected();
|
||||
if (!e) return '<div class="pp-hint">Выберите элемент или источник (клик по схеме)</div>';
|
||||
let h = '<div class="gp-section-title" style="margin:4px 0 6px">' + _benchElName(e) + '</div>';
|
||||
if (e.type === 'lens') {
|
||||
h += _benchRow('f, px', _benchSlider(e.id, 'f', -300, 300, 5, e.f));
|
||||
h += _benchRow('Апертура', _benchSlider(e.id, 'ap', 30, 130, 5, e.ap));
|
||||
h += _benchCtl('f, px', e.id, 'f', -300, 300, 5, e.f);
|
||||
h += _benchCtl('Апертура', e.id, 'ap', 30, 130, 5, e.ap);
|
||||
} else if (e.type === 'mirror') {
|
||||
h += '<div style="display:flex;gap:3px;margin-bottom:6px">' +
|
||||
['plane:Плоск', 'concave:Вогн', 'convex:Выпукл'].map(o => {
|
||||
const [k, lbl] = o.split(':');
|
||||
return '<button class="preset-btn' + (e.kind === k ? ' active' : '') + '" style="flex:1;font-size:.68rem" onclick="benchUpdate(' + e.id + ',\'kind\',\'' + k + '\');_benchUpdateUI()">' + lbl + '</button>';
|
||||
}).join('') + '</div>';
|
||||
if (e.kind !== 'plane') h += _benchRow('R, px', _benchSlider(e.id, 'R', 100, 600, 10, e.R));
|
||||
h += _benchRow('Апертура', _benchSlider(e.id, 'ap', 30, 130, 5, e.ap));
|
||||
h += _benchBtnRow(['plane:Плоск', 'concave:Вогн', 'convex:Выпукл'],
|
||||
k => e.kind === k, k => "benchUpdate(" + e.id + ",'kind','" + k + "');_benchUpdateUI()");
|
||||
if (e.kind !== 'plane') h += _benchCtl('R, px', e.id, 'R', 100, 600, 10, e.R);
|
||||
h += _benchCtl('Апертура', e.id, 'ap', 30, 130, 5, e.ap);
|
||||
} else if (e.type === 'aperture') {
|
||||
h += _benchRow('Зазор', _benchSlider(e.id, 'gap', 5, 110, 2, e.gap));
|
||||
h += _benchCtl('Зазор', e.id, 'gap', 5, 110, 2, e.gap);
|
||||
} else if (e.type === 'prism') {
|
||||
h += _benchRow('Угол', _benchSlider(e.id, 'apex', 20, 70, 1, e.apex));
|
||||
h += _benchRow('n', _benchSlider(e.id, 'n', 1.3, 1.9, 0.01, e.n));
|
||||
h += _benchRow('Размер', _benchSlider(e.id, 'size', 50, 130, 5, e.size));
|
||||
h += _benchCtl('Угол', e.id, 'apex', 20, 70, 1, e.apex);
|
||||
h += _benchCtl('n', e.id, 'n', 1.3, 1.9, 0.01, e.n);
|
||||
h += _benchCtl('Размер', e.id, 'size', 50, 130, 5, e.size);
|
||||
} else if (e.type === 'interface') {
|
||||
h += _benchCtl('n слева', e.id, 'n1', 1.0, 2.4, 0.01, e.n1);
|
||||
h += _benchCtl('n справа', e.id, 'n2', 1.0, 2.4, 0.01, e.n2);
|
||||
} else if (e.type === 'slab') {
|
||||
h += _benchCtl('n', e.id, 'n', 1.1, 2.0, 0.01, e.n);
|
||||
h += _benchCtl('Толщина', e.id, 't', 20, 140, 5, e.t);
|
||||
} else if (e.type === 'screen') {
|
||||
h += '<div class="pp-hint">Экран ловит изображение.</div>';
|
||||
}
|
||||
h += '<button class="preset-btn" style="width:100%;margin-top:6px;color:#EF476F" onclick="benchRemove(' + e.id + ')">Удалить элемент</button>';
|
||||
return h;
|
||||
}
|
||||
function _benchSourceSlider(key, min, max, step, val) {
|
||||
return '<input type="range" min="' + min + '" max="' + max + '" step="' + step + '" value="' + val +
|
||||
'" oninput="benchSourceParam(\'' + key + '\',this.value)" style="flex:1">';
|
||||
}
|
||||
function _benchUpdateUI() {
|
||||
if (!benchSim) return;
|
||||
const listEl = document.getElementById('bench-list');
|
||||
|
||||
+3
-1
@@ -3194,6 +3194,8 @@
|
||||
<button class="preset-btn" style="font-size:.68rem" onclick="benchAdd('aperture')">+ Диафрагма</button>
|
||||
<button class="preset-btn" style="font-size:.68rem" onclick="benchAdd('screen')">+ Экран</button>
|
||||
<button class="preset-btn" style="font-size:.68rem" onclick="benchAdd('prism')">+ Призма</button>
|
||||
<button class="preset-btn" style="font-size:.68rem" onclick="benchAdd('interface')">+ Граница</button>
|
||||
<button class="preset-btn" style="font-size:.68rem" onclick="benchAdd('slab')">+ Пластина</button>
|
||||
</div>
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Схема</div>
|
||||
<div id="bench-list" style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:8px"></div>
|
||||
@@ -4844,7 +4846,7 @@
|
||||
<script src="/js/labs/graphtransform.js"></script>
|
||||
<script src="/js/labs/pendulum.js"></script>
|
||||
<script src="/js/labs/equilibrium.js"></script>
|
||||
<script src="/js/labs/opticsbench.js?v=4"></script>
|
||||
<script src="/js/labs/opticsbench.js?v=5"></script>
|
||||
<script src="/js/labs/isoprocess.js"></script>
|
||||
<script src="/js/labs/titration.js"></script>
|
||||
<script src="/js/labs/probability.js"></script>
|
||||
|
||||
@@ -39,7 +39,14 @@
|
||||
- [x] 3.2 Пресеты систем (микроскоп/телескоп/проектор/зеркальная) — Фаза 1; экспорт PNG (`benchExportPng`, кнопка «Снимок PNG»).
|
||||
- [x] 3.3 Экран ловит изображение: светящиеся пятна (additive `lighter`) в точках попадания лучей, по λ — видно формирование изображения и спектр.
|
||||
|
||||
Бэклог: точная двухгранная призма (Снеллиус на гранях вместо тонкопризменного), апертурное отсечение лучей вне линзы (сейчас проходят прямо), профиль интенсивности на экране, поворот элементов, удаление legacy `FreeBuildSim`.
|
||||
### Фаза 4 — Контент и улучшения — [x]
|
||||
- [x] Источники: **одиночный луч** и **лазер** (узкий пучок) + **угол прицеливания** (point/single/laser/parallel наклоняются на `ang`).
|
||||
- [x] Новые элементы: **граница сред** (Снеллиус на вертикальной плоскости + ПВО — проверено: 30°→19.47°, ПВО при 50°) и **стеклянная пластина** (параллельный сдвиг, преломление на входе/выходе).
|
||||
- [x] Отсечение апертурой: лучи вне апертуры линзы/зеркала поглощаются (видно виньетирование, размер апертуры значим).
|
||||
- [x] Метки **F и 2F** у собирающей линзы.
|
||||
- [x] Числовые значения у слайдеров инспектора (живое обновление без пересборки панели).
|
||||
|
||||
Бэклог: точная двухгранная призма (Снеллиус на гранях вместо тонкопризменного); профиль интенсивности на экране; поворот/наклон элементов и 2D-перетаскивание (yf); делитель пучка (форк луча); удаление legacy `FreeBuildSim`.
|
||||
|
||||
---
|
||||
История: создан 2026-05-30.
|
||||
|
||||
Reference in New Issue
Block a user