fix(rbxl-import): UDim2 viewport-relative + rbxasset URL filter
All checks were successful
CI / Lint (pull_request) Successful in 1m5s
CI / Build (pull_request) Successful in 2m1s
CI / Secret scan (pull_request) Successful in 27s
CI / PR size check (pull_request) Successful in 11s
CI / Deploy to S1 + S2 (pull_request) Has been skipped

- UDim2: scale теперь умножается на viewport reference (1280×720),
  раньше игнорировался — фреймы получали 0×0 и фейлились на дефолт 100×30
  или наоборот заполняли всё окно
- _udim2_pair(): пара (x,y) через _udim2_to_rublox(axis='x'|'y')
- Фильтр rbxasset:// rbxassetid:// rbxhttp:// rbxthumb:// URL'ов на пустую
  строку — браузер их не загружает, спам в console исчезает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-08 01:49:20 +03:00
parent 624bbc636b
commit cc6447b851
2 changed files with 30 additions and 22 deletions

View File

@ -725,22 +725,28 @@ class Converter:
return None # top-level в ScreenGui = parentId=None в Rublox return None # top-level в ScreenGui = parentId=None в Rublox
return f'rbx_gui_{parent_ref}' return f'rbx_gui_{parent_ref}'
def _udim2_to_rublox(self, udim2, default_scale=None) -> Tuple[float, float]: def _udim2_to_rublox(self, udim2, axis: str = 'x') -> int:
"""Roblox UDim2(scale, offset) → pixel x/y для Rublox GUI. """Roblox UDim2(scale, offset) → pixel размер для Rublox GUI.
Берём только offset (scale*screen игнорим: точную viewport-ширину не знаем Reference viewport: 1280×720 (стандарт Roblox при импорте).
в момент импорта). Если offset=0 а scale0 used 1280×720 как референс. axis='x' используется ширина 1280, 'y' высота 720.
""" """
if udim2 is None: if udim2 is None:
return 0
ref = 1280 if axis == 'x' else 720
if isinstance(udim2, dict):
scale = udim2.get('scale', 0) or 0
offset = udim2.get('offset', 0) or 0
return int(offset + scale * ref)
return 0
def _udim2_pair(self, udim2) -> Tuple[int, int]:
"""UDim2 = {x: UDim, y: UDim} → (px, py)."""
if not isinstance(udim2, dict):
return (0, 0) return (0, 0)
# Формат: UDim2 = {x: UDim(scale, offset), y: UDim(scale, offset)} return (
x_obj = udim2.get('x') if isinstance(udim2, dict) else None self._udim2_to_rublox(udim2.get('x'), 'x'),
y_obj = udim2.get('y') if isinstance(udim2, dict) else None self._udim2_to_rublox(udim2.get('y'), 'y'),
def resolve(u): )
if not u: return 0
scale = u.get('scale', 0) if isinstance(u, dict) else 0
offset = u.get('offset', 0) if isinstance(u, dict) else 0
return int(offset + scale * 1280)
return (resolve(x_obj), resolve(y_obj))
def _color3_to_hex(self, c3) -> str: def _color3_to_hex(self, c3) -> str:
if c3 is None: if c3 is None:
@ -770,12 +776,19 @@ class Converter:
else: else:
r_type = 'frame' r_type = 'frame'
pos_x, pos_y = self._udim2_to_rublox(props.get('Position')) pos_x, pos_y = self._udim2_pair(props.get('Position'))
size_x, size_y = self._udim2_to_rublox(props.get('Size')) size_x, size_y = self._udim2_pair(props.get('Size'))
# Size может быть 0×0 (если только scale) — дефолтим в 100×30 # Size может быть 0×0 (если только scale) — дефолтим в 100×30
if size_x <= 0: size_x = 100 if size_x <= 0: size_x = 100
if size_y <= 0: size_y = 30 if size_y <= 0: size_y = 30
# Фильтр Roblox CDN URL'ов: rbxasset://, rbxassetid://, rbxhttp:// —
# браузер их не поймёт, даём пустую строку. В будущем asset_downloader
# может подменить на cached_url.
raw_image = str(props.get('Image', '') or '')
if raw_image.startswith(('rbxasset://', 'rbxassetid://', 'rbxhttp://', 'rbxthumb://')):
raw_image = ''
element = { element = {
'id': f'rbx_gui_{inst.referent}', 'id': f'rbx_gui_{inst.referent}',
'type': r_type, 'type': r_type,
@ -797,7 +810,7 @@ class Converter:
'textSize': int(props.get('TextSize', 14) or 14), 'textSize': int(props.get('TextSize', 14) or 14),
'textAlign': 'center', 'textAlign': 'center',
'fontWeight': 700 if cls in ('TextButton',) else 500, 'fontWeight': 700 if cls in ('TextButton',) else 500,
'imageUrl': str(props.get('Image', '') or ''), 'imageUrl': raw_image,
'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(),

View File

@ -13,7 +13,7 @@ import { RobloxLuaSharedSandbox } from './RobloxLuaSharedSandbox.js';
/** /**
* Распаковывает Lua-исходник из поля code упакованного rbxl-importer'ом. * Распаковывает Lua-исходник из поля code упакованного rbxl-importer'ом.
* Формат: `// @roblox-lua\n// {meta json}\n/* lua_source:\n<код>\n*/` * Формат: "// @roblox-lua\\n// {meta json}\\n/[*] lua_source:\\n<код>\\n[*]/"
*/ */
export function unpackRobloxLuaCode(code) { export function unpackRobloxLuaCode(code) {
const openTag = '/* lua_source:\n'; const openTag = '/* lua_source:\n';
@ -153,8 +153,3 @@ export function handleLuaCommand(_scriptId, cmd, payload, runtime) {
} }
} }
/* ──────── Legacy single-script API (для обратной совместимости) ──────── */
// Старая логика per-script Worker оставлена в RobloxLuaSandbox.js + RobloxLuaWorker.js,
// но GameRuntime теперь использует startRobloxLuaShared. Эти экспорты не удалены
// чтобы тесты в player/tests/ продолжали работать.
export { startRobloxLuaShared as startRobloxLuaScript };