@clovis500c/figma-bridge 1.13.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/server.js +569 -147
- package/package.json +1 -1
- package/plugin/code.js +418 -72
- package/plugin/ui.html +11 -2
package/dist/server.js
CHANGED
|
@@ -214,7 +214,7 @@ var init_setup = __esm(() => {
|
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
// package.json
|
|
217
|
-
var version = "1.
|
|
217
|
+
var version = "1.14.2";
|
|
218
218
|
var init_package = () => {};
|
|
219
219
|
|
|
220
220
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -27388,13 +27388,13 @@ Turn ${opt(a.target, "the selected frame")} into code for the project at ${opt(a
|
|
|
27388
27388
|
name: "figma-to-roblox",
|
|
27389
27389
|
title: "Figma to Roblox",
|
|
27390
27390
|
description: "Turn a frame into Roblox UI and place it in Studio with a Roblox Studio MCP.",
|
|
27391
|
-
args: { target: "Layer name or id (default: the selection)", mode: "scale, offset or hybrid
|
|
27391
|
+
args: { target: "Layer name or id (default: the selection)", mode: "scale (default), offset or hybrid" },
|
|
27392
27392
|
render: (a) => `
|
|
27393
27393
|
Turn ${opt(a.target, "the selected frame")} into Roblox UI that looks the same, and put it in the open Roblox Studio place.
|
|
27394
|
-
1. export_roblox {${a.target ? `nodeId:"<id of ${a.target}>", ` : ""}mode:"${opt(a.mode, "
|
|
27394
|
+
1. export_roblox {${a.target ? `nodeId:"<id of ${a.target}>", ` : ""}mode:"${opt(a.mode, "scale")}"}. Read the warnings (approximations, font substitutions).
|
|
27395
27395
|
2. If assets are not uploaded (placeholders rbxassetid://PENDING_n): upload each file in assets[] with the Roblox Studio MCP (e.g. upload_image), and replace each placeholder in the Luau with the returned id.
|
|
27396
27396
|
3. Run the Luau with execute_luau: it creates the ScreenGui under StarterGui (replacing an earlier copy) and returns it.
|
|
27397
|
-
4. Take a Studio screenshot and compare it with screenshot {returnImage:true} of the Figma frame. Fix differences in the Luau (or in Figma, then export again).
|
|
27397
|
+
4. Take a Studio screenshot and compare it with screenshot {returnImage:true} of the Figma frame. Fix differences in the Luau (or in Figma, then export again), keeping every size, position, padding and gap in scale (no offsets) and the PascalCase names.
|
|
27398
27398
|
5. Report the result and anything Roblox could not reproduce.`
|
|
27399
27399
|
},
|
|
27400
27400
|
{
|
|
@@ -27413,6 +27413,218 @@ Audit and improve this file${a.target ? ` (fixes limited to ${a.target})` : ""}.
|
|
|
27413
27413
|
];
|
|
27414
27414
|
});
|
|
27415
27415
|
|
|
27416
|
+
// plugin/lib/naming.ts
|
|
27417
|
+
function isGenericName(n) {
|
|
27418
|
+
const name = n.name.trim();
|
|
27419
|
+
if (!name || GENERIC_NAME.test(name))
|
|
27420
|
+
return true;
|
|
27421
|
+
if (n.kind === "text" && n.text !== undefined) {
|
|
27422
|
+
const t = n.text.trim();
|
|
27423
|
+
const a = name.toLowerCase();
|
|
27424
|
+
const b = t.toLowerCase();
|
|
27425
|
+
return a === b || a.length >= 12 && b.indexOf(a) === 0;
|
|
27426
|
+
}
|
|
27427
|
+
return false;
|
|
27428
|
+
}
|
|
27429
|
+
function visibleKids(n) {
|
|
27430
|
+
return n.children.filter(function(c) {
|
|
27431
|
+
return !c.hidden;
|
|
27432
|
+
});
|
|
27433
|
+
}
|
|
27434
|
+
function maxFont(n) {
|
|
27435
|
+
let m = n.kind === "text" && n.fontSize ? n.fontSize : 0;
|
|
27436
|
+
for (let i = 0;i < n.children.length; i++) {
|
|
27437
|
+
const c = n.children[i];
|
|
27438
|
+
if (c && !c.hidden)
|
|
27439
|
+
m = Math.max(m, maxFont(c));
|
|
27440
|
+
}
|
|
27441
|
+
return m;
|
|
27442
|
+
}
|
|
27443
|
+
function onlyKinds(list, kinds) {
|
|
27444
|
+
for (let i = 0;i < list.length; i++) {
|
|
27445
|
+
const c = list[i];
|
|
27446
|
+
if (c && kinds.indexOf(c.kind) === -1)
|
|
27447
|
+
return false;
|
|
27448
|
+
}
|
|
27449
|
+
return list.length > 0;
|
|
27450
|
+
}
|
|
27451
|
+
function buttonLike(n) {
|
|
27452
|
+
const kids = visibleKids(n);
|
|
27453
|
+
if (!n.background || n.h > 72 || kids.length === 0 || kids.length > 3)
|
|
27454
|
+
return false;
|
|
27455
|
+
let texts = 0;
|
|
27456
|
+
for (let i = 0;i < kids.length; i++) {
|
|
27457
|
+
const c = kids[i];
|
|
27458
|
+
if (!c)
|
|
27459
|
+
continue;
|
|
27460
|
+
if (c.kind === "text") {
|
|
27461
|
+
texts++;
|
|
27462
|
+
if ((c.text || "").length > 32)
|
|
27463
|
+
return false;
|
|
27464
|
+
} else if (c.kind !== "vector" && c.kind !== "image")
|
|
27465
|
+
return false;
|
|
27466
|
+
}
|
|
27467
|
+
return texts === 1;
|
|
27468
|
+
}
|
|
27469
|
+
function isScreen(n) {
|
|
27470
|
+
return SCREEN_W.indexOf(Math.round(n.w)) !== -1 && n.h >= 640 || n.w >= 1024 && n.h >= 600;
|
|
27471
|
+
}
|
|
27472
|
+
function largestText(n, parent) {
|
|
27473
|
+
const others = parent ? visibleKids(parent).filter(function(c) {
|
|
27474
|
+
return c !== n && c.kind === "text";
|
|
27475
|
+
}) : [];
|
|
27476
|
+
const size = n.fontSize || 14;
|
|
27477
|
+
return others.length > 0 && others.every(function(c) {
|
|
27478
|
+
return (c.fontSize || 14) < size;
|
|
27479
|
+
});
|
|
27480
|
+
}
|
|
27481
|
+
function covers(n, p) {
|
|
27482
|
+
return Math.abs(n.x) <= 1 && Math.abs(n.y) <= 1 && n.w >= p.w - 1 && n.h >= p.h - 1;
|
|
27483
|
+
}
|
|
27484
|
+
function roleOf(n, parent, root, top, inButton) {
|
|
27485
|
+
const kids = visibleKids(n);
|
|
27486
|
+
if (n.kind === "text") {
|
|
27487
|
+
const t = (n.text || "").trim();
|
|
27488
|
+
const size = n.fontSize || 14;
|
|
27489
|
+
if (inButton)
|
|
27490
|
+
return "Label";
|
|
27491
|
+
if (NUMERIC.test(t))
|
|
27492
|
+
return "Value";
|
|
27493
|
+
if (size >= top - 0.5 && size >= 18)
|
|
27494
|
+
return "Title";
|
|
27495
|
+
if (size >= 16 && largestText(n, parent))
|
|
27496
|
+
return "Title";
|
|
27497
|
+
if (size >= 20)
|
|
27498
|
+
return "Heading";
|
|
27499
|
+
if (t.length > 60)
|
|
27500
|
+
return "Description";
|
|
27501
|
+
if (size <= 12)
|
|
27502
|
+
return "Caption";
|
|
27503
|
+
if (t.length > 30)
|
|
27504
|
+
return "Description";
|
|
27505
|
+
return "Label";
|
|
27506
|
+
}
|
|
27507
|
+
if (n.kind === "vector")
|
|
27508
|
+
return "Icon";
|
|
27509
|
+
if (n.kind === "image" && kids.length === 0) {
|
|
27510
|
+
const round = n.ellipse || n.radius !== undefined && Math.abs(n.w - n.h) <= 1 && n.radius >= n.w / 2 - 1;
|
|
27511
|
+
return round ? "Avatar" : "Image";
|
|
27512
|
+
}
|
|
27513
|
+
if (kids.length === 0) {
|
|
27514
|
+
if (Math.min(n.w, n.h) <= 2 && Math.max(n.w, n.h) > 8)
|
|
27515
|
+
return "Divider";
|
|
27516
|
+
if (parent && covers(n, parent))
|
|
27517
|
+
return "Background";
|
|
27518
|
+
if (n.clickable)
|
|
27519
|
+
return "Button";
|
|
27520
|
+
if (n.ellipse)
|
|
27521
|
+
return n.w <= 16 ? "Dot" : "Circle";
|
|
27522
|
+
return "Shape";
|
|
27523
|
+
}
|
|
27524
|
+
if (n.clickable)
|
|
27525
|
+
return "Button";
|
|
27526
|
+
if (buttonLike(n)) {
|
|
27527
|
+
const label = kids.filter(function(c) {
|
|
27528
|
+
return c.kind === "text";
|
|
27529
|
+
})[0];
|
|
27530
|
+
return n.h <= 24 || label && (label.fontSize || 14) <= 12 ? "Badge" : "Button";
|
|
27531
|
+
}
|
|
27532
|
+
if (!parent) {
|
|
27533
|
+
if (isScreen(n))
|
|
27534
|
+
return "Screen";
|
|
27535
|
+
} else if (parent === root && isScreen(root)) {
|
|
27536
|
+
if (n.w >= root.w * 0.9 && n.y <= root.h * 0.1 && n.h <= root.h * 0.25)
|
|
27537
|
+
return "Header";
|
|
27538
|
+
if (n.w >= root.w * 0.9 && n.y + n.h >= root.h * 0.9 && n.h <= root.h * 0.25)
|
|
27539
|
+
return "Footer";
|
|
27540
|
+
if (n.h >= root.h * 0.9 && n.w <= root.w * 0.35 && n.x <= 1)
|
|
27541
|
+
return "Sidebar";
|
|
27542
|
+
}
|
|
27543
|
+
const flow = kids.filter(function(c) {
|
|
27544
|
+
return !c.absolute;
|
|
27545
|
+
});
|
|
27546
|
+
if (flow.length >= 2) {
|
|
27547
|
+
const r = baseName(flow[0]);
|
|
27548
|
+
const same = flow.every(function(c) {
|
|
27549
|
+
return baseName(c) === r;
|
|
27550
|
+
});
|
|
27551
|
+
if (same && r && r !== "Label" && r !== "Value" && r !== "Divider") {
|
|
27552
|
+
if (r === "Button")
|
|
27553
|
+
return "Actions";
|
|
27554
|
+
return r + (n.layout === "GRID" || n.wrap ? "Grid" : "List");
|
|
27555
|
+
}
|
|
27556
|
+
}
|
|
27557
|
+
if (n.background)
|
|
27558
|
+
return n.radius ? "Card" : "Panel";
|
|
27559
|
+
if (onlyKinds(kids, ["text"]))
|
|
27560
|
+
return "TextGroup";
|
|
27561
|
+
if (n.layout === "HORIZONTAL")
|
|
27562
|
+
return "Row";
|
|
27563
|
+
if (n.layout === "GRID")
|
|
27564
|
+
return "Grid";
|
|
27565
|
+
return "Container";
|
|
27566
|
+
}
|
|
27567
|
+
function baseName(n) {
|
|
27568
|
+
return pascalName(n.role || n.name).replace(/\d+$/, "");
|
|
27569
|
+
}
|
|
27570
|
+
function inferRoles(root) {
|
|
27571
|
+
const top = maxFont(root);
|
|
27572
|
+
const walk = function(n, parent, inButton) {
|
|
27573
|
+
const button = n.kind !== "text" && (n.clickable || buttonLike(n) || /button|btn/i.test(n.name));
|
|
27574
|
+
for (let i = 0;i < n.children.length; i++) {
|
|
27575
|
+
const c = n.children[i];
|
|
27576
|
+
if (c)
|
|
27577
|
+
walk(c, n, inButton || button);
|
|
27578
|
+
}
|
|
27579
|
+
if (isGenericName(n))
|
|
27580
|
+
n.role = roleOf(n, parent, root, top, inButton);
|
|
27581
|
+
};
|
|
27582
|
+
walk(root, null, false);
|
|
27583
|
+
}
|
|
27584
|
+
function pascalName(name) {
|
|
27585
|
+
let segs = name.split("/").map(function(s) {
|
|
27586
|
+
return s.replace(/^[\w-]+:/, "").trim();
|
|
27587
|
+
}).filter(function(s) {
|
|
27588
|
+
return s && !/^default$/i.test(s);
|
|
27589
|
+
});
|
|
27590
|
+
if (segs.length > 1)
|
|
27591
|
+
segs = segs.slice(1).concat([segs[0]]);
|
|
27592
|
+
const words = segs.join(" ").normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
27593
|
+
const out = words.map(function(w) {
|
|
27594
|
+
return w === w.toUpperCase() && w.length <= 4 ? w : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
|
|
27595
|
+
}).join("");
|
|
27596
|
+
return out.slice(0, 48);
|
|
27597
|
+
}
|
|
27598
|
+
function endsWithWord(name, words) {
|
|
27599
|
+
for (let i = 0;i < words.length; i++) {
|
|
27600
|
+
const w = words[i];
|
|
27601
|
+
if (name.length >= w.length && name.slice(-w.length) === w)
|
|
27602
|
+
return true;
|
|
27603
|
+
}
|
|
27604
|
+
return false;
|
|
27605
|
+
}
|
|
27606
|
+
function uniqueSiblingNames(names) {
|
|
27607
|
+
const counts = {};
|
|
27608
|
+
for (let i = 0;i < names.length; i++) {
|
|
27609
|
+
const k = names[i];
|
|
27610
|
+
counts[k] = (counts[k] || 0) + 1;
|
|
27611
|
+
}
|
|
27612
|
+
const seen = {};
|
|
27613
|
+
return names.map(function(k) {
|
|
27614
|
+
if ((counts[k] || 0) < 2)
|
|
27615
|
+
return k;
|
|
27616
|
+
seen[k] = (seen[k] || 0) + 1;
|
|
27617
|
+
return k + seen[k];
|
|
27618
|
+
});
|
|
27619
|
+
}
|
|
27620
|
+
var GENERIC_NAME, NUMERIC, SCREEN_W, ROLE_WORDS;
|
|
27621
|
+
var init_naming = __esm(() => {
|
|
27622
|
+
GENERIC_NAME = /^(frame|rectangle|ellipse|group|vector|text|line|polygon|star|component|instance|section|image|auto ?layout|union|subtract|intersect|exclude|boolean|layer|shape|slice|mask|container|wrapper|div|span|box)(\s*\d+)?(\s+copy(\s*\d+)?)?$/i;
|
|
27623
|
+
NUMERIC = /^[\s$€£¥+\-−]*[\d][\d\s.,:%/×x+\-−]*[\s$€£¥%kKmMbB]*$/;
|
|
27624
|
+
SCREEN_W = [360, 375, 390, 393, 402, 412, 414, 428, 430, 440];
|
|
27625
|
+
ROLE_WORDS = ("Actions Avatar Background Badge Banner Bar Body Button Card Column Container Content Controls Details Dialog Divider Footer Frame Grid Group Gui " + "Header Hero Hud HUD Info Item List Menu Modal Nav Navbar Navigation Overlay Panel Popup Row Screen Section Sidebar Slot Stack Stats Tab Tabs Tile " + "Toolbar Tooltip Window Wrapper").split(" ");
|
|
27626
|
+
});
|
|
27627
|
+
|
|
27416
27628
|
// src/roblox/fonts.ts
|
|
27417
27629
|
function robloxFamily(family, overrides = {}) {
|
|
27418
27630
|
const key = family.trim().toLowerCase();
|
|
@@ -27517,26 +27729,26 @@ function visible(list) {
|
|
|
27517
27729
|
function decide(n, opts) {
|
|
27518
27730
|
const approximations = [];
|
|
27519
27731
|
if (n.text)
|
|
27520
|
-
return { kind: "text",
|
|
27732
|
+
return { kind: "text", approximations };
|
|
27521
27733
|
const raster = opts.rasterize;
|
|
27522
27734
|
const fills = visible(n.fills);
|
|
27523
27735
|
const strokes = visible(n.strokes);
|
|
27524
|
-
const
|
|
27525
|
-
const drop = effects.filter((e) => e.type === "DROP_SHADOW");
|
|
27526
|
-
const other = effects.filter((e) => e.type !== "DROP_SHADOW");
|
|
27736
|
+
const other = (n.effects ?? []).filter((e) => e.type !== "DROP_SHADOW");
|
|
27527
27737
|
const hasChildren = !!n.children?.some((c) => !c.hidden);
|
|
27528
|
-
if (n.
|
|
27529
|
-
return
|
|
27738
|
+
if (n.rasterize && raster !== "none")
|
|
27739
|
+
return { kind: "picture", approximations: ["image file unreadable: pictured as it looks"] };
|
|
27740
|
+
if (n.vector || VECTOR_TYPES.includes(n.type)) {
|
|
27741
|
+
return raster === "none" ? { kind: "frame", approximations: ["vector shape skipped (rasterize is none)"] } : { kind: "picture", approximations };
|
|
27530
27742
|
}
|
|
27531
27743
|
const image = fills.length === 1 && fills[0].type === "IMAGE";
|
|
27532
|
-
if (image && !hasChildren && !strokes.length && !
|
|
27533
|
-
return { kind: "image",
|
|
27744
|
+
if (image && !hasChildren && !strokes.length && !other.length && raster !== "all")
|
|
27745
|
+
return { kind: "image", approximations };
|
|
27534
27746
|
const reasons = [];
|
|
27535
27747
|
if (fills.length > 1)
|
|
27536
27748
|
reasons.push("several fills");
|
|
27537
27749
|
if (fills.length === 1 && !["SOLID", "GRADIENT_LINEAR", "IMAGE"].includes(fills[0].type))
|
|
27538
27750
|
reasons.push(`${fills[0].type.toLowerCase().replace("_", " ")} fill`);
|
|
27539
|
-
if (image && (strokes.length ||
|
|
27751
|
+
if (image && (strokes.length || other.length))
|
|
27540
27752
|
reasons.push("image with stroke or effects");
|
|
27541
27753
|
if (strokes.length > 1 || strokes[0] && strokes[0].type !== "SOLID")
|
|
27542
27754
|
reasons.push("complex stroke");
|
|
@@ -27546,18 +27758,16 @@ function decide(n, opts) {
|
|
|
27546
27758
|
reasons.push(other.map((e) => e.type.toLowerCase().replace(/_/g, " ")).join(", "));
|
|
27547
27759
|
if (n.type === "ELLIPSE" && Math.abs(n.w - n.h) > 0.5)
|
|
27548
27760
|
reasons.push("oval");
|
|
27549
|
-
const styled = fills.length + strokes.length + effects.length > 0;
|
|
27761
|
+
const styled = fills.length + strokes.length + (n.effects ?? []).length > 0;
|
|
27550
27762
|
if ((reasons.length || raster === "all" && styled) && raster !== "none") {
|
|
27551
|
-
return { kind: hasChildren ? "panel" : "picture",
|
|
27763
|
+
return { kind: hasChildren ? "panel" : "picture", approximations };
|
|
27552
27764
|
}
|
|
27553
27765
|
if (reasons.length)
|
|
27554
27766
|
approximations.push(`${reasons.join(", ")}: approximated (rasterize is none)`);
|
|
27555
27767
|
const r = n.radius ?? [];
|
|
27556
27768
|
if (r.length && r.some((v) => Math.abs(v - r[0]) > 0.5))
|
|
27557
27769
|
approximations.push("different corner radii: used the largest");
|
|
27558
|
-
|
|
27559
|
-
approximations.push("drop shadow skipped (rasterize is none)");
|
|
27560
|
-
return { kind: "frame", shadow: drop.length > 0 && raster !== "none", approximations };
|
|
27770
|
+
return { kind: "frame", approximations };
|
|
27561
27771
|
}
|
|
27562
27772
|
function pictureRequests(root, opts) {
|
|
27563
27773
|
const out = [];
|
|
@@ -27569,8 +27779,6 @@ function pictureRequests(root, opts) {
|
|
|
27569
27779
|
out.push({ id: n.id, mode: "full" });
|
|
27570
27780
|
if (d.kind === "panel")
|
|
27571
27781
|
out.push({ id: n.id, mode: "panel" });
|
|
27572
|
-
if (d.shadow)
|
|
27573
|
-
out.push({ id: n.id, mode: "shadow" });
|
|
27574
27782
|
if (d.kind !== "picture")
|
|
27575
27783
|
for (const c of n.children ?? [])
|
|
27576
27784
|
walk(c);
|
|
@@ -27590,6 +27798,56 @@ function imageHashes(root) {
|
|
|
27590
27798
|
walk(root);
|
|
27591
27799
|
return [...out];
|
|
27592
27800
|
}
|
|
27801
|
+
function robloxName(base, flavor) {
|
|
27802
|
+
switch (flavor) {
|
|
27803
|
+
case "text":
|
|
27804
|
+
return /Label$/.test(base) ? base : `${base}Label`;
|
|
27805
|
+
case "button": {
|
|
27806
|
+
const b = base.replace(/Btn$/, "Button");
|
|
27807
|
+
return /Button$/.test(b) ? b : `${b}Button`;
|
|
27808
|
+
}
|
|
27809
|
+
case "icon":
|
|
27810
|
+
return endsWithWord(base, ICON_WORDS) ? base : `${base}Icon`;
|
|
27811
|
+
case "image":
|
|
27812
|
+
return endsWithWord(base, IMAGE_WORDS) ? base : `${base}Image`;
|
|
27813
|
+
default:
|
|
27814
|
+
return endsWithWord(base, ROLE_WORDS) ? base : `${base}${flavor === "frame" ? "Frame" : "Container"}`;
|
|
27815
|
+
}
|
|
27816
|
+
}
|
|
27817
|
+
function roleTree(n) {
|
|
27818
|
+
const fills = visible(n.fills);
|
|
27819
|
+
const kind = n.text ? "text" : n.vector || VECTOR_TYPES.includes(n.type) ? "vector" : fills.length === 1 && fills[0].type === "IMAGE" && !n.children?.length ? "image" : n.children?.length || n.layout ? "frame" : "shape";
|
|
27820
|
+
return {
|
|
27821
|
+
id: n.id,
|
|
27822
|
+
kind,
|
|
27823
|
+
name: n.name,
|
|
27824
|
+
x: n.x,
|
|
27825
|
+
y: n.y,
|
|
27826
|
+
w: n.w,
|
|
27827
|
+
h: n.h,
|
|
27828
|
+
hidden: n.hidden,
|
|
27829
|
+
text: n.text?.characters,
|
|
27830
|
+
fontSize: n.text ? Math.max(0, ...n.text.segments.map((s) => s.size)) : undefined,
|
|
27831
|
+
layout: n.layout?.mode,
|
|
27832
|
+
wrap: n.layout?.wrap,
|
|
27833
|
+
background: fills.length > 0 || visible(n.strokes).length > 0,
|
|
27834
|
+
radius: Math.max(0, ...n.radius ?? []),
|
|
27835
|
+
ellipse: n.type === "ELLIPSE",
|
|
27836
|
+
clickable: n.clickable,
|
|
27837
|
+
absolute: n.absolute,
|
|
27838
|
+
children: (n.children ?? []).map(roleTree)
|
|
27839
|
+
};
|
|
27840
|
+
}
|
|
27841
|
+
function dedupe(i) {
|
|
27842
|
+
const layers = i.children.filter((c) => !/^UI/.test(c.className));
|
|
27843
|
+
const names = uniqueSiblingNames(layers.map((c) => c.name));
|
|
27844
|
+
layers.forEach((c, k) => {
|
|
27845
|
+
c.name = names[k];
|
|
27846
|
+
c.props[0] = ["Name", S(c.name)];
|
|
27847
|
+
});
|
|
27848
|
+
for (const c of i.children)
|
|
27849
|
+
dedupe(c);
|
|
27850
|
+
}
|
|
27593
27851
|
|
|
27594
27852
|
class Mapper {
|
|
27595
27853
|
opts;
|
|
@@ -27598,56 +27856,98 @@ class Mapper {
|
|
|
27598
27856
|
fontSubstitutions = new Map;
|
|
27599
27857
|
counts = { instances: 0, pictures: 0, skippedHidden: 0 };
|
|
27600
27858
|
textFactor = 1;
|
|
27859
|
+
names = new Map;
|
|
27860
|
+
roles = new Map;
|
|
27601
27861
|
constructor(opts, assets) {
|
|
27602
27862
|
this.opts = opts;
|
|
27603
27863
|
this.assets = assets;
|
|
27604
27864
|
}
|
|
27865
|
+
get scaleOnly() {
|
|
27866
|
+
return this.opts.mode === "scale";
|
|
27867
|
+
}
|
|
27605
27868
|
warn(n, msg) {
|
|
27606
27869
|
if (this.warnings.size < 60)
|
|
27607
27870
|
this.warnings.add(n ? `${n.name}: ${msg}` : msg);
|
|
27608
27871
|
}
|
|
27872
|
+
nameLayers(root) {
|
|
27873
|
+
const tree = roleTree(root);
|
|
27874
|
+
inferRoles(tree);
|
|
27875
|
+
const walk = (r) => {
|
|
27876
|
+
const base = pascalName(r.role ?? r.name) || pascalName(r.role ?? "") || "Layer";
|
|
27877
|
+
this.names.set(r.id, base);
|
|
27878
|
+
if (r.role)
|
|
27879
|
+
this.roles.set(r.id, r.role);
|
|
27880
|
+
for (const c of r.children)
|
|
27881
|
+
walk(c);
|
|
27882
|
+
};
|
|
27883
|
+
walk(tree);
|
|
27884
|
+
}
|
|
27885
|
+
label(n, flavor) {
|
|
27886
|
+
return robloxName(this.names.get(n.id) ?? (pascalName(n.name) || "Layer"), flavor);
|
|
27887
|
+
}
|
|
27888
|
+
isButton(n) {
|
|
27889
|
+
return !!n.clickable || BUTTON_NAME.test(n.name) || this.roles.get(n.id) === "Button";
|
|
27890
|
+
}
|
|
27609
27891
|
map(root) {
|
|
27892
|
+
this.nameLayers(root);
|
|
27610
27893
|
const [tw, th] = this.opts.targetResolution;
|
|
27611
27894
|
const fullScreen = Math.abs(root.w / root.h - tw / th) < 0.1 * (tw / th) && root.w >= tw * 0.4;
|
|
27612
27895
|
if (fullScreen && this.opts.mode !== "offset")
|
|
27613
27896
|
this.textFactor = tw / root.w;
|
|
27614
27897
|
const top = this.node(root, null, 0);
|
|
27615
27898
|
const geo = this.opts.mode === "offset" ? [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(0, root.w, 0, root.h)]] : fullScreen ? [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(1, 0, 1, 0)]] : [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(root.w / tw, 0, root.h / th, 0)]];
|
|
27616
|
-
top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex"].includes(k));
|
|
27899
|
+
top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex", "AutomaticSize"].includes(k));
|
|
27617
27900
|
top.props.splice(1, 0, ...geo);
|
|
27618
|
-
|
|
27619
|
-
if (this.opts.
|
|
27620
|
-
|
|
27621
|
-
|
|
27901
|
+
const ratio = [["AspectRatio", F(root.w / root.h)]];
|
|
27902
|
+
if (this.opts.mode !== "offset" && !fullScreen)
|
|
27903
|
+
ratio.push(["DominantAxis", E("DominantAxis", "Height")]);
|
|
27904
|
+
top.children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", ratio));
|
|
27905
|
+
const out = this.opts.asRootFrame ? top : this.inst("ScreenGui", `${(this.names.get(root.id) ?? "Ui").replace(/Gui$/, "")}Gui`, [["ResetOnSpawn", B(false)], ["ZIndexBehavior", E("ZIndexBehavior", "Sibling")], ["IgnoreGuiInset", B(true)]], [top]);
|
|
27906
|
+
dedupe(out);
|
|
27907
|
+
return out;
|
|
27622
27908
|
}
|
|
27623
27909
|
inst(className, name, props = [], children = [], source) {
|
|
27624
27910
|
this.counts.instances++;
|
|
27625
27911
|
return { className, name, props: [["Name", S(name)], ...props], children, ...source ? { source } : {} };
|
|
27626
27912
|
}
|
|
27913
|
+
frameOf(n, parent) {
|
|
27914
|
+
const inFlow = !!parent.layout && !n.absolute;
|
|
27915
|
+
const scale = this.opts.mode === "scale" || this.opts.mode === "hybrid" && !inFlow;
|
|
27916
|
+
if (!inFlow)
|
|
27917
|
+
return { scale, w: Math.max(1, parent.w), h: Math.max(1, parent.h), inFlow };
|
|
27918
|
+
const p = parent.layout.padding;
|
|
27919
|
+
return { scale, w: Math.max(1, parent.w - (p[1] ?? 0) - (p[3] ?? 0)), h: Math.max(1, parent.h - (p[0] ?? 0) - (p[2] ?? 0)), inFlow };
|
|
27920
|
+
}
|
|
27921
|
+
hugs(n, d) {
|
|
27922
|
+
if (this.scaleOnly)
|
|
27923
|
+
return { w: false, h: false };
|
|
27924
|
+
const content = d.kind === "text" || !!n.layout;
|
|
27925
|
+
return { w: n.sizing?.h === "HUG" && content, h: n.sizing?.v === "HUG" && content };
|
|
27926
|
+
}
|
|
27627
27927
|
geometry(n, parent, index, d) {
|
|
27628
27928
|
const out = [];
|
|
27629
27929
|
if (!parent)
|
|
27630
27930
|
return out;
|
|
27631
|
-
const
|
|
27632
|
-
const
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
if (inFlow) {
|
|
27636
|
-
const l = parent.layout;
|
|
27637
|
-
const horizontal = l.mode === "HORIZONTAL";
|
|
27638
|
-
const pw = parent.w - (l.padding[1] ?? 0) - (l.padding[3] ?? 0);
|
|
27639
|
-
const ph = parent.h - (l.padding[0] ?? 0) - (l.padding[2] ?? 0);
|
|
27931
|
+
const f = this.frameOf(n, parent);
|
|
27932
|
+
const hug = this.hugs(n, d);
|
|
27933
|
+
if (f.inFlow) {
|
|
27934
|
+
const horizontal = parent.layout.mode === "HORIZONTAL";
|
|
27640
27935
|
const crossFill = horizontal ? n.sizing?.v === "FILL" || n.stretch : n.sizing?.h === "FILL" || n.stretch;
|
|
27641
|
-
const w =
|
|
27642
|
-
const h =
|
|
27936
|
+
const w = !horizontal && crossFill ? U(1, 0) : hug.w ? U(0, 0) : f.scale ? U(n.w / f.w, 0) : U(0, n.w);
|
|
27937
|
+
const h = horizontal && crossFill ? U(1, 0) : hug.h ? U(0, 0) : f.scale ? U(n.h / f.h, 0) : U(0, n.h);
|
|
27643
27938
|
out.push(["Size", { t: "UDim2", xs: w.s, xo: w.o, ys: h.s, yo: h.o }]);
|
|
27644
27939
|
out.push(["LayoutOrder", I(index + 1)]);
|
|
27645
27940
|
return out;
|
|
27646
27941
|
}
|
|
27647
|
-
const c = n.constraints ?? { h: "MIN", v: "MIN" };
|
|
27942
|
+
const c = { ...n.constraints ?? { h: "MIN", v: "MIN" } };
|
|
27943
|
+
const centered = this.centered(n, parent);
|
|
27944
|
+
if (centered.x && c.h === "MIN")
|
|
27945
|
+
c.h = "CENTER";
|
|
27946
|
+
if (centered.y && c.v === "MIN")
|
|
27947
|
+
c.v = "CENTER";
|
|
27648
27948
|
const axis = (pos, size, total, k) => {
|
|
27649
27949
|
const end = total - pos - size;
|
|
27650
|
-
if (k === "SCALE" ||
|
|
27950
|
+
if (k === "SCALE" || f.scale) {
|
|
27651
27951
|
if (k === "MAX")
|
|
27652
27952
|
return { anchor: 1, p: [(pos + size) / total, 0], s: [size / total, 0] };
|
|
27653
27953
|
if (k === "CENTER")
|
|
@@ -27662,39 +27962,72 @@ class Mapper {
|
|
|
27662
27962
|
return { anchor: 0, p: [0, pos], s: [1, -(pos + end)] };
|
|
27663
27963
|
return { anchor: 0, p: [0, pos], s: [0, size] };
|
|
27664
27964
|
};
|
|
27665
|
-
const x = axis(n.x, n.w,
|
|
27666
|
-
const y = axis(n.y, n.h,
|
|
27965
|
+
const x = axis(n.x, n.w, f.w, c.h);
|
|
27966
|
+
const y = axis(n.y, n.h, f.h, c.v);
|
|
27667
27967
|
if (x.anchor || y.anchor)
|
|
27668
27968
|
out.push(["AnchorPoint", V2(x.anchor, y.anchor)]);
|
|
27669
27969
|
out.push(["Position", U2(x.p[0], x.p[1], y.p[0], y.p[1])]);
|
|
27670
|
-
out.push(["Size", U2(
|
|
27970
|
+
out.push(["Size", U2(hug.w ? 0 : x.s[0], hug.w ? 0 : x.s[1], hug.h ? 0 : y.s[0], hug.h ? 0 : y.s[1])]);
|
|
27671
27971
|
out.push(["ZIndex", I(index + 1)]);
|
|
27672
27972
|
return out;
|
|
27673
27973
|
}
|
|
27974
|
+
centered(n, parent) {
|
|
27975
|
+
if (!parent || parent.layout && !n.absolute)
|
|
27976
|
+
return { x: false, y: false };
|
|
27977
|
+
const t = n.text?.autoResize;
|
|
27978
|
+
const hugX = t === "WIDTH_AND_HEIGHT" || !n.text && n.sizing?.h === "HUG";
|
|
27979
|
+
const hugY = t === "WIDTH_AND_HEIGHT" || t === "HEIGHT" || !n.text && n.sizing?.v === "HUG";
|
|
27980
|
+
const near = (pos, size, total) => Math.abs(pos + size / 2 - total / 2) <= 1.5 && size < total;
|
|
27981
|
+
return { x: hugX && near(n.x, n.w, parent.w), y: hugY && near(n.y, n.h, parent.h) };
|
|
27982
|
+
}
|
|
27674
27983
|
automaticSize(n, d) {
|
|
27675
|
-
const
|
|
27676
|
-
|
|
27677
|
-
if (!hugW && !hugH)
|
|
27984
|
+
const hug = this.hugs(n, d);
|
|
27985
|
+
if (!hug.w && !hug.h)
|
|
27678
27986
|
return [];
|
|
27679
|
-
return [["AutomaticSize", E("AutomaticSize",
|
|
27987
|
+
return [["AutomaticSize", E("AutomaticSize", hug.w && hug.h ? "XY" : hug.w ? "X" : "Y")]];
|
|
27680
27988
|
}
|
|
27681
27989
|
corner(n) {
|
|
27682
27990
|
const r = Math.max(0, ...n.radius ?? []);
|
|
27683
27991
|
if (!r && !(n.type === "ELLIPSE"))
|
|
27684
27992
|
return null;
|
|
27685
|
-
const
|
|
27686
|
-
|
|
27993
|
+
const short = Math.max(1, Math.min(n.w, n.h));
|
|
27994
|
+
const pill = n.type === "ELLIPSE" || r >= short / 2 - 0.5;
|
|
27995
|
+
return this.inst("UICorner", "UICorner", [["CornerRadius", pill ? U(0.5, 0) : this.scaleOnly ? U(r / short, 0) : U(0, r)]]);
|
|
27687
27996
|
}
|
|
27688
27997
|
stroke(n) {
|
|
27689
27998
|
const s = visible(n.strokes)[0];
|
|
27690
27999
|
if (!s || s.type !== "SOLID" || !n.strokeWeight)
|
|
27691
28000
|
return null;
|
|
27692
|
-
|
|
28001
|
+
const short = Math.max(1, Math.min(n.w, n.h));
|
|
28002
|
+
const props = [
|
|
27693
28003
|
["ApplyStrokeMode", E("ApplyStrokeMode", "Border")],
|
|
27694
28004
|
["Color", C3(s.color)],
|
|
27695
|
-
["Thickness", F(n.strokeWeight)],
|
|
28005
|
+
["Thickness", F(this.scaleOnly ? n.strokeWeight / short : n.strokeWeight)],
|
|
27696
28006
|
["Transparency", F(1 - s.opacity)],
|
|
27697
|
-
["LineJoinMode", E("LineJoinMode", "Round")]
|
|
28007
|
+
["LineJoinMode", E("LineJoinMode", "Round")],
|
|
28008
|
+
["BorderStrokePosition", E("BorderStrokePosition", n.strokeAlign === "OUTSIDE" ? "Outer" : n.strokeAlign === "CENTER" ? "Center" : "Inner")]
|
|
28009
|
+
];
|
|
28010
|
+
if (this.scaleOnly)
|
|
28011
|
+
props.push(["StrokeSizingMode", E("StrokeSizingMode", "ScaledSize")]);
|
|
28012
|
+
return this.inst("UIStroke", "UIStroke", props);
|
|
28013
|
+
}
|
|
28014
|
+
shadow(n) {
|
|
28015
|
+
const list = (n.effects ?? []).filter((e) => e.type === "DROP_SHADOW");
|
|
28016
|
+
if (!list.length)
|
|
28017
|
+
return null;
|
|
28018
|
+
if (list.length > 1)
|
|
28019
|
+
this.warn(n, "several drop shadows: kept the largest");
|
|
28020
|
+
const e = list.reduce((a, b) => b.blur + (b.spread ?? 0) > a.blur + (a.spread ?? 0) ? b : a);
|
|
28021
|
+
const w = Math.max(1, n.w);
|
|
28022
|
+
const h = Math.max(1, n.h);
|
|
28023
|
+
const spread = (e.spread ?? 0) * 2;
|
|
28024
|
+
const s = this.scaleOnly;
|
|
28025
|
+
return this.inst("UIShadow", "UIShadow", [
|
|
28026
|
+
["Color", C3(e.color ?? "#000000")],
|
|
28027
|
+
["Transparency", F(1 - (e.alpha ?? 0.25))],
|
|
28028
|
+
["BlurRadius", s ? U(e.blur / Math.min(w, h), 0) : U(0, e.blur)],
|
|
28029
|
+
["Offset", s ? U2((e.x ?? 0) / w, 0, (e.y ?? 0) / h, 0) : U2(0, e.x ?? 0, 0, e.y ?? 0)],
|
|
28030
|
+
["Spread", s ? U2(spread / w, 0, spread / h, 0) : U2(0, spread, 0, spread)]
|
|
27698
28031
|
]);
|
|
27699
28032
|
}
|
|
27700
28033
|
gradient(p) {
|
|
@@ -27717,18 +28050,26 @@ class Mapper {
|
|
|
27717
28050
|
}
|
|
27718
28051
|
layoutChildren(n) {
|
|
27719
28052
|
const l = n.layout;
|
|
28053
|
+
const s = this.scaleOnly;
|
|
27720
28054
|
const out = [];
|
|
27721
|
-
const [t, r, b, lft] = l.padding;
|
|
28055
|
+
const [t = 0, r = 0, b = 0, lft = 0] = l.padding;
|
|
28056
|
+
const w = Math.max(1, n.w);
|
|
28057
|
+
const h = Math.max(1, n.h);
|
|
28058
|
+
const cw = Math.max(1, w - r - lft);
|
|
28059
|
+
const ch = Math.max(1, h - t - b);
|
|
28060
|
+
const pad = (v, total) => s ? U(v / total, 0) : U(0, v);
|
|
27722
28061
|
if (t || r || b || lft)
|
|
27723
|
-
out.push(this.inst("UIPadding", "UIPadding", [["PaddingTop",
|
|
28062
|
+
out.push(this.inst("UIPadding", "UIPadding", [["PaddingTop", pad(t, h)], ["PaddingRight", pad(r, w)], ["PaddingBottom", pad(b, h)], ["PaddingLeft", pad(lft, w)]]));
|
|
27724
28063
|
if (l.mode === "GRID") {
|
|
27725
28064
|
const kids = (n.children ?? []).filter((c) => !c.hidden && !c.absolute);
|
|
27726
28065
|
const first = kids[0];
|
|
27727
28066
|
if (kids.some((k) => Math.abs(k.w - (first?.w ?? 0)) > 1 || Math.abs(k.h - (first?.h ?? 0)) > 1))
|
|
27728
28067
|
this.warn(n, "grid cells of different sizes: UIGridLayout uses the first cell's size");
|
|
28068
|
+
const fw = first?.w ?? 100;
|
|
28069
|
+
const fh = first?.h ?? 100;
|
|
27729
28070
|
out.push(this.inst("UIGridLayout", "UIGridLayout", [
|
|
27730
|
-
["CellSize", U2(0,
|
|
27731
|
-
["CellPadding", U2(0, l.columnGap ?? 0, 0, l.rowGap ?? 0)],
|
|
28071
|
+
["CellSize", s ? U2(fw / cw, 0, fh / ch, 0) : U2(0, fw, 0, fh)],
|
|
28072
|
+
["CellPadding", s ? U2((l.columnGap ?? 0) / cw, 0, (l.rowGap ?? 0) / ch, 0) : U2(0, l.columnGap ?? 0, 0, l.rowGap ?? 0)],
|
|
27732
28073
|
["FillDirectionMaxCells", I(l.columns ?? 0)],
|
|
27733
28074
|
["SortOrder", E("SortOrder", "LayoutOrder")]
|
|
27734
28075
|
]));
|
|
@@ -27737,10 +28078,11 @@ class Mapper {
|
|
|
27737
28078
|
const horizontal = l.mode === "HORIZONTAL";
|
|
27738
28079
|
const main = l.primary === "CENTER" ? "Center" : l.primary === "MAX" ? horizontal ? "Right" : "Bottom" : horizontal ? "Left" : "Top";
|
|
27739
28080
|
const cross = l.counter === "CENTER" ? "Center" : l.counter === "MAX" ? horizontal ? "Bottom" : "Right" : horizontal ? "Top" : "Left";
|
|
28081
|
+
const gap = l.primary === "SPACE_BETWEEN" ? 0 : l.gap;
|
|
27740
28082
|
const props = [
|
|
27741
28083
|
["FillDirection", E("FillDirection", horizontal ? "Horizontal" : "Vertical")],
|
|
27742
28084
|
["SortOrder", E("SortOrder", "LayoutOrder")],
|
|
27743
|
-
["Padding",
|
|
28085
|
+
["Padding", pad(gap, horizontal ? cw : ch)],
|
|
27744
28086
|
["HorizontalAlignment", E("HorizontalAlignment", horizontal ? main === "Right" ? "Right" : main === "Center" ? "Center" : "Left" : cross === "Right" ? "Right" : cross === "Center" ? "Center" : "Left")],
|
|
27745
28087
|
["VerticalAlignment", E("VerticalAlignment", horizontal ? cross === "Bottom" ? "Bottom" : cross === "Center" ? "Center" : "Top" : main === "Bottom" ? "Bottom" : main === "Center" ? "Center" : "Top")]
|
|
27746
28088
|
];
|
|
@@ -27803,10 +28145,12 @@ class Mapper {
|
|
|
27803
28145
|
props.push(["LineHeight", F(Math.max(1, Math.min(3, base.lineHeight / (base.size * 1.2))))]);
|
|
27804
28146
|
if (t.maxLines)
|
|
27805
28147
|
props.push(["TextTruncate", E("TextTruncate", "AtEnd")]);
|
|
27806
|
-
if (this.opts.textScaled)
|
|
28148
|
+
if (this.opts.textScaled || this.scaleOnly)
|
|
27807
28149
|
props.push(["TextScaled", B(true)]);
|
|
27808
28150
|
if (segs.some((s) => s.letterSpacing))
|
|
27809
28151
|
this.warn(n, "letter spacing has no Roblox equivalent: ignored");
|
|
28152
|
+
if (rich && this.scaleOnly && segs.some((s) => s.size !== base.size))
|
|
28153
|
+
this.warn(n, "several text sizes with TextScaled: the size tags may not scale with the label");
|
|
27810
28154
|
return props;
|
|
27811
28155
|
}
|
|
27812
28156
|
richRun(s, base) {
|
|
@@ -27844,7 +28188,7 @@ class Mapper {
|
|
|
27844
28188
|
const d = decide(n, this.opts);
|
|
27845
28189
|
for (const a of d.approximations)
|
|
27846
28190
|
this.warn(n, a);
|
|
27847
|
-
const button = !!parent &&
|
|
28191
|
+
const button = !!parent && this.isButton(n) && d.kind !== "text";
|
|
27848
28192
|
const geo = this.geometry(n, parent, index, d);
|
|
27849
28193
|
const common = [...geo, ...this.automaticSize(n, d)];
|
|
27850
28194
|
if (n.rotation)
|
|
@@ -27853,26 +28197,36 @@ class Mapper {
|
|
|
27853
28197
|
let className = "Frame";
|
|
27854
28198
|
let props = [];
|
|
27855
28199
|
if (d.kind === "text") {
|
|
27856
|
-
|
|
28200
|
+
const isButton = !!n.clickable || BUTTON_NAME.test(n.name);
|
|
28201
|
+
className = isButton ? "TextButton" : "TextLabel";
|
|
27857
28202
|
props = this.textProps(n);
|
|
27858
|
-
|
|
28203
|
+
const centered = this.centered(n, parent);
|
|
28204
|
+
const auto = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
|
|
28205
|
+
props = props.map(([k, v]) => k === "TextXAlignment" && centered.x ? [k, E("TextXAlignment", "Center")] : k === "TextYAlignment" && auto && n.text.valign === "TOP" ? [k, E("TextYAlignment", "Center")] : [k, v]);
|
|
28206
|
+
if (isButton)
|
|
27859
28207
|
props.push(["AutoButtonColor", B(false)]);
|
|
28208
|
+
if ((n.effects ?? []).some((e) => e.type === "DROP_SHADOW"))
|
|
28209
|
+
this.warn(n, "text shadow has no Roblox equivalent: ignored");
|
|
27860
28210
|
const size = props.find(([k]) => k === "TextSize")[1].v;
|
|
27861
|
-
|
|
27862
|
-
|
|
28211
|
+
const tight = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
|
|
28212
|
+
const max = this.scaleOnly && tight ? size * 2 : size;
|
|
28213
|
+
const min = this.scaleOnly ? 1 : Math.max(1, Math.round(size * 0.5));
|
|
28214
|
+
children.push(this.inst("UITextSizeConstraint", "UITextSizeConstraint", [["MaxTextSize", I(max)], ["MinTextSize", I(min)]]));
|
|
28215
|
+
return this.inst(className, this.label(n, isButton ? "button" : "text"), [...common, ...props], children, n.id);
|
|
27863
28216
|
}
|
|
27864
28217
|
if (d.kind === "picture") {
|
|
27865
28218
|
this.counts.pictures++;
|
|
27866
28219
|
const a = this.assetUrl(`${n.id}:full`, n);
|
|
27867
28220
|
className = button ? "ImageButton" : "ImageLabel";
|
|
27868
|
-
const pic = this.pictureGeometry(n, a, geo);
|
|
28221
|
+
const pic = parent ? this.pictureGeometry(n, a, geo, parent) : geo;
|
|
27869
28222
|
props = [["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ["ScaleType", E("ScaleType", "Stretch")]];
|
|
27870
28223
|
if (n.opacity !== undefined && n.opacity < 1)
|
|
27871
28224
|
props.push(["ImageTransparency", F(1 - n.opacity)]);
|
|
27872
28225
|
if (button)
|
|
27873
28226
|
props.push(["AutoButtonColor", B(false)]);
|
|
27874
28227
|
children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F((a.size?.w ?? n.w) / Math.max(1, a.size?.h ?? n.h))]]));
|
|
27875
|
-
|
|
28228
|
+
const flavor = button ? "button" : n.vector || VECTOR_TYPES.includes(n.type) ? "icon" : "image";
|
|
28229
|
+
return this.inst(className, this.label(n, flavor), [...pic, ...props], children, n.id);
|
|
27876
28230
|
}
|
|
27877
28231
|
const fills = visible(n.fills);
|
|
27878
28232
|
const opacity = n.opacity ?? 1;
|
|
@@ -27890,30 +28244,34 @@ class Mapper {
|
|
|
27890
28244
|
const corner = this.corner(n);
|
|
27891
28245
|
if (corner)
|
|
27892
28246
|
children.push(corner);
|
|
28247
|
+
const shadow = this.shadow(n);
|
|
28248
|
+
if (shadow)
|
|
28249
|
+
children.push(shadow);
|
|
27893
28250
|
children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F(n.w / Math.max(1, n.h))]]));
|
|
27894
|
-
return this.inst(className, n
|
|
28251
|
+
return this.inst(className, this.label(n, button ? "button" : "image"), [...common, ...props], children, n.id);
|
|
27895
28252
|
}
|
|
27896
28253
|
if (d.kind === "panel") {
|
|
27897
28254
|
this.counts.pictures++;
|
|
27898
28255
|
const a = this.assetUrl(`${n.id}:panel`, n);
|
|
27899
|
-
const scale = this.opts.scale ?? 2;
|
|
28256
|
+
const scale = a.scale ?? this.opts.scale ?? 2;
|
|
27900
28257
|
const inset = Math.ceil((Math.max(0, ...n.radius ?? [0]) + (n.strokeWeight ?? 0) + 2) * scale);
|
|
27901
|
-
const pw = Math.round((a.size?.w ?? n.w) * scale);
|
|
27902
|
-
const ph = Math.round((a.size?.h ?? n.h) * scale);
|
|
28258
|
+
const pw = a.px?.w ?? Math.round((a.size?.w ?? n.w) * scale);
|
|
28259
|
+
const ph = a.px?.h ?? Math.round((a.size?.h ?? n.h) * scale);
|
|
27903
28260
|
const ix = Math.min(inset, Math.floor(pw / 2) - 1);
|
|
27904
28261
|
const iy = Math.min(inset, Math.floor(ph / 2) - 1);
|
|
27905
28262
|
className = button ? "ImageButton" : "ImageLabel";
|
|
27906
|
-
props = [
|
|
27907
|
-
["BackgroundTransparency", F(1)],
|
|
27908
|
-
["Image", { t: "Content", url: a.url }],
|
|
27909
|
-
["ScaleType", E("ScaleType", "Slice")],
|
|
27910
|
-
["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
|
|
27911
|
-
["SliceScale", F(1 / scale)]
|
|
27912
|
-
];
|
|
28263
|
+
props = [["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ...this.imageScaling(n, ix, iy, pw, ph, scale)];
|
|
27913
28264
|
if (opacity < 1)
|
|
27914
28265
|
props.push(["ImageTransparency", F(1 - opacity)]);
|
|
27915
28266
|
if (button)
|
|
27916
28267
|
props.push(["AutoButtonColor", B(false)]);
|
|
28268
|
+
const shadow = this.shadow(n);
|
|
28269
|
+
if (shadow) {
|
|
28270
|
+
const corner = this.corner(n);
|
|
28271
|
+
if (corner)
|
|
28272
|
+
children.push(corner);
|
|
28273
|
+
children.push(shadow);
|
|
28274
|
+
}
|
|
27917
28275
|
} else {
|
|
27918
28276
|
className = button ? "TextButton" : hasKids && opacity < 1 ? "CanvasGroup" : "Frame";
|
|
27919
28277
|
const f = fills[0];
|
|
@@ -27930,7 +28288,7 @@ class Mapper {
|
|
|
27930
28288
|
} else if (f.type === "IMAGE") {
|
|
27931
28289
|
props.push(["BackgroundTransparency", F(1)]);
|
|
27932
28290
|
const a = this.assetUrl(`image:${f.imageHash}`, n);
|
|
27933
|
-
const bg = this.inst("ImageLabel", "
|
|
28291
|
+
const bg = this.inst("ImageLabel", "BackgroundImage", [["Size", U2(1, 0, 1, 0)], ["ZIndex", I(0)], ["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ["ScaleType", E("ScaleType", f.scaleMode === "FIT" ? "Fit" : "Crop")]]);
|
|
27934
28292
|
const corner = this.corner(n);
|
|
27935
28293
|
if (corner)
|
|
27936
28294
|
bg.children.push(corner);
|
|
@@ -27946,6 +28304,9 @@ class Mapper {
|
|
|
27946
28304
|
const stroke = this.stroke(n);
|
|
27947
28305
|
if (stroke)
|
|
27948
28306
|
children.push(stroke);
|
|
28307
|
+
const shadow = this.shadow(n);
|
|
28308
|
+
if (shadow)
|
|
28309
|
+
children.push(shadow);
|
|
27949
28310
|
}
|
|
27950
28311
|
props.push(["BorderSizePixel", I(0)]);
|
|
27951
28312
|
if (n.clip)
|
|
@@ -27954,8 +28315,7 @@ class Mapper {
|
|
|
27954
28315
|
const flow = n.layout ? kids.filter((k) => !k.absolute) : kids;
|
|
27955
28316
|
const loose = n.layout ? kids.filter((k) => k.absolute) : [];
|
|
27956
28317
|
const me = { w: n.w, h: n.h, layout: n.layout };
|
|
27957
|
-
const
|
|
27958
|
-
const flowMapped = mapped(flow, me);
|
|
28318
|
+
const flowMapped = flow.map((k, i) => this.node(k, me, i)).filter((x) => !!x);
|
|
27959
28319
|
if (n.layout && n.layout.mode !== "GRID") {
|
|
27960
28320
|
for (const k of flow) {
|
|
27961
28321
|
const target = k.grow && !k.hidden ? flowMapped.find((c) => c.source === k.id) : undefined;
|
|
@@ -27973,44 +28333,37 @@ class Mapper {
|
|
|
27973
28333
|
children.push(...this.layoutChildren(n));
|
|
27974
28334
|
children.push(...flowMapped);
|
|
27975
28335
|
}
|
|
27976
|
-
const
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
const
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
const inset = Math.ceil((Math.max(0, ...n.radius ?? [0]) + reach) * scale);
|
|
27987
|
-
const pw = Math.round(size.w * scale);
|
|
27988
|
-
const ph = Math.round(size.h * scale);
|
|
27989
|
-
const ix = Math.min(inset, Math.floor(pw / 2) - 1);
|
|
27990
|
-
const iy = Math.min(inset, Math.floor(ph / 2) - 1);
|
|
27991
|
-
const shadow = this.inst("ImageLabel", "Shadow", [
|
|
27992
|
-
["Position", U2(0, off.x, 0, off.y)],
|
|
27993
|
-
["Size", U2(1, size.w - n.w, 1, size.h - n.h)],
|
|
27994
|
-
["ZIndex", I(1)],
|
|
27995
|
-
["BackgroundTransparency", F(1)],
|
|
27996
|
-
["Image", { t: "Content", url: a.url }],
|
|
28336
|
+
const flavor = button ? "button" : fills.length || visible(n.strokes).length || d.kind === "panel" ? "frame" : "container";
|
|
28337
|
+
return this.inst(className, this.label(n, flavor), [...common, ...props], children, n.id);
|
|
28338
|
+
}
|
|
28339
|
+
imageScaling(n, ix, iy, pw, ph, scale) {
|
|
28340
|
+
if (this.scaleOnly)
|
|
28341
|
+
return [["ScaleType", E("ScaleType", "Stretch")]];
|
|
28342
|
+
const resizes = n.grow || n.stretch || n.sizing?.h === "FILL" || n.sizing?.v === "FILL" || n.sizing?.h === "HUG" || n.sizing?.v === "HUG";
|
|
28343
|
+
if (this.opts.mode === "offset" && !resizes)
|
|
28344
|
+
return [["ScaleType", E("ScaleType", "Stretch")]];
|
|
28345
|
+
return [
|
|
27997
28346
|
["ScaleType", E("ScaleType", "Slice")],
|
|
27998
28347
|
["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
|
|
27999
28348
|
["SliceScale", F(1 / scale)]
|
|
28000
|
-
]
|
|
28001
|
-
self.props = self.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "AutomaticSize"].includes(k));
|
|
28002
|
-
self.props.splice(1, 0, ["Size", U2(1, 0, 1, 0)], ["ZIndex", I(2)]);
|
|
28003
|
-
return this.inst("Frame", n.name, [...common.filter(([k]) => k !== "AutomaticSize"), ["BackgroundTransparency", F(1)], ["BorderSizePixel", I(0)]], [shadow, self], n.id);
|
|
28349
|
+
];
|
|
28004
28350
|
}
|
|
28005
|
-
pictureGeometry(n, a, geo) {
|
|
28351
|
+
pictureGeometry(n, a, geo, parent) {
|
|
28006
28352
|
if (!a.offset || !a.size || Math.abs(a.offset.x) < 0.5 && Math.abs(a.offset.y) < 0.5 && Math.abs(a.size.w - n.w) < 0.5 && Math.abs(a.size.h - n.h) < 0.5)
|
|
28007
28353
|
return geo;
|
|
28354
|
+
const f = this.frameOf(n, parent);
|
|
28355
|
+
const dw = a.size.w - n.w;
|
|
28356
|
+
const dh = a.size.h - n.h;
|
|
28357
|
+
const anchor = geo.find(([k]) => k === "AnchorPoint")?.[1];
|
|
28358
|
+
const ox = a.offset.x + (anchor?.x ?? 0) * dw;
|
|
28359
|
+
const oy = a.offset.y + (anchor?.y ?? 0) * dh;
|
|
28008
28360
|
return geo.map(([k, v]) => {
|
|
28009
|
-
if (k
|
|
28010
|
-
return [k,
|
|
28011
|
-
if (
|
|
28012
|
-
return [k, { ...v,
|
|
28013
|
-
|
|
28361
|
+
if (v.t !== "UDim2" || k !== "Size" && k !== "Position")
|
|
28362
|
+
return [k, v];
|
|
28363
|
+
if (f.scale) {
|
|
28364
|
+
return k === "Size" ? [k, { ...v, xs: num(v.xs + dw / f.w), ys: num(v.ys + dh / f.h) }] : [k, { ...v, xs: num(v.xs + ox / f.w), ys: num(v.ys + oy / f.h) }];
|
|
28365
|
+
}
|
|
28366
|
+
return k === "Size" ? [k, { ...v, xo: Math.round(v.xo + dw), yo: Math.round(v.yo + dh) }] : [k, { ...v, xo: Math.round(v.xo + ox), yo: Math.round(v.yo + oy) }];
|
|
28014
28367
|
});
|
|
28015
28368
|
}
|
|
28016
28369
|
}
|
|
@@ -28021,10 +28374,14 @@ function mapToRoblox(root, opts, assets) {
|
|
|
28021
28374
|
m.warn(null, `${m.counts.skippedHidden} hidden layer(s) skipped`);
|
|
28022
28375
|
return { root: out, warnings: [...m.warnings], fontSubstitutions: Object.fromEntries(m.fontSubstitutions), counts: m.counts };
|
|
28023
28376
|
}
|
|
28024
|
-
var num = (v) => Math.round(v *
|
|
28377
|
+
var num = (v) => Math.round(v * 1e4) / 1e4, S = (v) => ({ t: "string", v }), B = (v) => ({ t: "bool", v }), I = (v) => ({ t: "int", v: Math.round(v) }), F = (v) => ({ t: "float", v: num(v) }), E = (e, v) => ({ t: "enum", e, v }), U = (s, o) => ({ t: "UDim", s: num(s), o: Math.round(o) }), U2 = (xs, xo, ys, yo) => ({ t: "UDim2", xs: num(xs), xo: Math.round(xo), ys: num(ys), yo: Math.round(yo) }), V2 = (x, y) => ({ t: "Vector2", x: num(x), y: num(y) }), C3 = (hex) => ({ t: "Color3", ...color3(hex) }), BUTTON_NAME, VECTOR_TYPES, ICON_WORDS, IMAGE_WORDS;
|
|
28025
28378
|
var init_map2 = __esm(() => {
|
|
28379
|
+
init_naming();
|
|
28026
28380
|
init_fonts();
|
|
28027
|
-
BUTTON_NAME =
|
|
28381
|
+
BUTTON_NAME = /button|btn|\bcta\b|^(close|back|buy|confirm|cancel|next|previous|prev|exit|play|claim|equip|purchase)$/i;
|
|
28382
|
+
VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
|
|
28383
|
+
ICON_WORDS = ["Icon", "Logo", "Image", "Illustration", "Avatar", "Emoji"];
|
|
28384
|
+
IMAGE_WORDS = ["Image", "Icon", "Avatar", "Logo", "Thumbnail", "Picture", "Photo", "Banner", "Background", "Illustration", "Art", "Cover"];
|
|
28028
28385
|
});
|
|
28029
28386
|
|
|
28030
28387
|
// src/roblox/output.ts
|
|
@@ -28149,15 +28506,23 @@ function toLuau(root, opts = {}) {
|
|
|
28149
28506
|
const lines = [];
|
|
28150
28507
|
const emit = (i, parentVar, depth) => {
|
|
28151
28508
|
const v = identifier(i.name, i.className, used);
|
|
28509
|
+
const outer = "\t".repeat(Math.max(0, depth - 1));
|
|
28510
|
+
const tab = "\t".repeat(depth);
|
|
28511
|
+
const add = (line) => lines.push(tab + line.replace(/\n/g, `
|
|
28512
|
+
${tab}`));
|
|
28152
28513
|
if (!/^UI/.test(i.className) && depth <= 2 && parentVar)
|
|
28153
|
-
lines.push("",
|
|
28154
|
-
|
|
28514
|
+
lines.push("", `${outer}-- ${i.name.replace(/\n/g, " ")}`);
|
|
28515
|
+
if (parentVar)
|
|
28516
|
+
lines.push(`${outer}do`);
|
|
28517
|
+
add(`local ${v} = Instance.new(${luauString(i.className)})`);
|
|
28155
28518
|
for (const [k, val] of i.props)
|
|
28156
|
-
|
|
28519
|
+
add(`${v}.${k} = ${luauValue(val)}`);
|
|
28157
28520
|
for (const c of i.children)
|
|
28158
28521
|
emit(c, v, depth + 1);
|
|
28159
|
-
if (parentVar)
|
|
28160
|
-
|
|
28522
|
+
if (parentVar) {
|
|
28523
|
+
add(`${v}.Parent = ${parentVar}`);
|
|
28524
|
+
lines.push(`${outer}end`);
|
|
28525
|
+
}
|
|
28161
28526
|
return v;
|
|
28162
28527
|
};
|
|
28163
28528
|
const header = [
|
|
@@ -28186,9 +28551,10 @@ return ${rootVar}
|
|
|
28186
28551
|
function substituteAssets(text, ids) {
|
|
28187
28552
|
return text.replace(/rbxassetid:\/\/PENDING_\d+/g, (m) => ids[m] ? `rbxassetid://${ids[m]}` : m);
|
|
28188
28553
|
}
|
|
28189
|
-
var ENUMS, n = (v) => String(Math.round(v *
|
|
28554
|
+
var ENUMS, n = (v) => String(Math.round(v * 1e4) / 1e4), xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """), KEYWORDS, rgb = (v) => Math.round(v * 255);
|
|
28190
28555
|
var init_output = __esm(() => {
|
|
28191
28556
|
ENUMS = {
|
|
28557
|
+
DominantAxis: { Width: 0, Height: 1 },
|
|
28192
28558
|
ZIndexBehavior: { Global: 0, Sibling: 1 },
|
|
28193
28559
|
FillDirection: { Horizontal: 0, Vertical: 1 },
|
|
28194
28560
|
HorizontalAlignment: { Center: 0, Left: 1, Right: 2 },
|
|
@@ -28202,7 +28568,9 @@ var init_output = __esm(() => {
|
|
|
28202
28568
|
ScaleType: { Stretch: 0, Slice: 1, Tile: 2, Fit: 3, Crop: 4 },
|
|
28203
28569
|
ApplyStrokeMode: { Contextual: 0, Border: 1 },
|
|
28204
28570
|
LineJoinMode: { Round: 0, Bevel: 1, Miter: 2 },
|
|
28205
|
-
TextTruncate: { None: 0, AtEnd: 1, SplitWord: 2 }
|
|
28571
|
+
TextTruncate: { None: 0, AtEnd: 1, SplitWord: 2 },
|
|
28572
|
+
StrokeSizingMode: { FixedSize: 0, ScaledSize: 1 },
|
|
28573
|
+
BorderStrokePosition: { Outer: 0, Center: 1, Inner: 2 }
|
|
28206
28574
|
};
|
|
28207
28575
|
KEYWORDS = new Set(["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", "continue", "export", "type", "game", "workspace", "script", "PARENT", "existing"]);
|
|
28208
28576
|
});
|
|
@@ -28308,7 +28676,7 @@ import { join as join2 } from "node:path";
|
|
|
28308
28676
|
async function exportRoblox(args, deps) {
|
|
28309
28677
|
const env = deps.env ?? process.env;
|
|
28310
28678
|
const opts = {
|
|
28311
|
-
mode: args.mode ?? "
|
|
28679
|
+
mode: args.mode ?? "scale",
|
|
28312
28680
|
targetResolution: args.targetResolution ?? [1920, 1080],
|
|
28313
28681
|
rasterize: args.rasterize ?? "auto",
|
|
28314
28682
|
asRootFrame: args.asRootFrame,
|
|
@@ -28326,7 +28694,7 @@ async function exportRoblox(args, deps) {
|
|
|
28326
28694
|
const r = await deps.request("roblox_images", { items: wanted.slice(i, i + 20).map((w) => ({ ...w, scale: opts.scale })) }, 120000);
|
|
28327
28695
|
for (const img of r.images) {
|
|
28328
28696
|
if (img.b64)
|
|
28329
|
-
pictures.set(`${img.id}:${img.mode}`, { b64: img.b64, offset: img.offset, size: img.size });
|
|
28697
|
+
pictures.set(`${img.id}:${img.mode}`, { b64: img.b64, offset: img.offset, size: img.size, scale: img.scale });
|
|
28330
28698
|
else
|
|
28331
28699
|
warnings.push(`Picture of ${img.id} failed: ${img.error}`);
|
|
28332
28700
|
}
|
|
@@ -28352,7 +28720,14 @@ async function exportRoblox(args, deps) {
|
|
|
28352
28720
|
const p = pictures.get(`${w.id}:${w.mode}`);
|
|
28353
28721
|
if (!p)
|
|
28354
28722
|
continue;
|
|
28355
|
-
|
|
28723
|
+
const bytes = new Uint8Array(Buffer.from(p.b64, "base64"));
|
|
28724
|
+
const info = imageInfo(bytes);
|
|
28725
|
+
add(`${w.id}:${w.mode}`, bytes, { nodeId: w.id, layer: names.get(w.id) ?? w.id, kind: "picture", mode: w.mode }, "png", {
|
|
28726
|
+
offset: p.offset,
|
|
28727
|
+
size: p.size,
|
|
28728
|
+
scale: p.scale,
|
|
28729
|
+
...info ? { px: { w: info.width, h: info.height } } : {}
|
|
28730
|
+
});
|
|
28356
28731
|
}
|
|
28357
28732
|
for (const hash of imageHashes(t.tree)) {
|
|
28358
28733
|
const b64 = t.images[hash];
|
|
@@ -30441,6 +30816,15 @@ function snapshot(opts) {
|
|
|
30441
30816
|
return false;
|
|
30442
30817
|
}
|
|
30443
30818
|
const SKIP = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "HEAD", "META", "LINK", "TITLE", "BASE"];
|
|
30819
|
+
const skewed = (t) => {
|
|
30820
|
+
if (!t || t === "none")
|
|
30821
|
+
return false;
|
|
30822
|
+
const m = /^matrix(([^)]+))$/.exec(t);
|
|
30823
|
+
if (!m)
|
|
30824
|
+
return true;
|
|
30825
|
+
const v = m[1].split(",").map(Number);
|
|
30826
|
+
return Math.abs(v[1]) > 0.001 || Math.abs(v[2]) > 0.001;
|
|
30827
|
+
};
|
|
30444
30828
|
const RASTER = ["CANVAS", "VIDEO", "IFRAME", "OBJECT", "EMBED", "AUDIO", "MATH"];
|
|
30445
30829
|
function isPlainInline(el, cs) {
|
|
30446
30830
|
if (cs.display !== "inline" || el.tagName === "IMG" || el.tagName === "svg" || el.tagName === "INPUT" || el.tagName === "BUTTON" || el.tagName === "SELECT" || el.tagName === "TEXTAREA")
|
|
@@ -30647,10 +31031,11 @@ function snapshot(opts) {
|
|
|
30647
31031
|
return [{ kind: "image", tag, name, box, css: cssOf(cs), children: [], src: img.currentSrc || img.src }];
|
|
30648
31032
|
}
|
|
30649
31033
|
const nativeControl = tag === "input" && /^(checkbox|radio|range|color|file)$/i.test(el.type) || tag === "select" && cs.appearance !== "none";
|
|
30650
|
-
if (RASTER.indexOf(el.tagName) !== -1 || nativeControl) {
|
|
31034
|
+
if (RASTER.indexOf(el.tagName) !== -1 || nativeControl || skewed(cs.transform) && !(el.textContent || "").trim()) {
|
|
30651
31035
|
if (box.w < 0.5 || box.h < 0.5)
|
|
30652
31036
|
return [];
|
|
30653
|
-
|
|
31037
|
+
el.setAttribute("data-fb-pic", String(count));
|
|
31038
|
+
return [{ kind: "raster", tag, name, box, css: cssOf(cs), children: [], ref: count }];
|
|
30654
31039
|
}
|
|
30655
31040
|
const node = { kind: "box", tag, name, box, css: cssOf(cs), children: [] };
|
|
30656
31041
|
if (tag === "input" || tag === "textarea") {
|
|
@@ -30832,6 +31217,7 @@ async function render(source, opts) {
|
|
|
30832
31217
|
screenshot = opts.selector ? await page.locator(opts.selector).first().screenshot() : await page.screenshot({ fullPage: true, clip });
|
|
30833
31218
|
} catch {}
|
|
30834
31219
|
const r = { viewport: width, snap, screenshot };
|
|
31220
|
+
let queue = Promise.resolve();
|
|
30835
31221
|
await opts.withPage(r, {
|
|
30836
31222
|
fetch: async (u) => {
|
|
30837
31223
|
const res = await context.request.get(u, { timeout: 20000 });
|
|
@@ -30839,7 +31225,24 @@ async function render(source, opts) {
|
|
|
30839
31225
|
throw new Error(`HTTP ${res.status()}`);
|
|
30840
31226
|
return new Uint8Array(await res.body());
|
|
30841
31227
|
},
|
|
30842
|
-
picture:
|
|
31228
|
+
picture: (b, ref) => {
|
|
31229
|
+
const shot = queue.then(async () => {
|
|
31230
|
+
const clip = { x: b.x, y: b.y, width: Math.max(1, b.w), height: Math.max(1, b.h) };
|
|
31231
|
+
if (ref === undefined)
|
|
31232
|
+
return new Uint8Array(await page.screenshot({ fullPage: true, clip, omitBackground: true }));
|
|
31233
|
+
const sel = `[data-fb-pic="${ref}"]`;
|
|
31234
|
+
const style = await page.addStyleTag({
|
|
31235
|
+
content: `html,body{background:transparent!important}body *{visibility:hidden!important}${sel},${sel} *{visibility:visible!important}`
|
|
31236
|
+
});
|
|
31237
|
+
try {
|
|
31238
|
+
return new Uint8Array(await page.screenshot({ fullPage: true, clip, omitBackground: true }));
|
|
31239
|
+
} finally {
|
|
31240
|
+
await style.evaluate((el) => el.remove()).catch(() => {});
|
|
31241
|
+
}
|
|
31242
|
+
});
|
|
31243
|
+
queue = shot.catch(() => {});
|
|
31244
|
+
return shot;
|
|
31245
|
+
}
|
|
30843
31246
|
});
|
|
30844
31247
|
out.push(r);
|
|
30845
31248
|
} finally {
|
|
@@ -31073,13 +31476,13 @@ class Converter {
|
|
|
31073
31476
|
if (this.warnings.size < 40)
|
|
31074
31477
|
this.warnings.add(msg);
|
|
31075
31478
|
}
|
|
31076
|
-
asset(url, box, layer) {
|
|
31479
|
+
asset(url, box, layer, ref) {
|
|
31077
31480
|
if (url && !layer && this.urlKeys.has(url))
|
|
31078
31481
|
return this.urlKeys.get(url);
|
|
31079
31482
|
if (url && layer && this.urlKeys.has(url) && this.assets[this.urlKeys.get(url)].layer)
|
|
31080
31483
|
return this.urlKeys.get(url);
|
|
31081
31484
|
const key = `w${this.n++}`;
|
|
31082
|
-
this.assets[key] = { url, box, layer };
|
|
31485
|
+
this.assets[key] = ref === undefined ? { url, box, layer } : { url, box, layer, ref };
|
|
31083
31486
|
if (url)
|
|
31084
31487
|
this.urlKeys.set(url, key);
|
|
31085
31488
|
return key;
|
|
@@ -31141,7 +31544,7 @@ class Converter {
|
|
|
31141
31544
|
if (n.kind === "image" || n.kind === "raster") {
|
|
31142
31545
|
this.stats.images++;
|
|
31143
31546
|
const fits = { contain: "fit", cover: "fill", none: "crop", "scale-down": "fit" };
|
|
31144
|
-
const key = this.asset(n.kind === "image" ? n.src : undefined, n.box, true);
|
|
31547
|
+
const key = this.asset(n.kind === "image" ? n.src : undefined, n.box, true, n.ref);
|
|
31145
31548
|
return {
|
|
31146
31549
|
type: "image",
|
|
31147
31550
|
name: n.name || TAG_NAMES[n.tag] || "Image",
|
|
@@ -31187,7 +31590,9 @@ class Converter {
|
|
|
31187
31590
|
spec.clip = true;
|
|
31188
31591
|
const flow = n.children.filter((c) => !outOfFlow(c));
|
|
31189
31592
|
const positioned = n.children.filter(outOfFlow);
|
|
31190
|
-
|
|
31593
|
+
let layout = flow.length ? this.layout(n, flow) : null;
|
|
31594
|
+
if (layout && layout.dir === "grid" && !this.opts.grid)
|
|
31595
|
+
layout = null;
|
|
31191
31596
|
const children = [];
|
|
31192
31597
|
if (layout) {
|
|
31193
31598
|
this.stats.autoLayout++;
|
|
@@ -31210,6 +31615,8 @@ class Converter {
|
|
|
31210
31615
|
child.colSpan = item.colSpan;
|
|
31211
31616
|
if (item.rowSpan && item.rowSpan > 1)
|
|
31212
31617
|
child.rowSpan = item.rowSpan;
|
|
31618
|
+
if (layout.dir === "grid")
|
|
31619
|
+
child.place = [r1(item.node.box.x - n.box.x), r1(item.node.box.y - n.box.y), r1(item.node.box.w), r1(item.node.box.h)];
|
|
31213
31620
|
if (item.wrap) {
|
|
31214
31621
|
this.stats.layers++;
|
|
31215
31622
|
children.push({
|
|
@@ -31225,28 +31632,32 @@ class Converter {
|
|
|
31225
31632
|
}
|
|
31226
31633
|
spec.w = layout.fixedW || children.some((c) => c.w === "fill") ? n.box.w : "hug";
|
|
31227
31634
|
spec.h = layout.fixedH || children.some((c) => c.h === "fill") ? n.box.h : "hug";
|
|
31635
|
+
if (layout.dir === "grid")
|
|
31636
|
+
spec.gridSize = [r1(n.box.w), r1(n.box.h)];
|
|
31228
31637
|
} else {
|
|
31229
31638
|
if (flow.length)
|
|
31230
31639
|
this.stats.free++;
|
|
31231
31640
|
spec.w = n.box.w;
|
|
31232
31641
|
spec.h = n.box.h;
|
|
31233
|
-
for (const c of paintOrder(flow)) {
|
|
31234
|
-
const child = this.node(c);
|
|
31235
|
-
if (!child)
|
|
31236
|
-
break;
|
|
31237
|
-
children.push(this.placeAt(child, c, n));
|
|
31238
|
-
}
|
|
31239
31642
|
}
|
|
31240
|
-
|
|
31643
|
+
const free = layout ? positioned : n.children;
|
|
31644
|
+
const floor = layout ? Math.min(...flow.map(stackLevel)) : Infinity;
|
|
31645
|
+
const below = [];
|
|
31646
|
+
for (const c of paintOrder(free)) {
|
|
31241
31647
|
const child = this.node(c);
|
|
31242
31648
|
if (!child)
|
|
31243
31649
|
break;
|
|
31244
|
-
|
|
31650
|
+
const placed = { ...this.placeAt(child, c, n), ...layout ? { absolute: true } : {} };
|
|
31651
|
+
if (layout && stackLevel(c) < floor)
|
|
31652
|
+
below.push(placed);
|
|
31653
|
+
else
|
|
31654
|
+
children.push(placed);
|
|
31245
31655
|
}
|
|
31656
|
+
children.unshift(...below);
|
|
31246
31657
|
if (children.length)
|
|
31247
31658
|
spec.children = children;
|
|
31248
31659
|
if (!spec.name)
|
|
31249
|
-
spec.name = layout ? layout.dir === "grid" ? "Grid" : layout.dir === "row" ? "Row" : "Column" : "
|
|
31660
|
+
spec.name = layout ? layout.dir === "grid" ? "Grid" : layout.dir === "row" ? "Row" : "Column" : "Container";
|
|
31250
31661
|
return spec;
|
|
31251
31662
|
}
|
|
31252
31663
|
placeAt(child, c, parent) {
|
|
@@ -31310,7 +31721,7 @@ class Converter {
|
|
|
31310
31721
|
return span;
|
|
31311
31722
|
});
|
|
31312
31723
|
}
|
|
31313
|
-
if (
|
|
31724
|
+
if ((n.lines ?? 1) > 1 || !n.autoWidth && align)
|
|
31314
31725
|
spec.w = n.box.w;
|
|
31315
31726
|
return spec;
|
|
31316
31727
|
}
|
|
@@ -31345,9 +31756,17 @@ function label(n) {
|
|
|
31345
31756
|
function decorationOf(v) {
|
|
31346
31757
|
return /underline/.test(v) ? "underline" : /line-through/.test(v) ? "strike" : undefined;
|
|
31347
31758
|
}
|
|
31759
|
+
function stackLevel(n) {
|
|
31760
|
+
const z = n.css.zIndex && n.css.zIndex !== "auto" ? Number(n.css.zIndex) || 0 : null;
|
|
31761
|
+
if (z !== null && z < 0)
|
|
31762
|
+
return z - 1;
|
|
31763
|
+
const positioned = !!n.css.position && n.css.position !== "static";
|
|
31764
|
+
if (!positioned && z === null)
|
|
31765
|
+
return 0;
|
|
31766
|
+
return 1 + Math.max(0, z ?? 0);
|
|
31767
|
+
}
|
|
31348
31768
|
function paintOrder(list) {
|
|
31349
|
-
|
|
31350
|
-
return list.map((n, i) => ({ n, i })).sort((a, b) => z(a.n) - z(b.n) || a.i - b.i).map((x) => x.n);
|
|
31769
|
+
return list.map((n, i) => ({ n, i })).sort((a, b) => stackLevel(a.n) - stackLevel(b.n) || a.i - b.i).map((x) => x.n);
|
|
31351
31770
|
}
|
|
31352
31771
|
function padOf(n) {
|
|
31353
31772
|
const b = borderWidths(n.css);
|
|
@@ -31797,7 +32216,7 @@ async function importWeb(opts, deps) {
|
|
|
31797
32216
|
}
|
|
31798
32217
|
if (a.layer) {
|
|
31799
32218
|
try {
|
|
31800
|
-
images[key] = await page.picture(a.box);
|
|
32219
|
+
images[key] = await page.picture(a.box, a.ref);
|
|
31801
32220
|
return;
|
|
31802
32221
|
} catch {}
|
|
31803
32222
|
}
|
|
@@ -32078,7 +32497,7 @@ async function layerNames(session, ids) {
|
|
|
32078
32497
|
return bridge.request("node_info", { ids: [...new Set(ids)] }, 20000).catch(() => ({}));
|
|
32079
32498
|
}
|
|
32080
32499
|
var PORT, CHANNEL, OUT_DIR, LOG_FILE, MAX_OUTPUT_CHARS = 60000, MAX_IMAGE_BYTES, FIGMA_MAX_IMAGE_DIM = 4096, VIEW_MAX_DIM = 1568, bridge, ok = (value) => ({ content: [{ type: "text", text: json(value) }] }), plain = (text) => ({ content: [{ type: "text", text }] }), oneLine = (s, max = 120) => s.replace(/\s+/g, " ").trim().slice(0, max), safeName = (s) => s.replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "node", INSTRUCTIONS, BUILD_DESCRIPTION = `Create a whole layout in ONE call from a declarative spec. Fonts load automatically, icons are fetched, images loaded. Much faster and safer than run_script for new UI.
|
|
32081
|
-
Node: {type?, name?, ...props, children?: Node[]}. type is inferred (text/spans→text, icon→icon, src→image, component→instance, variants→componentSet, else frame). Types: frame, component, componentSet, text, rect, ellipse, line, icon, image, svg, instance.
|
|
32500
|
+
Node: {type?, name?, ...props, children?: Node[]}. Name layers by role (Header, Card, CardList, Title, Description, PrimaryButton, Icon); unnamed layers get a role name (Container, Row, Card, Title, Label, Button, Icon, Divider…). type is inferred (text/spans→text, icon→icon, src→image, component→instance, variants→componentSet, else frame). Types: frame, component, componentSet, text, rect, ellipse, line, icon, image, svg, instance.
|
|
32082
32501
|
FRAME: layout "row"|"column"|"grid" (auto-layout; omit for free positioning), gap (number|"auto"), padding (n | [v,h] | [t,r,b,l]), align (cross axis: start|center|end|baseline), justify (main axis: start|center|end|between), wrap, rowGap, clip. Frames have no fill unless set.
|
|
32083
32502
|
GRID: layout:"grid", columns (count | tracks like [200,"1fr","2fr","hug"]), rows (same; default: enough rows, hugging), columnGap, rowGap (or gap). Children: span:[rows,cols] or colSpan/rowSpan, cellAlign/cellValign. Give the grid a w for "fr" columns.
|
|
32084
32503
|
SIZE: w / h: number (fixed) | "fill" (stretch inside an auto-layout parent) | "hug". Auto-layout frames hug by default. grow:true. absolute:true with x/y inside auto-layout; x/y for children of free frames.
|
|
@@ -32144,6 +32563,7 @@ var init_server3 = __esm(async () => {
|
|
|
32144
32563
|
"Workflow:",
|
|
32145
32564
|
"1. get_context, then get_design_system when the file has styles, variables or components: reuse them instead of raw values. find locates layers by name, text, type, style or component across pages.",
|
|
32146
32565
|
"2. New UI → build (one call per screen or section, with auto-layout; grid, rich text spans, component sets with variants, prototype reactions). Editing existing design → describe it first, then build into it (parentId) or run_script.",
|
|
32566
|
+
"Name every layer by its role, like a design system would: Header, NavBar, HeroSection, Content, PricingCard, CardTitle, Price, PrimaryButton, Avatar & Info, Icon/Search; components as Category/Variant (Button/Primary). Never leave Frame 12 or Rectangle 3: names become class names in code and instance names in Roblox.",
|
|
32147
32567
|
"3. Design system → design_tokens writes variables (with Light/Dark modes) and styles from simple JSON, W3C tokens or a Tailwind theme; build uses them via var:, style: and modes.",
|
|
32148
32568
|
"4. Verify → screenshot with returnImage:true, and audit to catch contrast, overflow and naming issues. Fix, then check again. audit {scope:'design-system'} scores the file's design system; audit {fix:true} binds raw values to variables and styles.",
|
|
32149
32569
|
"Reproduce a mockup or screenshot: build it at the mockup's size → compare {nodeId, reference, returnImage:true} → fix the largest regions → compare again until mismatchPercent stops dropping.",
|
|
@@ -32157,7 +32577,8 @@ var init_server3 = __esm(async () => {
|
|
|
32157
32577
|
"Rules: load fonts before editing text; pages load on demand (await figma.setCurrentPageAsync / getNodeByIdAsync); keep results small.",
|
|
32158
32578
|
"The user can cancel a command from the plugin (code CANCELLED): build stops, a script may still finish, so check the file before retrying.",
|
|
32159
32579
|
"With FIGMA_TOKEN set: comments reads and posts file comments pinned to layers, versions lists the history and diffs a version against now.",
|
|
32160
|
-
"
|
|
32580
|
+
"Names: give every layer you create a professional name for its role, as a design system or a developer would: Screen, Header, Navbar, Sidebar, Section, Container, Card, CardList, Row, Grid, Title, Subtitle, Description, Label, Price, PrimaryButton, Icon, Avatar, Badge, Input, Divider, Background. Never leave Frame 12, Rectangle 3 or a text named after its content; build names unnamed layers by role and audit {fix:true} renames default names.",
|
|
32581
|
+
"Roblox: export_roblox turns a frame into a ScreenGui (.rbxmx + Luau for execute_luau + PNG assets), in scale only (no offsets) with PascalCase names (ShopGui, ItemCard, TitleLabel, BuyButton); with a Roblox Studio MCP, upload the assets, run the Luau and check a Studio screenshot. Any Roblox UI you write by hand follows the same rules: UDim2.fromScale, scale padding and gaps, UIStroke ScaledSize, TextScaled + UITextSizeConstraint, UIAspectRatioConstraint on the root.",
|
|
32161
32582
|
"Prompts new-screen, apply-design-system, reproduce-screenshot, import-website, figma-to-code, audit-and-fix and figma-to-roblox are ready-made workflows; the resources figma-bridge://docs/build-spec and figma-bridge://docs/workflow are the full reference.",
|
|
32162
32583
|
"If no file is connected, tell the user to run Plugins → Development → Figma Bridge in Figma (Ctrl+Alt+P re-runs it). If list_sessions reports versionMismatch, ask them to reopen or update the plugin."
|
|
32163
32584
|
];
|
|
@@ -32692,21 +33113,22 @@ ${f.content}`).join(`
|
|
|
32692
33113
|
server.registerTool("export_roblox", {
|
|
32693
33114
|
title: "Export a frame to Roblox UI",
|
|
32694
33115
|
description: "Turn a Figma frame (default: selection) into Roblox UI that looks the same: a .rbxmx model (a ScreenGui, or a Frame with asRootFrame), an equivalent Luau builder " + `script for execute_luau (creates the UI under PARENT, default StarterGui, replaces a previous copy with the same name, returns the root), and the PNG assets.
|
|
32695
|
-
` + "
|
|
32696
|
-
` + "
|
|
33116
|
+
` + "Everything is in SCALE by default: sizes, positions, UIPadding, list gaps, grid cells, UICorner, UIStroke (StrokeSizingMode ScaledSize) and UIShadow are relative, " + "text uses TextScaled with a UITextSizeConstraint, and nothing is in offset. Instances get professional PascalCase names (ShopGui, ItemCard, TitleLabel, BuyButton, CoinIcon, " + `CardList, Header): the layer's own name, or its role when Figma named it (Frame 12 → Card, Container, Row…).
|
|
33117
|
+
` + "Frames become Frames with UICorner, UIStroke, UIGradient, UIShadow, UIPadding; auto-layout becomes UIListLayout (UIFlexItem for fill) or UIGridLayout; " + "constraints become AnchorPoint + UDim2; text becomes TextLabel with FontFace (Inter → Builder Sans…), RichText for mixed styles; clickable or " + "*Button layers become TextButton/ImageButton. What Roblox can't draw (vectors, icons, blurs, several fills, radial gradients) is rasterized at 2×; panels whose background " + `can't be native become 9-slice images with their content on top.
|
|
33118
|
+
` + "mode: scale (default, recommended); offset (pixels) or hybrid (pixels inside auto-layout) only when the user asks for them. " + "Assets: with upload:true and ROBLOX_API_KEY + ROBLOX_CREATOR_ID set, pictures are uploaded through Open Cloud and their ids written in; otherwise each asset has a " + "placeholder (rbxassetid://PENDING_n): upload the files with your Roblox Studio tools (e.g. upload_image), replace the placeholders in the Luau, run it with execute_luau and check a Studio screenshot. " + 'When writing Roblox UI yourself, follow the same rules: scale only, PascalCase role names. Example: {"nodeId":"12:34"}',
|
|
32697
33119
|
inputSchema: {
|
|
32698
33120
|
nodeId: string2().optional(),
|
|
32699
|
-
mode: _enum(["scale", "offset", "hybrid"]).optional().describe("Default hybrid"),
|
|
33121
|
+
mode: _enum(["scale", "offset", "hybrid"]).optional().describe("Default scale (no offsets); offset or hybrid only if the user asks"),
|
|
32700
33122
|
targetResolution: array(number2().int().min(100).max(8000)).length(2).optional().describe("Screen size the UI is designed for, default [1920, 1080]"),
|
|
32701
33123
|
rasterize: _enum(["auto", "none", "all"]).optional().describe("auto (default): pictures only where Roblox can't draw it; none: approximate instead; all: every styled layer"),
|
|
32702
33124
|
outDir: string2().optional().describe("Folder for the files (default: a new temp folder)"),
|
|
32703
33125
|
upload: boolean2().optional().describe("Upload the pictures through Open Cloud (needs ROBLOX_API_KEY and ROBLOX_CREATOR_ID)"),
|
|
32704
33126
|
asRootFrame: boolean2().optional().describe("Root is a Frame instead of a ScreenGui"),
|
|
32705
|
-
textScaled: boolean2().optional().describe("TextScaled on labels
|
|
33127
|
+
textScaled: boolean2().optional().describe("TextScaled on labels in offset or hybrid mode (always on in scale mode)"),
|
|
32706
33128
|
fonts: object2({}).passthrough().optional().describe('Figma family → Roblox family or rbxasset URL, e.g. {"Inter":"GothamSSm"}'),
|
|
32707
33129
|
parent: string2().optional().describe('Luau expression for the parent, default game:GetService("StarterGui")')
|
|
32708
33130
|
}
|
|
32709
|
-
}, (args) => track("export_roblox", `${args.nodeId ?? "(selection)"} ${args.mode ?? "
|
|
33131
|
+
}, (args) => track("export_roblox", `${args.nodeId ?? "(selection)"} ${args.mode ?? "scale"}`, async () => {
|
|
32710
33132
|
const r = await exportRoblox({ ...args, targetResolution: args.targetResolution, fonts: args.fonts }, { request: (m, p, t) => bridge.request(m, p, t), outDir: OUT_DIR });
|
|
32711
33133
|
let luau = r.luau;
|
|
32712
33134
|
if (luau.length > MAX_OUTPUT_CHARS)
|