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:
parent
a16c819726
commit
b09dd703af
Binary file not shown.
@ -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 к проекту.
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 < 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user