@ttsalpha/mirra 1.1.0 → 1.2.0
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 +6 -2
- package/dist/index.js +69 -24
- package/dist/plugin/main.js +40 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ttsalpha/mirra/refs/heads/main/assets/icon.png" width="96" height="96" alt="" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Mirra</h1>
|
|
2
6
|
|
|
3
7
|
Two-way MCP server for Figma: an agent reads a design and renders one back through the
|
|
4
8
|
Figma Plugin API, so it needs no REST token and has no per-month call limit.
|
|
@@ -104,7 +108,7 @@ the tool when `MIRRA_ALLOW_EXEC` is set, and the plugin only runs it while **All
|
|
|
104
108
|
execution** is ticked in its panel. The tick resets every time the plugin runs.
|
|
105
109
|
|
|
106
110
|
```sh
|
|
107
|
-
claude mcp add -s user
|
|
111
|
+
claude mcp add -s user Mirra-exec -e MIRRA_ALLOW_EXEC=1 -- npx -y @ttsalpha/mirra serve
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
The socket listens on loopback and refuses any browser origin other than Figma's, so a page
|
package/dist/index.js
CHANGED
|
@@ -32495,15 +32495,19 @@ var PatchResult = external_exports.object({
|
|
|
32495
32495
|
keys: external_exports.record(external_exports.string(), external_exports.string())
|
|
32496
32496
|
});
|
|
32497
32497
|
var ScreenshotParams = external_exports.object({
|
|
32498
|
-
/** Defaults to the current selection
|
|
32498
|
+
/** Defaults to the current selection. */
|
|
32499
32499
|
target: Target.optional(),
|
|
32500
|
-
scale: external_exports.number().min(0.1).max(4).optional()
|
|
32500
|
+
scale: external_exports.number().min(0.1).max(4).optional(),
|
|
32501
|
+
/** Longest edge in pixels; the scale is lowered to fit, not refused. */
|
|
32502
|
+
maxEdge: external_exports.number().int().min(64).max(4096).optional()
|
|
32501
32503
|
});
|
|
32502
32504
|
var ScreenshotResult = external_exports.object({
|
|
32503
32505
|
id: external_exports.string(),
|
|
32504
32506
|
name: external_exports.string(),
|
|
32505
32507
|
width: external_exports.number(),
|
|
32506
32508
|
height: external_exports.number(),
|
|
32509
|
+
/** What the export used, once maxEdge had its say. */
|
|
32510
|
+
scale: external_exports.number(),
|
|
32507
32511
|
/** base64 PNG. */
|
|
32508
32512
|
png: external_exports.string()
|
|
32509
32513
|
});
|
|
@@ -32620,7 +32624,9 @@ var ReadParams = external_exports.object({
|
|
|
32620
32624
|
/** Defaults to the current selection. */
|
|
32621
32625
|
nodeId: external_exports.string().optional(),
|
|
32622
32626
|
/** How many levels of children to walk. */
|
|
32623
|
-
depth: external_exports.number().int().min(0).max(20).optional()
|
|
32627
|
+
depth: external_exports.number().int().min(0).max(20).optional(),
|
|
32628
|
+
/** Most nodes to return, so one call cannot fill the context. */
|
|
32629
|
+
limit: external_exports.number().int().min(1).max(2e3).optional()
|
|
32624
32630
|
});
|
|
32625
32631
|
var ReadResult = external_exports.object({
|
|
32626
32632
|
nodes: external_exports.array(external_exports.object({ id: external_exports.string(), tree: UINode })),
|
|
@@ -32631,7 +32637,8 @@ var PingResult = external_exports.object({
|
|
|
32631
32637
|
fileName: external_exports.string(),
|
|
32632
32638
|
fileKey: external_exports.string().nullable(),
|
|
32633
32639
|
currentPage: external_exports.string(),
|
|
32634
|
-
selectionCount: external_exports.number()
|
|
32640
|
+
selectionCount: external_exports.number(),
|
|
32641
|
+
pluginVersion: external_exports.string()
|
|
32635
32642
|
});
|
|
32636
32643
|
|
|
32637
32644
|
// src/clients.ts
|
|
@@ -32640,7 +32647,8 @@ import { access, copyFile, readFile, rename, writeFile } from "node:fs/promises"
|
|
|
32640
32647
|
import { homedir } from "node:os";
|
|
32641
32648
|
import { dirname, join } from "node:path";
|
|
32642
32649
|
import { realpath } from "node:fs/promises";
|
|
32643
|
-
var SERVER_NAME = "
|
|
32650
|
+
var SERVER_NAME = "Mirra";
|
|
32651
|
+
var LEGACY_NAME = "mirra";
|
|
32644
32652
|
async function stableNodePath() {
|
|
32645
32653
|
try {
|
|
32646
32654
|
return await realpath(process.execPath);
|
|
@@ -32684,6 +32692,7 @@ async function writeEntry(target, node2, entryPoint) {
|
|
|
32684
32692
|
const raw = await readFile(target.file, "utf8");
|
|
32685
32693
|
const config2 = JSON.parse(raw);
|
|
32686
32694
|
const servers = config2[target.key] ?? {};
|
|
32695
|
+
delete servers[LEGACY_NAME];
|
|
32687
32696
|
servers[SERVER_NAME] = {
|
|
32688
32697
|
...target.typed ? { type: "stdio" } : {},
|
|
32689
32698
|
command: node2,
|
|
@@ -32708,6 +32717,7 @@ function claudeCode(node2, entryPoint) {
|
|
|
32708
32717
|
const probe = spawnSync("claude", ["--version"], { stdio: "ignore" });
|
|
32709
32718
|
if (probe.error)
|
|
32710
32719
|
return { client: "Claude Code", status: "skipped", detail: "claude not on PATH" };
|
|
32720
|
+
spawnSync("claude", ["mcp", "remove", "-s", "user", LEGACY_NAME], { stdio: "ignore" });
|
|
32711
32721
|
const result = spawnSync(
|
|
32712
32722
|
"claude",
|
|
32713
32723
|
["mcp", "add", "-s", "user", SERVER_NAME, "--", node2, entryPoint, "serve"],
|
|
@@ -32724,7 +32734,7 @@ function claudeCode(node2, entryPoint) {
|
|
|
32724
32734
|
|
|
32725
32735
|
// src/install.ts
|
|
32726
32736
|
import { spawn } from "node:child_process";
|
|
32727
|
-
import { access as access2, cp, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
32737
|
+
import { access as access2, cp, mkdir, readFile as readFile2, rm } from "node:fs/promises";
|
|
32728
32738
|
import { homedir as homedir2, platform } from "node:os";
|
|
32729
32739
|
import { join as join2, resolve, sep } from "node:path";
|
|
32730
32740
|
import { fileURLToPath } from "node:url";
|
|
@@ -32733,9 +32743,9 @@ var PLUGIN_HOME = join2(MIRRA_HOME, "plugin");
|
|
|
32733
32743
|
var SERVER_HOME = join2(MIRRA_HOME, "server");
|
|
32734
32744
|
var DIST = fileURLToPath(new URL(".", import.meta.url));
|
|
32735
32745
|
var BUNDLED_PLUGIN = resolve(DIST, "plugin");
|
|
32736
|
-
async function installPlugin() {
|
|
32746
|
+
async function installPlugin(from = BUNDLED_PLUGIN) {
|
|
32737
32747
|
await mkdir(PLUGIN_HOME, { recursive: true });
|
|
32738
|
-
await cp(
|
|
32748
|
+
await cp(from, PLUGIN_HOME, { recursive: true });
|
|
32739
32749
|
const manifest = join2(PLUGIN_HOME, "manifest.json");
|
|
32740
32750
|
return { manifest, version: await installedVersion() };
|
|
32741
32751
|
}
|
|
@@ -32755,11 +32765,18 @@ async function isInstalled() {
|
|
|
32755
32765
|
return false;
|
|
32756
32766
|
}
|
|
32757
32767
|
}
|
|
32758
|
-
|
|
32759
|
-
|
|
32760
|
-
|
|
32768
|
+
function isTransientInstall(directory) {
|
|
32769
|
+
return directory.includes(`${sep}_npx${sep}`);
|
|
32770
|
+
}
|
|
32771
|
+
async function installServer(from = DIST) {
|
|
32772
|
+
if (!isTransientInstall(from)) return resolve(from, "index.js");
|
|
32773
|
+
const plugin = resolve(from, "plugin");
|
|
32774
|
+
await rm(SERVER_HOME, { recursive: true, force: true });
|
|
32761
32775
|
await mkdir(SERVER_HOME, { recursive: true });
|
|
32762
|
-
await cp(
|
|
32776
|
+
await cp(from, SERVER_HOME, {
|
|
32777
|
+
recursive: true,
|
|
32778
|
+
filter: (source) => source !== plugin && !source.startsWith(plugin + sep)
|
|
32779
|
+
});
|
|
32763
32780
|
return join2(SERVER_HOME, "index.js");
|
|
32764
32781
|
}
|
|
32765
32782
|
function reveal(path) {
|
|
@@ -40803,6 +40820,9 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
40803
40820
|
}
|
|
40804
40821
|
};
|
|
40805
40822
|
|
|
40823
|
+
// src/version.ts
|
|
40824
|
+
var SERVER_VERSION = "1.2.0";
|
|
40825
|
+
|
|
40806
40826
|
// src/errors.ts
|
|
40807
40827
|
var MirraError = class extends Error {
|
|
40808
40828
|
constructor(message, code, hint) {
|
|
@@ -41048,11 +41068,18 @@ function registerPing(server, bridge) {
|
|
|
41048
41068
|
async () => {
|
|
41049
41069
|
try {
|
|
41050
41070
|
const result = PingResult.parse(await bridge.request("ping"));
|
|
41071
|
+
const drifted = result.pluginVersion !== SERVER_VERSION;
|
|
41051
41072
|
return text([
|
|
41052
41073
|
`file: ${result.fileName}${result.fileKey ? ` (${result.fileKey})` : ""}`,
|
|
41053
41074
|
`page: ${result.currentPage}`,
|
|
41054
41075
|
`selection: ${result.selectionCount} node(s)`,
|
|
41055
|
-
`port: ${bridge.status.port}
|
|
41076
|
+
`port: ${bridge.status.port}`,
|
|
41077
|
+
`server ${SERVER_VERSION}, plugin ${result.pluginVersion}`,
|
|
41078
|
+
...drifted ? [
|
|
41079
|
+
"",
|
|
41080
|
+
"The two halves are different versions. Run `npx @ttsalpha/mirra init`, then run",
|
|
41081
|
+
"the plugin again in Figma so both come from the same release."
|
|
41082
|
+
] : []
|
|
41056
41083
|
]);
|
|
41057
41084
|
} catch (error62) {
|
|
41058
41085
|
return text([describeError(error62)], true);
|
|
@@ -41064,7 +41091,8 @@ function registerPing(server, bridge) {
|
|
|
41064
41091
|
// src/tools/read.ts
|
|
41065
41092
|
var DESCRIPTION5 = `Read layers out of the open Figma file as the same tree mirra_render takes,
|
|
41066
41093
|
so a design can be edited or turned into code without learning a second format.
|
|
41067
|
-
Reads the current selection unless a nodeId is given
|
|
41094
|
+
Reads the current selection unless a nodeId is given, and at most 200 nodes unless limit
|
|
41095
|
+
says otherwise, so a large design cannot fill the context in one call.`;
|
|
41068
41096
|
function registerRead(server, bridge) {
|
|
41069
41097
|
server.registerTool(
|
|
41070
41098
|
"mirra_read",
|
|
@@ -41076,7 +41104,9 @@ function registerRead(server, bridge) {
|
|
|
41076
41104
|
async (params) => {
|
|
41077
41105
|
try {
|
|
41078
41106
|
const result = ReadResult.parse(await bridge.request("read", params));
|
|
41079
|
-
const note = result.skipped ? [
|
|
41107
|
+
const note = result.skipped ? [
|
|
41108
|
+
`${result.skipped} node(s) left out: unsupported type, or past the depth or node limit`
|
|
41109
|
+
] : [];
|
|
41080
41110
|
return text([...note, JSON.stringify(result.nodes, null, 2)]);
|
|
41081
41111
|
} catch (error62) {
|
|
41082
41112
|
return text([describeError(error62)], true);
|
|
@@ -41119,7 +41149,8 @@ function registerRender(server, bridge) {
|
|
|
41119
41149
|
|
|
41120
41150
|
// src/tools/screenshot.ts
|
|
41121
41151
|
var DESCRIPTION7 = `Export a layer as a PNG and return the image, so the rendered result can be
|
|
41122
|
-
checked by eye and corrected with mirra_patch. Defaults to the current selection
|
|
41152
|
+
checked by eye and corrected with mirra_patch. Defaults to the current selection. The scale
|
|
41153
|
+
is lowered when it has to be, so the longest edge stays within maxEdge, 1200px by default.`;
|
|
41123
41154
|
function registerScreenshot(server, bridge) {
|
|
41124
41155
|
server.registerTool(
|
|
41125
41156
|
"mirra_screenshot",
|
|
@@ -41167,9 +41198,8 @@ function registerTokens(server, bridge) {
|
|
|
41167
41198
|
}
|
|
41168
41199
|
|
|
41169
41200
|
// src/mcp.ts
|
|
41170
|
-
var SERVER_VERSION = "1.1.0";
|
|
41171
41201
|
function createMcpServer(bridge) {
|
|
41172
|
-
const server = new McpServer({ name: "
|
|
41202
|
+
const server = new McpServer({ name: "Mirra", version: SERVER_VERSION });
|
|
41173
41203
|
registerPing(server, bridge);
|
|
41174
41204
|
registerRender(server, bridge);
|
|
41175
41205
|
registerRead(server, bridge);
|
|
@@ -41203,6 +41233,15 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
41203
41233
|
// src/transport.ts
|
|
41204
41234
|
var HOSTS = ["127.0.0.1", "::1"];
|
|
41205
41235
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
41236
|
+
var TIMEOUT_MS = {
|
|
41237
|
+
ping: 5e3,
|
|
41238
|
+
pages: 1e4,
|
|
41239
|
+
render: 12e4,
|
|
41240
|
+
patch: 12e4,
|
|
41241
|
+
export: 12e4,
|
|
41242
|
+
define: 6e4,
|
|
41243
|
+
exec: 12e4
|
|
41244
|
+
};
|
|
41206
41245
|
function originAllowed(origin) {
|
|
41207
41246
|
if (!origin || origin === "null") return true;
|
|
41208
41247
|
if (process.env["MIRRA_ALLOW_ORIGIN"] === origin) return true;
|
|
@@ -41213,16 +41252,22 @@ function originAllowed(origin) {
|
|
|
41213
41252
|
return false;
|
|
41214
41253
|
}
|
|
41215
41254
|
}
|
|
41255
|
+
var UPDATE_HINT = "Run `npx @ttsalpha/mirra init` to install both halves from the same version, then run the plugin again in Figma.";
|
|
41216
41256
|
var OPEN_PLUGIN_HINT = "Open the file in Figma Desktop, then Plugins -> Development -> Mirra, and keep the panel open.";
|
|
41217
41257
|
var PluginBridge = class {
|
|
41218
41258
|
#servers = [];
|
|
41219
41259
|
#socket = null;
|
|
41220
41260
|
#hello = null;
|
|
41261
|
+
#protocolMismatch = null;
|
|
41221
41262
|
#port = null;
|
|
41222
41263
|
#pending = /* @__PURE__ */ new Map();
|
|
41223
41264
|
get status() {
|
|
41224
41265
|
return { port: this.#port, connected: this.#socket !== null, hello: this.#hello };
|
|
41225
41266
|
}
|
|
41267
|
+
/** Set while a connected plugin speaks a protocol this server does not. */
|
|
41268
|
+
get protocolMismatch() {
|
|
41269
|
+
return this.#protocolMismatch;
|
|
41270
|
+
}
|
|
41226
41271
|
async listen(preferredPort2) {
|
|
41227
41272
|
const first = preferredPort2 ?? DEFAULT_PORT;
|
|
41228
41273
|
const last = preferredPort2 ?? PORT_RANGE_END;
|
|
@@ -41265,11 +41310,14 @@ var PluginBridge = class {
|
|
|
41265
41310
|
this.#servers = [];
|
|
41266
41311
|
this.#port = null;
|
|
41267
41312
|
}
|
|
41268
|
-
async request(op, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
41313
|
+
async request(op, params, timeoutMs = TIMEOUT_MS[op] ?? DEFAULT_TIMEOUT_MS) {
|
|
41269
41314
|
const socket = this.#socket;
|
|
41270
41315
|
if (!socket) {
|
|
41271
41316
|
throw new MirraError("No Figma plugin is connected", "plugin_offline", OPEN_PLUGIN_HINT);
|
|
41272
41317
|
}
|
|
41318
|
+
if (this.#protocolMismatch && op !== "ping") {
|
|
41319
|
+
throw new MirraError(this.#protocolMismatch, "protocol_mismatch", UPDATE_HINT);
|
|
41320
|
+
}
|
|
41273
41321
|
const id = randomUUID();
|
|
41274
41322
|
const envelope = { id, op, params };
|
|
41275
41323
|
return new Promise((resolve4, reject) => {
|
|
@@ -41313,11 +41361,8 @@ var PluginBridge = class {
|
|
|
41313
41361
|
if ("type" in data) {
|
|
41314
41362
|
if (data.type === "log") return log(`[plugin:${data.level}] ${data.message}`);
|
|
41315
41363
|
this.#hello = data;
|
|
41316
|
-
|
|
41317
|
-
|
|
41318
|
-
`protocol mismatch: plugin speaks v${data.protocolVersion}, server speaks v${PROTOCOL_VERSION}. Rebuild and re-import the plugin.`
|
|
41319
|
-
);
|
|
41320
|
-
}
|
|
41364
|
+
this.#protocolMismatch = data.protocolVersion === PROTOCOL_VERSION ? null : `The plugin speaks protocol v${data.protocolVersion}, this server speaks v${PROTOCOL_VERSION}`;
|
|
41365
|
+
if (this.#protocolMismatch) log(this.#protocolMismatch);
|
|
41321
41366
|
return log(`plugin connected: ${data.fileName} (plugin v${data.pluginVersion})`);
|
|
41322
41367
|
}
|
|
41323
41368
|
const pending = this.#pending.get(data.id);
|
package/dist/plugin/main.js
CHANGED
|
@@ -949,30 +949,39 @@
|
|
|
949
949
|
return node;
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
// src/version.ts
|
|
953
|
+
var PLUGIN_VERSION = "1.2.0";
|
|
954
|
+
|
|
952
955
|
// src/ops/ping.ts
|
|
953
956
|
function ping() {
|
|
954
957
|
return {
|
|
955
958
|
fileName: figma.root.name,
|
|
956
959
|
fileKey: readFileKey(),
|
|
957
960
|
currentPage: figma.currentPage.name,
|
|
958
|
-
selectionCount: figma.currentPage.selection.length
|
|
961
|
+
selectionCount: figma.currentPage.selection.length,
|
|
962
|
+
pluginVersion: PLUGIN_VERSION
|
|
959
963
|
};
|
|
960
964
|
}
|
|
961
965
|
|
|
962
966
|
// src/figma/read.ts
|
|
963
|
-
function readNode(node, depth, names = EMPTY_NAMES) {
|
|
964
|
-
const
|
|
965
|
-
const tree = toSpec(node, depth,
|
|
966
|
-
return { tree, skipped:
|
|
967
|
-
}
|
|
968
|
-
function toSpec(node, depth,
|
|
967
|
+
function readNode(node, depth, names = EMPTY_NAMES, limit = Number.MAX_SAFE_INTEGER) {
|
|
968
|
+
const budget = { count: 0, left: limit };
|
|
969
|
+
const tree = toSpec(node, depth, budget, names);
|
|
970
|
+
return { tree, skipped: budget.count };
|
|
971
|
+
}
|
|
972
|
+
function toSpec(node, depth, budget, names) {
|
|
973
|
+
if (budget.left <= 0) {
|
|
974
|
+
budget.count += 1;
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
budget.left -= 1;
|
|
969
978
|
const svg = node.getPluginData(SVG_FIELD);
|
|
970
979
|
if (svg) return { type: "svg", ...common(node), src: svg };
|
|
971
980
|
const src = node.getPluginData(SRC_FIELD);
|
|
972
981
|
if (src) return { type: "image", ...common(node), ...painted(node, names), src };
|
|
973
982
|
switch (node.type) {
|
|
974
983
|
case "FRAME":
|
|
975
|
-
return frameSpec(node, depth,
|
|
984
|
+
return frameSpec(node, depth, budget, names);
|
|
976
985
|
case "TEXT":
|
|
977
986
|
return textSpec(node, names);
|
|
978
987
|
case "RECTANGLE":
|
|
@@ -982,19 +991,19 @@
|
|
|
982
991
|
case "INSTANCE":
|
|
983
992
|
return instanceSpec(node);
|
|
984
993
|
default:
|
|
985
|
-
|
|
994
|
+
budget.count += 1;
|
|
986
995
|
return null;
|
|
987
996
|
}
|
|
988
997
|
}
|
|
989
|
-
function frameSpec(node, depth,
|
|
998
|
+
function frameSpec(node, depth, budget, names) {
|
|
990
999
|
const children = [];
|
|
991
1000
|
if (depth > 0) {
|
|
992
1001
|
for (const child of node.children) {
|
|
993
|
-
const spec = toSpec(child, depth - 1,
|
|
1002
|
+
const spec = toSpec(child, depth - 1, budget, names);
|
|
994
1003
|
if (spec) children.push(spec);
|
|
995
1004
|
}
|
|
996
1005
|
} else if (node.children.length) {
|
|
997
|
-
|
|
1006
|
+
budget.count += node.children.length;
|
|
998
1007
|
}
|
|
999
1008
|
return {
|
|
1000
1009
|
type: "frame",
|
|
@@ -1151,6 +1160,7 @@
|
|
|
1151
1160
|
|
|
1152
1161
|
// src/ops/read.ts
|
|
1153
1162
|
var DEFAULT_DEPTH = 6;
|
|
1163
|
+
var DEFAULT_LIMIT2 = 200;
|
|
1154
1164
|
async function read(params) {
|
|
1155
1165
|
const targets = params.nodeId ? [await resolve(params.nodeId)] : [...figma.currentPage.selection];
|
|
1156
1166
|
if (!targets.length) {
|
|
@@ -1160,10 +1170,14 @@
|
|
|
1160
1170
|
const nodes = [];
|
|
1161
1171
|
let skipped = 0;
|
|
1162
1172
|
for (const target of targets) {
|
|
1163
|
-
const outcome = readNode(
|
|
1173
|
+
const outcome = readNode(
|
|
1174
|
+
target,
|
|
1175
|
+
params.depth ?? DEFAULT_DEPTH,
|
|
1176
|
+
names,
|
|
1177
|
+
params.limit ?? DEFAULT_LIMIT2
|
|
1178
|
+
);
|
|
1164
1179
|
skipped += outcome.skipped;
|
|
1165
1180
|
if (outcome.tree) nodes.push({ id: target.id, tree: outcome.tree });
|
|
1166
|
-
else skipped += 1;
|
|
1167
1181
|
}
|
|
1168
1182
|
return { nodes, skipped };
|
|
1169
1183
|
}
|
|
@@ -1211,21 +1225,29 @@
|
|
|
1211
1225
|
}
|
|
1212
1226
|
|
|
1213
1227
|
// src/ops/screenshot.ts
|
|
1228
|
+
var DEFAULT_MAX_EDGE = 1200;
|
|
1229
|
+
function fittingScale(wanted, box, maxEdge) {
|
|
1230
|
+
const longest = Math.max(box.width, box.height) * wanted;
|
|
1231
|
+
if (longest <= maxEdge) return wanted;
|
|
1232
|
+
return Math.max(0.01, wanted * maxEdge / longest);
|
|
1233
|
+
}
|
|
1214
1234
|
async function screenshot(params) {
|
|
1215
1235
|
const node = params.target ? await new NodeIndex().resolve(params.target) : figma.currentPage.selection[0];
|
|
1216
1236
|
if (!node) {
|
|
1217
1237
|
throw new Error("Nothing is selected: select a layer in Figma, or pass a target");
|
|
1218
1238
|
}
|
|
1239
|
+
const box = node;
|
|
1240
|
+
const scale = fittingScale(params.scale ?? 1, box, params.maxEdge ?? DEFAULT_MAX_EDGE);
|
|
1219
1241
|
const bytes = await node.exportAsync({
|
|
1220
1242
|
format: "PNG",
|
|
1221
|
-
constraint: { type: "SCALE", value:
|
|
1243
|
+
constraint: { type: "SCALE", value: scale }
|
|
1222
1244
|
});
|
|
1223
|
-
const box = node;
|
|
1224
1245
|
return {
|
|
1225
1246
|
id: node.id,
|
|
1226
1247
|
name: node.name,
|
|
1227
|
-
width: Math.round(box.width),
|
|
1228
|
-
height: Math.round(box.height),
|
|
1248
|
+
width: Math.round(box.width * scale),
|
|
1249
|
+
height: Math.round(box.height * scale),
|
|
1250
|
+
scale,
|
|
1229
1251
|
png: figma.base64Encode(bytes)
|
|
1230
1252
|
};
|
|
1231
1253
|
}
|
|
@@ -1269,7 +1291,6 @@
|
|
|
1269
1291
|
}
|
|
1270
1292
|
|
|
1271
1293
|
// src/main.ts
|
|
1272
|
-
var PLUGIN_VERSION = "1.1.0";
|
|
1273
1294
|
figma.showUI(__html__, { width: 300, height: 300, themeColors: true });
|
|
1274
1295
|
var queue = Promise.resolve();
|
|
1275
1296
|
figma.ui.onmessage = (message2) => {
|