diff --git a/support-figma/extended-layout-plugin/manifest.json b/support-figma/extended-layout-plugin/manifest.json index 054774009..57c195c4d 100644 --- a/support-figma/extended-layout-plugin/manifest.json +++ b/support-figma/extended-layout-plugin/manifest.json @@ -18,7 +18,9 @@ { "name": "Generate String Resource", "command": "localization" }, { "name": "Clear String Resource", "command": "clear-localization" }, { "name": "Export Images", "command": "export-images" }, - { "name": "Clear Image Resource", "command": "clear-image-res" } + { "name": "Clear Image Resource", "command": "clear-image-res" }, + { "name": "Export Vectors", "command": "export-vectors" }, + { "name": "Clear Vector Resource", "command": "clear-vector-res" } ] } ], diff --git a/support-figma/extended-layout-plugin/src/code.ts b/support-figma/extended-layout-plugin/src/code.ts index a58d8f8b0..df789e78a 100644 --- a/support-figma/extended-layout-plugin/src/code.ts +++ b/support-figma/extended-layout-plugin/src/code.ts @@ -18,6 +18,7 @@ import * as Utils from "./utils"; import * as Localization from "./localization-module"; import * as DesignSpecs from "./design-spec-module"; import * as ImageRes from "./image-res-module"; +import * as VectorRes from "./vector-res-module"; // Warning component. interface ClippyWarningRun { @@ -564,6 +565,16 @@ if (figma.command === "sync") { }; } else if (figma.command === "clear-image-res") { ImageRes.clear(); +} else if (figma.command === "export-vectors") { + VectorRes.exportAllVectorsAsync(); + figma.ui.onmessage = (msg) => { + if (msg.msg === "show-node") { + Utils.showNode(msg.node); + } else if (msg.msg === "update-vector-res-name") { + } + } +} else if (figma.command === "clear-vector-res") { + } else if (figma.command === "move-plugin-data") { function movePluginDataWithKey(node: BaseNode, key: string) { // Read the private plugin data, write to shared diff --git a/support-figma/extended-layout-plugin/src/image-res-module.ts b/support-figma/extended-layout-plugin/src/image-res-module.ts index 3d470ef90..47682a4c3 100644 --- a/support-figma/extended-layout-plugin/src/image-res-module.ts +++ b/support-figma/extended-layout-plugin/src/image-res-module.ts @@ -242,3 +242,12 @@ function parseCachedImageHashToResMap(imageHashToResNameData?: string) { ); return cachedImageHashToResMap; } + +export function loadImageDrawableResNames(): Array { + let imageResNames = new Array(); + let cachedHashToResMap = loadExportedImages(); + cachedHashToResMap.forEach((resName, _) => imageResNames.push(resName)); + let cachedHashToResMapExcluded = loadNonExportedImages(); + cachedHashToResMapExcluded.forEach((resName, _) => imageResNames.push(resName)); + return imageResNames; +} diff --git a/support-figma/extended-layout-plugin/src/ui.html b/support-figma/extended-layout-plugin/src/ui.html index eec8025dc..ac81eba67 100644 --- a/support-figma/extended-layout-plugin/src/ui.html +++ b/support-figma/extended-layout-plugin/src/ui.html @@ -398,6 +398,18 @@ + +
+
Vector Frames:
+
  • Vector frames will be exported as png files, supporting ldpi, mdpi, hdpi, xhdpi and xxhdpi densities.
  • +
    +
    + +
    + +
    diff --git a/support-figma/extended-layout-plugin/src/vector-res-module.ts b/support-figma/extended-layout-plugin/src/vector-res-module.ts new file mode 100644 index 000000000..3ab03e714 --- /dev/null +++ b/support-figma/extended-layout-plugin/src/vector-res-module.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { loadImageDrawableResNames } from "./image-res-module"; +import * as Utils from "./utils"; + +const IMAGE_REPLACEMENT_RES_NAME = "image_replacement_res_name"; + +// Node id to vector root(frame node) +const vectorFrames = new Map(); +// Res name to VectorRes map. +const vectorResMap = new Map(); +// Image res names that vector exported images can not use +var existingImageDrawableResNames : Array; + +// ldpi: 0.75x, mdpi: 1x, hdpi: 1.5x, xhdpi: 2x, xxhdpi: 3x, xxxhdpi: 4x according to: +// https://developer.android.com/training/multiscreen/screendensities +// Ignores the 0.5x export option. +interface VectorRes { + resName: string; + nodeId: string; + ldpiBytes: Uint8Array; + mdpiBytes: Uint8Array; + hdpiBytes: Uint8Array; + xhdpiBytes: Uint8Array; + xxhdpiBytes: Uint8Array; +} + +export async function exportAllVectorsAsync(){ + await figma.loadAllPagesAsync(); + + vectorFrames.clear(); + vectorResMap.clear(); + + const vectorNodes = figma.root.findAll((node) => node.type === "VECTOR"); + + for (const vectorNode of vectorNodes) { + const frameAncestor = findFrameAncestor(vectorNode); + if (frameAncestor && isVectorRoot(frameAncestor)) { + vectorFrames.set(frameAncestor.id, frameAncestor); + } + } + + existingImageDrawableResNames = loadImageDrawableResNames(); + for (const [frameNodeId, frameNode] of vectorFrames) { + const cachedResName = getResName(frameNode); + var resName = cachedResName + ? cachedResName + : `ic_${Utils.toSnakeCase(frameNode.name)}`; + var index = 0; + while (vectorResMap.has(resName) || existingImageDrawableResNames.includes(resName)) { + index += 1; + resName = resName + "_" + index; + } + + let vectorRes = { + resName: resName, + nodeId: frameNodeId, + ldpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 0.75 }, + }), + mdpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 1 }, + }), + hdpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 1.5 }, + }), + xhdpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 2 }, + }), + xxhdpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 3 }, + }), + xxxhdpiBytes: await frameNode.exportAsync({ + format: "PNG", + colorProfile: "SRGB", + constraint: { type: "SCALE", value: 4 }, + }), + }; + vectorResMap.set(resName, vectorRes); + setResName(frameNode, resName); + } + + figma.showUI(__html__, { width: 600, height: 600 }); + figma.ui.postMessage({ + msg: "vector-export", + vectorResArray: Array.from(vectorResMap), + existingImageDrawableResNames: existingImageDrawableResNames, + }); +} + +function getResName(node: SceneNode): string { + return node.getSharedPluginData( + Utils.SHARED_PLUGIN_NAMESPACE, + IMAGE_REPLACEMENT_RES_NAME + ); +} + +function setResName(node: SceneNode, resName: string) { + node.setSharedPluginData( + Utils.SHARED_PLUGIN_NAMESPACE, + IMAGE_REPLACEMENT_RES_NAME, + resName + ); +} + +function findFrameAncestor(node: PageNode | SceneNode): FrameNode | undefined { + if (node.parent?.type === "FRAME") { + return node.parent!!; + } + + if (node.parent?.type === "GROUP") { + return findFrameAncestor(node.parent!!); + } + + return undefined; +} + +function isVectorRoot(frameNode: FrameNode): boolean { + for (const childNode of frameNode.children) { + if (!isVectorOrTextOrVectorGroup(childNode)) { + return false; + } + } + return true; +} + +// Returns true if the node is a vector node or a group node with vector as descendants. +function isVectorOrTextOrVectorGroup(node: SceneNode): boolean { + if (node.type === "VECTOR") { + return true; + } + if (node.type === "TEXT") { + return true; + } + if (node.type === "GROUP") { + return isVectorGroup(node); + } + return false; +} + +function isVectorGroup(groupNode: GroupNode): boolean { + for (const childNode of groupNode.children) { + if (childNode.type === "GROUP") { + if (!isVectorGroup(childNode)) { + return false; + } + } else if (childNode.type !== "VECTOR" && childNode.type != "TEXT") { + return false; + } + } + return true; +}