feat(rbxl): GUI mode + предупреждение про большие карты

Robloxity (20402 Part, 278 скриптов, 295 BillboardGui, 0.1 FPS) показал:
1. Большие карты могут зависнуть студию навсегда.
2. BillboardGui/SurfaceGui (вывески, табло) рендерятся в 3D-сцене и
   при 200+ штук убивают FPS.

Фиксы:

1. Предупреждение в модалке если parts > 5000 (жёлтое) или > 15000
   (красное "может зависнуть"). Подсказка про режимы.

2. Новая опция guiMode (показывается если GUI > 50 элементов):
   - 'all' — все, как было.
   - 'screen-only' (рекомендуется) — только ScreenGui HUD,
     BillboardGui/SurfaceGui удаляются.
   - 'skip' — без GUI совсем.

3. converter.py: маркирует элемент полем gui_container_kind:
   'screen' / 'billboard' / 'surface'.

4. app.py: _apply_gui_mode() фильтрует scene.gui[] по режиму.

Deploy app.py + converter.py на VM 130.

Robloxity рекомендуем импортировать со screen-only — Карта Robloxity
будет работать в 5-10× быстрее без вывесок города.
This commit is contained in:
min 2026-06-08 21:38:08 +03:00
parent a16c819726
commit b09dd703af
5 changed files with 109 additions and 1 deletions

View File

@ -216,6 +216,10 @@ def create():
scripts_mode = data.get('scripts_mode', 'disabled') scripts_mode = data.get('scripts_mode', 'disabled')
if scripts_mode not in ('disabled', 'enabled', 'skip'): if scripts_mode not in ('disabled', 'enabled', 'skip'):
scripts_mode = 'disabled' scripts_mode = 'disabled'
# gui_mode: 'all' / 'screen-only' (только ScreenGui-HUD) / 'skip' (без GUI)
gui_mode = data.get('gui_mode', 'all')
if gui_mode not in ('all', 'screen-only', 'skip'):
gui_mode = 'all'
if not preview_hash: if not preview_hash:
return jsonify({'error': 'preview_hash required'}), 400 return jsonify({'error': 'preview_hash required'}), 400
@ -284,6 +288,10 @@ def create():
# либо удаляем все скрипты полностью. # либо удаляем все скрипты полностью.
_apply_scripts_mode(project_data, scripts_mode) _apply_scripts_mode(project_data, scripts_mode)
# Применяем gui_mode: удаляем 3D-GUI (BillboardGui/SurfaceGui) или вообще
# всё, если выбрано 'skip'.
_apply_gui_mode(project_data, gui_mode)
# Создаём проект в kubikon3d_projects # Создаём проект в kubikon3d_projects
# Используем bridge на user-service / storys API, ИЛИ напрямую в storys_db. # Используем bridge на user-service / storys API, ИЛИ напрямую в storys_db.
# Прямой INSERT — проще для MVP. id автогенерируется. # Прямой INSERT — проще для MVP. id автогенерируется.
@ -345,6 +353,26 @@ def _resolve_asset_urls(project_data: dict, asset_map: dict) -> None:
snd['url'] = asset_map[rid] snd['url'] = asset_map[rid]
def _apply_gui_mode(project_data: dict, mode: str) -> None:
"""Фильтрует scene.gui[] по режиму.
'all' оставить всё (default).
'screen-only' оставить только ScreenGui-HUD, удалить billboard/surface.
Карты с 200+ BillboardGui (Robloxity) перестают тормозить.
'skip' удалить gui[] совсем.
"""
scene = project_data.get('scene', {})
if mode == 'skip':
scene['gui'] = []
return
if mode == 'screen-only':
gui = scene.get('gui', [])
scene['gui'] = [g for g in gui
if g.get('gui_container_kind', 'screen') == 'screen']
return
# 'all' — без изменений
def _apply_scripts_mode(project_data: dict, mode: str) -> None: def _apply_scripts_mode(project_data: dict, mode: str) -> None:
"""Применяет режим scripts_mode к проекту. """Применяет режим scripts_mode к проекту.

View File

@ -825,9 +825,13 @@ class Converter:
if not hasattr(self, '_screen_gui_refs'): if not hasattr(self, '_screen_gui_refs'):
self._screen_gui_refs = set() self._screen_gui_refs = set()
self._screen_gui_enabled = {} self._screen_gui_enabled = {}
self._screen_gui_kind = {} # ref → 'screen' | 'billboard' | 'surface'
self._screen_gui_refs.add(inst.referent) self._screen_gui_refs.add(inst.referent)
enabled = inst.properties.get('Enabled', True) enabled = inst.properties.get('Enabled', True)
self._screen_gui_enabled[inst.referent] = bool(enabled) if enabled is not None else True self._screen_gui_enabled[inst.referent] = bool(enabled) if enabled is not None else True
# Сохраняем тип контейнера — потом отфильтруем 3D-GUI если выбрано screen-only
kind = {'ScreenGui': 'screen', 'BillboardGui': 'billboard', 'SurfaceGui': 'surface'}.get(inst.class_name, 'screen')
self._screen_gui_kind[inst.referent] = kind
def _gui_parent_id(self, parent_ref) -> Optional[str]: def _gui_parent_id(self, parent_ref) -> Optional[str]:
if parent_ref is None: if parent_ref is None:
@ -921,12 +925,14 @@ class Converter:
# элемент тоже невидим. # элемент тоже невидим.
parent_ref = inst.parent_referent parent_ref = inst.parent_referent
screen_enabled = True screen_enabled = True
container_kind = 'screen' # default
if hasattr(self, '_screen_gui_refs'): if hasattr(self, '_screen_gui_refs'):
cur = parent_ref cur = parent_ref
depth = 0 depth = 0
while cur is not None and depth < 50: while cur is not None and depth < 50:
if cur in self._screen_gui_refs: if cur in self._screen_gui_refs:
screen_enabled = self._screen_gui_enabled.get(cur, True) screen_enabled = self._screen_gui_enabled.get(cur, True)
container_kind = self._screen_gui_kind.get(cur, 'screen')
break break
# Поиск родителя cur в instances (если есть) # Поиск родителя cur в instances (если есть)
cur_inst = self.model.by_referent.get(cur) if hasattr(self, 'model') else None cur_inst = self.model.by_referent.get(cur) if hasattr(self, 'model') else None
@ -979,6 +985,10 @@ class Converter:
'imageAsset': None, 'imageAsset': None,
'zIndex': int(props.get('ZIndex', 1) or 1), 'zIndex': int(props.get('ZIndex', 1) or 1),
'origin': 'roblox-' + cls.lower(), 'origin': 'roblox-' + cls.lower(),
# 'screen' — обычный HUD; 'billboard' — 3D-табличка над частью;
# 'surface' — на грани Part. Last 2 рендерятся в 3D-сцене и
# сильно тормозят если их сотни.
'gui_container_kind': container_kind,
} }
scene['gui'].append(element) scene['gui'].append(element)

View File

@ -63,6 +63,8 @@ export async function createRbxlProject(previewHash, title, opts = {}) {
// 'enabled' — попытаться запустить (может вешать карту) // 'enabled' — попытаться запустить (может вешать карту)
// 'skip' — не импортировать совсем // 'skip' — не импортировать совсем
scripts_mode: opts.scriptsMode || 'disabled', scripts_mode: opts.scriptsMode || 'disabled',
// 'all' (default) / 'screen-only' (только HUD) / 'skip' (без GUI)
gui_mode: opts.guiMode || 'all',
}), }),
}); });
if (!resp.ok) { if (!resp.ok) {

View File

@ -29,6 +29,10 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
// Режим скриптов: 'disabled' (импортнуть выключенными для чтения), // Режим скриптов: 'disabled' (импортнуть выключенными для чтения),
// 'enabled' (попытаться запустить может вешать карту), 'skip' (удалить). // 'enabled' (попытаться запустить может вешать карту), 'skip' (удалить).
const [scriptsMode, setScriptsMode] = useState('disabled'); const [scriptsMode, setScriptsMode] = useState('disabled');
// Режим GUI: 'all' все, 'screen-only' только ScreenGui (HUD),
// 'skip' не импортировать. Старые карты часто имеют 200+ BillboardGui
// (вывески города), что вешает рендер.
const [guiMode, setGuiMode] = useState('all');
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
if (!open) return null; if (!open) return null;
@ -49,6 +53,7 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
setFile(null); setReport(null); setPreviewHash(null); setFile(null); setReport(null); setPreviewHash(null);
setTitle(''); setError(null); setAnalyzing(false); setCreating(false); setTitle(''); setError(null); setAnalyzing(false); setCreating(false);
setScriptsMode('disabled'); setScriptsMode('disabled');
setGuiMode('all');
}; };
const handleClose = () => { reset(); onClose?.(); }; const handleClose = () => { reset(); onClose?.(); };
@ -92,7 +97,7 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
setCreating(true); setCreating(true);
setError(null); setError(null);
try { try {
const result = await createRbxlProject(previewHash, title, { scriptsMode }); const result = await createRbxlProject(previewHash, title, { scriptsMode, guiMode });
onCreated?.(result); onCreated?.(result);
handleClose(); handleClose();
// редирект на редактор // редирект на редактор
@ -179,6 +184,29 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
</tbody> </tbody>
</table> </table>
{report.primitives_created > 5000 && (
<div style={{
marginTop: 12, padding: 12,
background: report.primitives_created > 15000 ? '#5a1a1a' : '#4a3a1a',
borderRadius: 6,
border: '1px solid ' + (report.primitives_created > 15000 ? '#a55' : '#a85'),
}}>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>
{report.primitives_created > 15000
? '🛑 Очень большая карта'
: '⚠️ Большая карта'}
</div>
<div style={{ fontSize: 12, color: '#ddd' }}>
{report.primitives_created} Part'ов это много. Студия может
{report.primitives_created > 15000
? ' зависнуть или работать с FPS &lt; 1.'
: ' тормозить (FPS 10-30).'}
{' '}Рекомендуем выбрать ниже «Не импортировать скрипты»
чтобы хоть посмотреть геометрию.
</div>
</div>
)}
{report.top_classes?.length > 0 && ( {report.top_classes?.length > 0 && (
<details style={{ marginTop: 12 }}> <details style={{ marginTop: 12 }}>
<summary style={{ cursor: 'pointer' }}>Что внутри (топ-25 классов)</summary> <summary style={{ cursor: 'pointer' }}>Что внутри (топ-25 классов)</summary>
@ -262,6 +290,46 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
</div> </div>
)} )}
{(() => {
const guiCount = (report.top_classes || [])
.filter(c => /Gui|Frame|Label|Button|Image|Text/.test(c.class))
.reduce((s, c) => s + c.count, 0);
if (guiCount < 50) return null;
return (
<div style={{ marginTop: 16, padding: 12, background: '#1f1f1f', borderRadius: 6, border: '1px solid #333' }}>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>
Что делать с GUI ({guiCount}+ элементов)?
</div>
<div style={{ fontSize: 11, color: '#888', marginBottom: 8 }}>
В этой карте много GUI-элементов (BillboardGui вывески, табло).
Они сильно тормозят рендер если их сотни.
</div>
{['all', 'screen-only', 'skip'].map((m) => (
<label key={m} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '6px 0', cursor: 'pointer' }}>
<input
type="radio" name="guiMode" value={m}
checked={guiMode === m}
onChange={() => setGuiMode(m)}
style={{ marginTop: 3 }}
/>
<div>
<div style={{ fontSize: 13 }}>
{m === 'all' && 'Все GUI'}
{m === 'screen-only' && (<>Только <b>ScreenGui</b> (рекомендуется)</>)}
{m === 'skip' && 'Без GUI вообще'}
</div>
<div style={{ fontSize: 11, color: '#888' }}>
{m === 'all' && 'Может тормозить.'}
{m === 'screen-only' && 'HUD остаётся, BillboardGui/SurfaceGui (3D-вывески) удаляются.'}
{m === 'skip' && 'Самый быстрый рендер. Только геометрия мира.'}
</div>
</div>
</label>
))}
</div>
);
})()}
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<label style={{ display: 'block', marginBottom: 6 }}>Название игры:</label> <label style={{ display: 'block', marginBottom: 6 }}>Название игры:</label>
<input <input