@clovis500c/figma-bridge 1.14.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/dist/server.js +135 -44
- package/package.json +1 -1
- package/plugin/code.js +92 -20
- 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.14.
|
|
217
|
+
var version = "1.14.2";
|
|
218
218
|
var init_package = () => {};
|
|
219
219
|
|
|
220
220
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -27735,6 +27735,8 @@ function decide(n, opts) {
|
|
|
27735
27735
|
const strokes = visible(n.strokes);
|
|
27736
27736
|
const other = (n.effects ?? []).filter((e) => e.type !== "DROP_SHADOW");
|
|
27737
27737
|
const hasChildren = !!n.children?.some((c) => !c.hidden);
|
|
27738
|
+
if (n.rasterize && raster !== "none")
|
|
27739
|
+
return { kind: "picture", approximations: ["image file unreadable: pictured as it looks"] };
|
|
27738
27740
|
if (n.vector || VECTOR_TYPES.includes(n.type)) {
|
|
27739
27741
|
return raster === "none" ? { kind: "frame", approximations: ["vector shape skipped (rasterize is none)"] } : { kind: "picture", approximations };
|
|
27740
27742
|
}
|
|
@@ -27896,7 +27898,10 @@ class Mapper {
|
|
|
27896
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)]];
|
|
27897
27899
|
top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex", "AutomaticSize"].includes(k));
|
|
27898
27900
|
top.props.splice(1, 0, ...geo);
|
|
27899
|
-
|
|
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));
|
|
27900
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]);
|
|
27901
27906
|
dedupe(out);
|
|
27902
27907
|
return out;
|
|
@@ -27934,7 +27939,12 @@ class Mapper {
|
|
|
27934
27939
|
out.push(["LayoutOrder", I(index + 1)]);
|
|
27935
27940
|
return out;
|
|
27936
27941
|
}
|
|
27937
|
-
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";
|
|
27938
27948
|
const axis = (pos, size, total, k) => {
|
|
27939
27949
|
const end = total - pos - size;
|
|
27940
27950
|
if (k === "SCALE" || f.scale) {
|
|
@@ -27961,6 +27971,15 @@ class Mapper {
|
|
|
27961
27971
|
out.push(["ZIndex", I(index + 1)]);
|
|
27962
27972
|
return out;
|
|
27963
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
|
+
}
|
|
27964
27983
|
automaticSize(n, d) {
|
|
27965
27984
|
const hug = this.hugs(n, d);
|
|
27966
27985
|
if (!hug.w && !hug.h)
|
|
@@ -28181,6 +28200,9 @@ class Mapper {
|
|
|
28181
28200
|
const isButton = !!n.clickable || BUTTON_NAME.test(n.name);
|
|
28182
28201
|
className = isButton ? "TextButton" : "TextLabel";
|
|
28183
28202
|
props = this.textProps(n);
|
|
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]);
|
|
28184
28206
|
if (isButton)
|
|
28185
28207
|
props.push(["AutoButtonColor", B(false)]);
|
|
28186
28208
|
if ((n.effects ?? []).some((e) => e.type === "DROP_SHADOW"))
|
|
@@ -28188,7 +28210,8 @@ class Mapper {
|
|
|
28188
28210
|
const size = props.find(([k]) => k === "TextSize")[1].v;
|
|
28189
28211
|
const tight = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
|
|
28190
28212
|
const max = this.scaleOnly && tight ? size * 2 : size;
|
|
28191
|
-
|
|
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)]]));
|
|
28192
28215
|
return this.inst(className, this.label(n, isButton ? "button" : "text"), [...common, ...props], children, n.id);
|
|
28193
28216
|
}
|
|
28194
28217
|
if (d.kind === "picture") {
|
|
@@ -28230,20 +28253,14 @@ class Mapper {
|
|
|
28230
28253
|
if (d.kind === "panel") {
|
|
28231
28254
|
this.counts.pictures++;
|
|
28232
28255
|
const a = this.assetUrl(`${n.id}:panel`, n);
|
|
28233
|
-
const scale = this.opts.scale ?? 2;
|
|
28256
|
+
const scale = a.scale ?? this.opts.scale ?? 2;
|
|
28234
28257
|
const inset = Math.ceil((Math.max(0, ...n.radius ?? [0]) + (n.strokeWeight ?? 0) + 2) * scale);
|
|
28235
|
-
const pw = Math.round((a.size?.w ?? n.w) * scale);
|
|
28236
|
-
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);
|
|
28237
28260
|
const ix = Math.min(inset, Math.floor(pw / 2) - 1);
|
|
28238
28261
|
const iy = Math.min(inset, Math.floor(ph / 2) - 1);
|
|
28239
28262
|
className = button ? "ImageButton" : "ImageLabel";
|
|
28240
|
-
props = [
|
|
28241
|
-
["BackgroundTransparency", F(1)],
|
|
28242
|
-
["Image", { t: "Content", url: a.url }],
|
|
28243
|
-
["ScaleType", E("ScaleType", "Slice")],
|
|
28244
|
-
["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
|
|
28245
|
-
["SliceScale", F(1 / scale)]
|
|
28246
|
-
];
|
|
28263
|
+
props = [["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ...this.imageScaling(n, ix, iy, pw, ph, scale)];
|
|
28247
28264
|
if (opacity < 1)
|
|
28248
28265
|
props.push(["ImageTransparency", F(1 - opacity)]);
|
|
28249
28266
|
if (button)
|
|
@@ -28319,14 +28336,27 @@ class Mapper {
|
|
|
28319
28336
|
const flavor = button ? "button" : fills.length || visible(n.strokes).length || d.kind === "panel" ? "frame" : "container";
|
|
28320
28337
|
return this.inst(className, this.label(n, flavor), [...common, ...props], children, n.id);
|
|
28321
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 [
|
|
28346
|
+
["ScaleType", E("ScaleType", "Slice")],
|
|
28347
|
+
["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
|
|
28348
|
+
["SliceScale", F(1 / scale)]
|
|
28349
|
+
];
|
|
28350
|
+
}
|
|
28322
28351
|
pictureGeometry(n, a, geo, parent) {
|
|
28323
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)
|
|
28324
28353
|
return geo;
|
|
28325
28354
|
const f = this.frameOf(n, parent);
|
|
28326
28355
|
const dw = a.size.w - n.w;
|
|
28327
28356
|
const dh = a.size.h - n.h;
|
|
28328
|
-
const
|
|
28329
|
-
const
|
|
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;
|
|
28330
28360
|
return geo.map(([k, v]) => {
|
|
28331
28361
|
if (v.t !== "UDim2" || k !== "Size" && k !== "Position")
|
|
28332
28362
|
return [k, v];
|
|
@@ -28348,7 +28378,7 @@ var num = (v) => Math.round(v * 1e4) / 1e4, S = (v) => ({ t: "string", v }), B =
|
|
|
28348
28378
|
var init_map2 = __esm(() => {
|
|
28349
28379
|
init_naming();
|
|
28350
28380
|
init_fonts();
|
|
28351
|
-
BUTTON_NAME = /button|btn|\bcta\b
|
|
28381
|
+
BUTTON_NAME = /button|btn|\bcta\b|^(close|back|buy|confirm|cancel|next|previous|prev|exit|play|claim|equip|purchase)$/i;
|
|
28352
28382
|
VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
|
|
28353
28383
|
ICON_WORDS = ["Icon", "Logo", "Image", "Illustration", "Avatar", "Emoji"];
|
|
28354
28384
|
IMAGE_WORDS = ["Image", "Icon", "Avatar", "Logo", "Thumbnail", "Picture", "Photo", "Banner", "Background", "Illustration", "Art", "Cover"];
|
|
@@ -28476,15 +28506,23 @@ function toLuau(root, opts = {}) {
|
|
|
28476
28506
|
const lines = [];
|
|
28477
28507
|
const emit = (i, parentVar, depth) => {
|
|
28478
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}`));
|
|
28479
28513
|
if (!/^UI/.test(i.className) && depth <= 2 && parentVar)
|
|
28480
|
-
lines.push("",
|
|
28481
|
-
|
|
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)})`);
|
|
28482
28518
|
for (const [k, val] of i.props)
|
|
28483
|
-
|
|
28519
|
+
add(`${v}.${k} = ${luauValue(val)}`);
|
|
28484
28520
|
for (const c of i.children)
|
|
28485
28521
|
emit(c, v, depth + 1);
|
|
28486
|
-
if (parentVar)
|
|
28487
|
-
|
|
28522
|
+
if (parentVar) {
|
|
28523
|
+
add(`${v}.Parent = ${parentVar}`);
|
|
28524
|
+
lines.push(`${outer}end`);
|
|
28525
|
+
}
|
|
28488
28526
|
return v;
|
|
28489
28527
|
};
|
|
28490
28528
|
const header = [
|
|
@@ -28516,6 +28554,7 @@ function substituteAssets(text, ids) {
|
|
|
28516
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);
|
|
28517
28555
|
var init_output = __esm(() => {
|
|
28518
28556
|
ENUMS = {
|
|
28557
|
+
DominantAxis: { Width: 0, Height: 1 },
|
|
28519
28558
|
ZIndexBehavior: { Global: 0, Sibling: 1 },
|
|
28520
28559
|
FillDirection: { Horizontal: 0, Vertical: 1 },
|
|
28521
28560
|
HorizontalAlignment: { Center: 0, Left: 1, Right: 2 },
|
|
@@ -28655,7 +28694,7 @@ async function exportRoblox(args, deps) {
|
|
|
28655
28694
|
const r = await deps.request("roblox_images", { items: wanted.slice(i, i + 20).map((w) => ({ ...w, scale: opts.scale })) }, 120000);
|
|
28656
28695
|
for (const img of r.images) {
|
|
28657
28696
|
if (img.b64)
|
|
28658
|
-
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 });
|
|
28659
28698
|
else
|
|
28660
28699
|
warnings.push(`Picture of ${img.id} failed: ${img.error}`);
|
|
28661
28700
|
}
|
|
@@ -28681,7 +28720,14 @@ async function exportRoblox(args, deps) {
|
|
|
28681
28720
|
const p = pictures.get(`${w.id}:${w.mode}`);
|
|
28682
28721
|
if (!p)
|
|
28683
28722
|
continue;
|
|
28684
|
-
|
|
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
|
+
});
|
|
28685
28731
|
}
|
|
28686
28732
|
for (const hash of imageHashes(t.tree)) {
|
|
28687
28733
|
const b64 = t.images[hash];
|
|
@@ -30770,6 +30816,15 @@ function snapshot(opts) {
|
|
|
30770
30816
|
return false;
|
|
30771
30817
|
}
|
|
30772
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
|
+
};
|
|
30773
30828
|
const RASTER = ["CANVAS", "VIDEO", "IFRAME", "OBJECT", "EMBED", "AUDIO", "MATH"];
|
|
30774
30829
|
function isPlainInline(el, cs) {
|
|
30775
30830
|
if (cs.display !== "inline" || el.tagName === "IMG" || el.tagName === "svg" || el.tagName === "INPUT" || el.tagName === "BUTTON" || el.tagName === "SELECT" || el.tagName === "TEXTAREA")
|
|
@@ -30976,10 +31031,11 @@ function snapshot(opts) {
|
|
|
30976
31031
|
return [{ kind: "image", tag, name, box, css: cssOf(cs), children: [], src: img.currentSrc || img.src }];
|
|
30977
31032
|
}
|
|
30978
31033
|
const nativeControl = tag === "input" && /^(checkbox|radio|range|color|file)$/i.test(el.type) || tag === "select" && cs.appearance !== "none";
|
|
30979
|
-
if (RASTER.indexOf(el.tagName) !== -1 || nativeControl) {
|
|
31034
|
+
if (RASTER.indexOf(el.tagName) !== -1 || nativeControl || skewed(cs.transform) && !(el.textContent || "").trim()) {
|
|
30980
31035
|
if (box.w < 0.5 || box.h < 0.5)
|
|
30981
31036
|
return [];
|
|
30982
|
-
|
|
31037
|
+
el.setAttribute("data-fb-pic", String(count));
|
|
31038
|
+
return [{ kind: "raster", tag, name, box, css: cssOf(cs), children: [], ref: count }];
|
|
30983
31039
|
}
|
|
30984
31040
|
const node = { kind: "box", tag, name, box, css: cssOf(cs), children: [] };
|
|
30985
31041
|
if (tag === "input" || tag === "textarea") {
|
|
@@ -31161,6 +31217,7 @@ async function render(source, opts) {
|
|
|
31161
31217
|
screenshot = opts.selector ? await page.locator(opts.selector).first().screenshot() : await page.screenshot({ fullPage: true, clip });
|
|
31162
31218
|
} catch {}
|
|
31163
31219
|
const r = { viewport: width, snap, screenshot };
|
|
31220
|
+
let queue = Promise.resolve();
|
|
31164
31221
|
await opts.withPage(r, {
|
|
31165
31222
|
fetch: async (u) => {
|
|
31166
31223
|
const res = await context.request.get(u, { timeout: 20000 });
|
|
@@ -31168,7 +31225,24 @@ async function render(source, opts) {
|
|
|
31168
31225
|
throw new Error(`HTTP ${res.status()}`);
|
|
31169
31226
|
return new Uint8Array(await res.body());
|
|
31170
31227
|
},
|
|
31171
|
-
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
|
+
}
|
|
31172
31246
|
});
|
|
31173
31247
|
out.push(r);
|
|
31174
31248
|
} finally {
|
|
@@ -31402,13 +31476,13 @@ class Converter {
|
|
|
31402
31476
|
if (this.warnings.size < 40)
|
|
31403
31477
|
this.warnings.add(msg);
|
|
31404
31478
|
}
|
|
31405
|
-
asset(url, box, layer) {
|
|
31479
|
+
asset(url, box, layer, ref) {
|
|
31406
31480
|
if (url && !layer && this.urlKeys.has(url))
|
|
31407
31481
|
return this.urlKeys.get(url);
|
|
31408
31482
|
if (url && layer && this.urlKeys.has(url) && this.assets[this.urlKeys.get(url)].layer)
|
|
31409
31483
|
return this.urlKeys.get(url);
|
|
31410
31484
|
const key = `w${this.n++}`;
|
|
31411
|
-
this.assets[key] = { url, box, layer };
|
|
31485
|
+
this.assets[key] = ref === undefined ? { url, box, layer } : { url, box, layer, ref };
|
|
31412
31486
|
if (url)
|
|
31413
31487
|
this.urlKeys.set(url, key);
|
|
31414
31488
|
return key;
|
|
@@ -31470,7 +31544,7 @@ class Converter {
|
|
|
31470
31544
|
if (n.kind === "image" || n.kind === "raster") {
|
|
31471
31545
|
this.stats.images++;
|
|
31472
31546
|
const fits = { contain: "fit", cover: "fill", none: "crop", "scale-down": "fit" };
|
|
31473
|
-
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);
|
|
31474
31548
|
return {
|
|
31475
31549
|
type: "image",
|
|
31476
31550
|
name: n.name || TAG_NAMES[n.tag] || "Image",
|
|
@@ -31516,7 +31590,9 @@ class Converter {
|
|
|
31516
31590
|
spec.clip = true;
|
|
31517
31591
|
const flow = n.children.filter((c) => !outOfFlow(c));
|
|
31518
31592
|
const positioned = n.children.filter(outOfFlow);
|
|
31519
|
-
|
|
31593
|
+
let layout = flow.length ? this.layout(n, flow) : null;
|
|
31594
|
+
if (layout && layout.dir === "grid" && !this.opts.grid)
|
|
31595
|
+
layout = null;
|
|
31520
31596
|
const children = [];
|
|
31521
31597
|
if (layout) {
|
|
31522
31598
|
this.stats.autoLayout++;
|
|
@@ -31539,6 +31615,8 @@ class Converter {
|
|
|
31539
31615
|
child.colSpan = item.colSpan;
|
|
31540
31616
|
if (item.rowSpan && item.rowSpan > 1)
|
|
31541
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)];
|
|
31542
31620
|
if (item.wrap) {
|
|
31543
31621
|
this.stats.layers++;
|
|
31544
31622
|
children.push({
|
|
@@ -31554,28 +31632,32 @@ class Converter {
|
|
|
31554
31632
|
}
|
|
31555
31633
|
spec.w = layout.fixedW || children.some((c) => c.w === "fill") ? n.box.w : "hug";
|
|
31556
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)];
|
|
31557
31637
|
} else {
|
|
31558
31638
|
if (flow.length)
|
|
31559
31639
|
this.stats.free++;
|
|
31560
31640
|
spec.w = n.box.w;
|
|
31561
31641
|
spec.h = n.box.h;
|
|
31562
|
-
for (const c of paintOrder(flow)) {
|
|
31563
|
-
const child = this.node(c);
|
|
31564
|
-
if (!child)
|
|
31565
|
-
break;
|
|
31566
|
-
children.push(this.placeAt(child, c, n));
|
|
31567
|
-
}
|
|
31568
31642
|
}
|
|
31569
|
-
|
|
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)) {
|
|
31570
31647
|
const child = this.node(c);
|
|
31571
31648
|
if (!child)
|
|
31572
31649
|
break;
|
|
31573
|
-
|
|
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);
|
|
31574
31655
|
}
|
|
31656
|
+
children.unshift(...below);
|
|
31575
31657
|
if (children.length)
|
|
31576
31658
|
spec.children = children;
|
|
31577
31659
|
if (!spec.name)
|
|
31578
|
-
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";
|
|
31579
31661
|
return spec;
|
|
31580
31662
|
}
|
|
31581
31663
|
placeAt(child, c, parent) {
|
|
@@ -31639,7 +31721,7 @@ class Converter {
|
|
|
31639
31721
|
return span;
|
|
31640
31722
|
});
|
|
31641
31723
|
}
|
|
31642
|
-
if (
|
|
31724
|
+
if ((n.lines ?? 1) > 1 || !n.autoWidth && align)
|
|
31643
31725
|
spec.w = n.box.w;
|
|
31644
31726
|
return spec;
|
|
31645
31727
|
}
|
|
@@ -31674,9 +31756,17 @@ function label(n) {
|
|
|
31674
31756
|
function decorationOf(v) {
|
|
31675
31757
|
return /underline/.test(v) ? "underline" : /line-through/.test(v) ? "strike" : undefined;
|
|
31676
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
|
+
}
|
|
31677
31768
|
function paintOrder(list) {
|
|
31678
|
-
|
|
31679
|
-
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);
|
|
31680
31770
|
}
|
|
31681
31771
|
function padOf(n) {
|
|
31682
31772
|
const b = borderWidths(n.css);
|
|
@@ -32126,7 +32216,7 @@ async function importWeb(opts, deps) {
|
|
|
32126
32216
|
}
|
|
32127
32217
|
if (a.layer) {
|
|
32128
32218
|
try {
|
|
32129
|
-
images[key] = await page.picture(a.box);
|
|
32219
|
+
images[key] = await page.picture(a.box, a.ref);
|
|
32130
32220
|
return;
|
|
32131
32221
|
} catch {}
|
|
32132
32222
|
}
|
|
@@ -32473,6 +32563,7 @@ var init_server3 = __esm(async () => {
|
|
|
32473
32563
|
"Workflow:",
|
|
32474
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.",
|
|
32475
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.",
|
|
32476
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.",
|
|
32477
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.",
|
|
32478
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.",
|
package/package.json
CHANGED
package/plugin/code.js
CHANGED
|
@@ -2599,6 +2599,8 @@
|
|
|
2599
2599
|
if (mode === "GRID") {
|
|
2600
2600
|
if (await setupGrid(f, s, ctx, path))
|
|
2601
2601
|
return;
|
|
2602
|
+
if (Array.isArray(s.gridSize))
|
|
2603
|
+
return placeGridItems(f, s);
|
|
2602
2604
|
mode = "HORIZONTAL";
|
|
2603
2605
|
s = Object.assign({}, s, { wrap: true, rowGap: s.rowGap !== undefined ? s.rowGap : s.gap, gap: s.columnGap !== undefined ? s.columnGap : s.gap });
|
|
2604
2606
|
}
|
|
@@ -2643,7 +2645,28 @@
|
|
|
2643
2645
|
return { type: "FIXED", value: parseFloat(px[1]) };
|
|
2644
2646
|
return fallback === "FLEX" ? { type: "FLEX", value: 1 } : { type: "HUG" };
|
|
2645
2647
|
}
|
|
2648
|
+
var GRID_FALLBACK = `Figma's grid track API failed (a Figma bug: "invalid id"): grids were built as wrapping rows, or at their measured positions for imported pages.`;
|
|
2649
|
+
var gridTracksBroken = false;
|
|
2650
|
+
function tracksReadable(f) {
|
|
2651
|
+
try {
|
|
2652
|
+
const t = f.gridColumnSizes[0];
|
|
2653
|
+
return !!t && typeof t.type === "string";
|
|
2654
|
+
} catch (e) {
|
|
2655
|
+
return false;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2646
2658
|
async function setupGrid(f, s, ctx, path) {
|
|
2659
|
+
const fallback = function() {
|
|
2660
|
+
gridTracksBroken = true;
|
|
2661
|
+
try {
|
|
2662
|
+
f.layoutMode = "NONE";
|
|
2663
|
+
} catch (e) {}
|
|
2664
|
+
if (ctx.warnings.indexOf(GRID_FALLBACK) === -1)
|
|
2665
|
+
ctx.warnings.push(GRID_FALLBACK);
|
|
2666
|
+
return false;
|
|
2667
|
+
};
|
|
2668
|
+
if (gridTracksBroken)
|
|
2669
|
+
return fallback();
|
|
2647
2670
|
try {
|
|
2648
2671
|
f.layoutMode = "GRID";
|
|
2649
2672
|
} catch (e) {
|
|
@@ -2673,14 +2696,16 @@
|
|
|
2673
2696
|
set("rows", function() {
|
|
2674
2697
|
f.gridRowCount = rowCount;
|
|
2675
2698
|
});
|
|
2676
|
-
|
|
2699
|
+
if (!tracksReadable(f))
|
|
2700
|
+
return fallback();
|
|
2701
|
+
try {
|
|
2677
2702
|
for (let i = 0;i < colCount; i++)
|
|
2678
|
-
applyTrack(f
|
|
2679
|
-
});
|
|
2680
|
-
set("row sizes", function() {
|
|
2703
|
+
applyTrack(f, "gridColumnSizes", i, track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
|
|
2681
2704
|
for (let i = 0;i < rowCount; i++)
|
|
2682
|
-
applyTrack(f
|
|
2683
|
-
})
|
|
2705
|
+
applyTrack(f, "gridRowSizes", i, track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
|
|
2706
|
+
} catch (e) {
|
|
2707
|
+
return fallback();
|
|
2708
|
+
}
|
|
2684
2709
|
if (!fixedW)
|
|
2685
2710
|
set("sizing", function() {
|
|
2686
2711
|
f.layoutSizingHorizontal = "HUG";
|
|
@@ -2703,12 +2728,30 @@
|
|
|
2703
2728
|
await setPadding(f, s.padding);
|
|
2704
2729
|
return true;
|
|
2705
2730
|
}
|
|
2706
|
-
function
|
|
2707
|
-
if (
|
|
2731
|
+
function placeGridItems(f, s) {
|
|
2732
|
+
if (s.w === "hug" || s.w === undefined)
|
|
2733
|
+
s.w = s.gridSize[0];
|
|
2734
|
+
if (s.h === "hug" || s.h === undefined)
|
|
2735
|
+
s.h = s.gridSize[1];
|
|
2736
|
+
const kids = Array.isArray(s.children) ? s.children : [];
|
|
2737
|
+
for (let i = 0;i < kids.length; i++) {
|
|
2738
|
+
const p = kids[i] && kids[i].place;
|
|
2739
|
+
if (!Array.isArray(p))
|
|
2740
|
+
continue;
|
|
2741
|
+
kids[i].x = p[0];
|
|
2742
|
+
kids[i].y = p[1];
|
|
2743
|
+
kids[i].w = p[2];
|
|
2744
|
+
if (kids[i].type !== "text" || kids[i].h !== undefined)
|
|
2745
|
+
kids[i].h = p[3];
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function applyTrack(f, field, i, spec) {
|
|
2749
|
+
if (!f[field][i])
|
|
2708
2750
|
return;
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2751
|
+
if (f[field][i].type !== spec.type)
|
|
2752
|
+
f[field][i].type = spec.type;
|
|
2753
|
+
if (spec.value !== undefined && spec.type !== "HUG" && f[field][i].value !== spec.value)
|
|
2754
|
+
f[field][i].value = spec.value;
|
|
2712
2755
|
}
|
|
2713
2756
|
function spanOf(s, axis) {
|
|
2714
2757
|
if (!s || typeof s !== "object")
|
|
@@ -3127,10 +3170,13 @@
|
|
|
3127
3170
|
const right = v[1] === undefined ? v[0] : v[1];
|
|
3128
3171
|
const bottom = v[2] === undefined ? v[0] : v[2];
|
|
3129
3172
|
const left = v[3] === undefined ? right : v[3];
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
await setNumber(f, "
|
|
3173
|
+
const clamp = function(x) {
|
|
3174
|
+
return typeof x === "number" ? Math.max(0, x) : x;
|
|
3175
|
+
};
|
|
3176
|
+
await setNumber(f, "paddingTop", clamp(top));
|
|
3177
|
+
await setNumber(f, "paddingRight", clamp(right));
|
|
3178
|
+
await setNumber(f, "paddingBottom", clamp(bottom));
|
|
3179
|
+
await setNumber(f, "paddingLeft", clamp(left));
|
|
3134
3180
|
}
|
|
3135
3181
|
function lineHeight(v) {
|
|
3136
3182
|
if (v === "auto")
|
|
@@ -4314,7 +4360,13 @@
|
|
|
4314
4360
|
const image = figma.getImageByHash(h);
|
|
4315
4361
|
if (!image)
|
|
4316
4362
|
continue;
|
|
4317
|
-
|
|
4363
|
+
let bytes;
|
|
4364
|
+
try {
|
|
4365
|
+
bytes = await image.getBytesAsync();
|
|
4366
|
+
} catch (e) {
|
|
4367
|
+
out.rasterize = true;
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4318
4370
|
if (ctx.imageBytes + bytes.length > 40 << 20)
|
|
4319
4371
|
continue;
|
|
4320
4372
|
ctx.imageBytes += bytes.length;
|
|
@@ -4386,22 +4438,39 @@
|
|
|
4386
4438
|
})
|
|
4387
4439
|
};
|
|
4388
4440
|
}
|
|
4441
|
+
function withTimeout(p, ms, message) {
|
|
4442
|
+
return new Promise(function(resolve, reject) {
|
|
4443
|
+
const timer = setTimeout(function() {
|
|
4444
|
+
reject(new Error(message));
|
|
4445
|
+
}, ms);
|
|
4446
|
+
p.then(function(v) {
|
|
4447
|
+
clearTimeout(timer);
|
|
4448
|
+
resolve(v);
|
|
4449
|
+
}, function(e) {
|
|
4450
|
+
clearTimeout(timer);
|
|
4451
|
+
reject(e);
|
|
4452
|
+
});
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4389
4455
|
async function robloxImages(p) {
|
|
4390
4456
|
const items = Array.isArray(p.items) ? p.items : [];
|
|
4391
4457
|
const out = [];
|
|
4392
4458
|
for (let i = 0;i < items.length; i++) {
|
|
4393
4459
|
const it = items[i];
|
|
4394
|
-
|
|
4460
|
+
let scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
|
|
4395
4461
|
let temp = null;
|
|
4396
4462
|
try {
|
|
4397
4463
|
let node = await getNode(it.id);
|
|
4398
4464
|
if (it.mode === "panel" || it.mode === "shadow") {
|
|
4399
4465
|
temp = node.clone();
|
|
4466
|
+
figma.currentPage.appendChild(temp);
|
|
4400
4467
|
if ("children" in temp)
|
|
4401
4468
|
for (let k = temp.children.length - 1;k >= 0; k--)
|
|
4402
4469
|
temp.children[k].remove();
|
|
4403
4470
|
if (it.mode === "panel")
|
|
4404
|
-
temp.effects =
|
|
4471
|
+
temp.effects = node.effects.filter(function(e) {
|
|
4472
|
+
return e.type !== "DROP_SHADOW";
|
|
4473
|
+
});
|
|
4405
4474
|
else {
|
|
4406
4475
|
temp.effects = node.effects.filter(function(e) {
|
|
4407
4476
|
return e.type === "DROP_SHADOW" && e.visible !== false;
|
|
@@ -4410,7 +4479,10 @@
|
|
|
4410
4479
|
}
|
|
4411
4480
|
node = temp;
|
|
4412
4481
|
}
|
|
4413
|
-
const
|
|
4482
|
+
const box = node.absoluteRenderBounds || node.absoluteBoundingBox;
|
|
4483
|
+
if (box)
|
|
4484
|
+
scale = Math.min(scale, Math.floor(1024 / Math.max(1, box.width, box.height) * 1000) / 1000);
|
|
4485
|
+
const bytes = await withTimeout(node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } }), 20000, "export timed out");
|
|
4414
4486
|
const rb = node.absoluteRenderBounds || node.absoluteBoundingBox;
|
|
4415
4487
|
const bb = node.absoluteBoundingBox;
|
|
4416
4488
|
out.push({
|
|
@@ -4500,7 +4572,7 @@
|
|
|
4500
4572
|
finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
|
|
4501
4573
|
}
|
|
4502
4574
|
// package.json
|
|
4503
|
-
var version = "1.14.
|
|
4575
|
+
var version = "1.14.2";
|
|
4504
4576
|
|
|
4505
4577
|
// plugin/code.ts
|
|
4506
4578
|
var DEFAULT_SIZE = { width: 340, height: 540 };
|
package/plugin/ui.html
CHANGED
|
@@ -523,7 +523,8 @@ const partials = new Map();
|
|
|
523
523
|
function encode(msg) {
|
|
524
524
|
const text = JSON.stringify(msg);
|
|
525
525
|
if (text.length <= CHUNK) return [text];
|
|
526
|
-
|
|
526
|
+
// Not crypto.randomUUID: the plugin iframe is not a secure context, where it is undefined.
|
|
527
|
+
const cid = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
527
528
|
const n = Math.ceil(text.length / CHUNK);
|
|
528
529
|
const frames = [];
|
|
529
530
|
for (let i = 0; i < n; i++) frames.push(JSON.stringify({ t: "chunk", cid, i, n, d: text.slice(i * CHUNK, (i + 1) * CHUNK) }));
|
|
@@ -541,7 +542,15 @@ function decode(raw) {
|
|
|
541
542
|
}
|
|
542
543
|
function send(msg) {
|
|
543
544
|
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
544
|
-
|
|
545
|
+
let frames;
|
|
546
|
+
try {
|
|
547
|
+
frames = encode(msg);
|
|
548
|
+
} catch (e) {
|
|
549
|
+
// A reply that can't be encoded still answers the request, so the agent doesn't wait for a timeout.
|
|
550
|
+
if (msg.t !== "res") return false;
|
|
551
|
+
frames = encode({ t: "res", id: msg.id, ok: false, error: "Could not send the result: " + ((e && e.message) || e), code: "PLUGIN_ERROR" });
|
|
552
|
+
}
|
|
553
|
+
for (const f of frames) ws.send(f);
|
|
545
554
|
return true;
|
|
546
555
|
}
|
|
547
556
|
|