Browse Source

added cutom node limit

main
sina_sajjadi 3 weeks ago
parent
commit
4529f0423d
  1. 2
      README.md
  2. 8
      bridge-server/src/server.ts
  3. 86
      code.ts
  4. 63
      ui.html

2
README.md

@ -148,7 +148,7 @@ The bridge server exposes the following tools to your AI IDE:
## Limits ## Limits
- **500 node maximum** per extraction — the plugin counts all nodes (including deeply nested children) and will show an error if the selection exceeds this limit. This prevents performance issues with very large component trees.
- **Configurable node limit (500 default)** — the plugin counts all nodes (including deeply nested children) and validates selection against the configured limit before extraction. Users can adjust the **Max Nodes** setting directly in the plugin UI (persisted across sessions).
--- ---

8
bridge-server/src/server.ts

@ -55,8 +55,14 @@ function startWebSocketServer(): WebSocketServer {
ws.send(JSON.stringify({ type: "connected", message: "Bridge server ready" })); ws.send(JSON.stringify({ type: "connected", message: "Bridge server ready" }));
}); });
wss.on("error", (err) => {
wss.on("error", (err: any) => {
logToStderr(`[bridge] WebSocket server error: ${err.message}`); logToStderr(`[bridge] WebSocket server error: ${err.message}`);
if (err.code === "EADDRINUSE") {
logToStderr(`[bridge] Fatal error: Port ${WS_PORT} is already in use. Exiting process.`);
} else {
logToStderr(`[bridge] Fatal server error. Exiting process.`);
}
process.exit(1);
}); });
return wss; return wss;

86
code.ts

@ -1,8 +1,19 @@
figma.showUI(__html__, { width: 320, height: 180 });
figma.showUI(__html__, { width: 320, height: 215 });
const NODE_LIMIT = 500;
async function loadSavedSettings() {
try {
const savedLimit = await figma.clientStorage.getAsync('nodeLimit');
if (savedLimit !== undefined && savedLimit !== null) {
figma.ui.postMessage({ type: 'load-settings', nodeLimit: Number(savedLimit) });
}
} catch (e) {
console.error('Failed to load settings from storage', e);
}
}
async function runExport() {
loadSavedSettings();
async function runExport(nodeLimit: number = 500) {
const selection = figma.currentPage.selection; const selection = figma.currentPage.selection;
if (selection.length === 0) { if (selection.length === 0) {
@ -20,8 +31,8 @@ async function runExport() {
for (const node of selection) countNodes(node); for (const node of selection) countNodes(node);
if (nodeCount > NODE_LIMIT) {
figma.ui.postMessage({ type: 'error', message: `Selection exceeds limit. Selected: ${nodeCount}. Limit: ${NODE_LIMIT}.` });
if (nodeCount > nodeLimit) {
figma.ui.postMessage({ type: 'error', message: `Selection exceeds limit. Selected: ${nodeCount}. Limit: ${nodeLimit}.` });
return; return;
} }
@ -65,6 +76,47 @@ async function runExport() {
return val; return val;
} }
function toHex(c: { r: number; g: number; b: number; a?: number }): string {
const r = Math.round(c.r * 255).toString(16).padStart(2, '0');
const g = Math.round(c.g * 255).toString(16).padStart(2, '0');
const b = Math.round(c.b * 255).toString(16).padStart(2, '0');
if (c.a !== undefined && c.a < 1) {
const a = Math.round(c.a * 255).toString(16).padStart(2, '0');
return `#${r}${g}${b}${a}`;
}
return `#${r}${g}${b}`;
}
function convertColors(obj: any): any {
if (obj === null || obj === undefined) return obj;
// Detect a Figma color object: has r, g, b all numbers in [0,1]
if (
typeof obj === 'object' &&
!Array.isArray(obj) &&
typeof obj.r === 'number' &&
typeof obj.g === 'number' &&
typeof obj.b === 'number'
) {
const hex = toHex(obj);
// Keep alpha as separate field only if meaningful
if (typeof obj.a === 'number' && obj.a < 1) {
return { hex, a: Math.round(obj.a * 100) / 100 };
}
return hex;
}
if (Array.isArray(obj)) {
return obj.map(convertColors);
}
if (typeof obj === 'object') {
const out: any = {};
for (const key of Object.keys(obj)) {
out[key] = convertColors(obj[key]);
}
return out;
}
return obj;
}
function cleanRawProps(props: any): any { function cleanRawProps(props: any): any {
const cleaned: any = {}; const cleaned: any = {};
for (const key of Object.keys(props)) { for (const key of Object.keys(props)) {
@ -127,9 +179,9 @@ async function runExport() {
height: node.height, height: node.height,
x: localX, x: localX,
y: localY, y: localY,
fills: "fills" in node ? node.fills : null,
strokes: "strokes" in node ? node.strokes : null,
effects: "effects" in node ? node.effects : null,
fills: "fills" in node ? convertColors(node.fills) : null,
strokes: "strokes" in node ? convertColors(node.strokes) : null,
effects: "effects" in node ? convertColors(node.effects) : null,
opacity: "opacity" in node ? node.opacity : null, opacity: "opacity" in node ? node.opacity : null,
rotation: "rotation" in node ? node.rotation : null rotation: "rotation" in node ? node.rotation : null
}; };
@ -209,8 +261,22 @@ async function runExport() {
}); });
} }
figma.ui.onmessage = (msg) => {
figma.ui.onmessage = async (msg) => {
if (msg.type === 'run-extraction') { if (msg.type === 'run-extraction') {
runExport();
const limit = typeof msg.nodeLimit === 'number' && msg.nodeLimit > 0 ? msg.nodeLimit : 500;
try {
await figma.clientStorage.setAsync('nodeLimit', limit);
} catch (e) {
console.error('Failed to save settings to storage', e);
}
runExport(limit);
} else if (msg.type === 'save-settings') {
if (typeof msg.nodeLimit === 'number' && msg.nodeLimit > 0) {
try {
await figma.clientStorage.setAsync('nodeLimit', msg.nodeLimit);
} catch (e) {
console.error('Failed to save settings to storage', e);
}
}
} }
}; };

63
ui.html

@ -28,7 +28,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 180px;
height: 215px;
width: 320px; width: 320px;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
@ -256,6 +256,39 @@
background-color: #10b981; background-color: #10b981;
box-shadow: 0 0 4px rgba(16, 185, 129, 0.5); box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);
} }
.settings-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
font-size: 11px;
color: var(--text-muted);
}
.settings-row label {
font-weight: 500;
}
.settings-row input {
width: 70px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 11px;
font-weight: 600;
padding: 4px 6px;
text-align: center;
outline: none;
transition: all 0.2s ease;
}
.settings-row input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
</style> </style>
</head> </head>
<body class="idle"> <body class="idle">
@ -269,6 +302,10 @@
<div class="progress-container" id="progress-container"> <div class="progress-container" id="progress-container">
<div class="progress-bar"></div> <div class="progress-bar"></div>
</div> </div>
<div class="settings-row">
<label for="node-limit-input">Max Nodes:</label>
<input type="number" id="node-limit-input" value="500" min="1" max="50000" step="50" title="Max allowed node count" />
</div>
<button id="action-btn">Extract Selection</button> <button id="action-btn">Extract Selection</button>
</div> </div>
@ -318,13 +355,25 @@
} }
} }
function getNodeLimit() {
const input = document.getElementById('node-limit-input');
if (!input) return 500;
const val = parseInt(input.value, 10);
return (isNaN(val) || val <= 0) ? 500 : val;
}
const nodeLimitInput = document.getElementById('node-limit-input');
nodeLimitInput.addEventListener('change', () => {
parent.postMessage({ pluginMessage: { type: 'save-settings', nodeLimit: getNodeLimit() } }, '*');
});
// Handle extraction trigger / manual download // Handle extraction trigger / manual download
document.getElementById('action-btn').onclick = () => { document.getElementById('action-btn').onclick = () => {
if (document.body.classList.contains('complete') && bridgeConnected) { if (document.body.classList.contains('complete') && bridgeConnected) {
downloadZipManually(); downloadZipManually();
} else { } else {
updateState('processing', 'Processing selection...'); updateState('processing', 'Processing selection...');
parent.postMessage({ pluginMessage: { type: 'run-extraction' } }, '*');
parent.postMessage({ pluginMessage: { type: 'run-extraction', nodeLimit: getNodeLimit() } }, '*');
} }
}; };
@ -408,7 +457,7 @@
if (msg.type === 'trigger-extraction') { if (msg.type === 'trigger-extraction') {
// Remote trigger from MCP server // Remote trigger from MCP server
updateState('processing', 'Processing selection (remote)...'); updateState('processing', 'Processing selection (remote)...');
parent.postMessage({ pluginMessage: { type: 'run-extraction' } }, '*');
parent.postMessage({ pluginMessage: { type: 'run-extraction', nodeLimit: getNodeLimit() } }, '*');
} else if (msg.type === 'extraction-received') { } else if (msg.type === 'extraction-received') {
console.log('[bridge] Extraction acknowledged:', msg.id); console.log('[bridge] Extraction acknowledged:', msg.id);
} }
@ -495,7 +544,13 @@
onmessage = async (event) => { onmessage = async (event) => {
const msg = event.data.pluginMessage; const msg = event.data.pluginMessage;
if (msg.type === 'error') {
if (msg.type === 'load-settings') {
if (typeof msg.nodeLimit === 'number' && msg.nodeLimit > 0) {
const input = document.getElementById('node-limit-input');
if (input) input.value = msg.nodeLimit;
}
}
else if (msg.type === 'error') {
updateState('error', msg.message); updateState('error', msg.message);
} }
else if (msg.type === 'export') { else if (msg.type === 'export') {

Loading…
Cancel
Save