├── .gitignore ├── LICENSE.md ├── README.md ├── code.ts ├── figma.d.ts ├── manifest.json ├── tsconfig.json └── ui.html /.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Evan Wallace 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fill Rule Editor 2 | 3 | This is a Figma plugin that lets you edit the fill rules of vector objects. Click here to install it: https://www.figma.com/c/plugin/771155994770327940. 4 | -------------------------------------------------------------------------------- /code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { 2 | width: 400, 3 | height: 400, 4 | }); 5 | 6 | let updateTimeout = 0; 7 | let updateCounter = 0; 8 | let prevMsg = ''; 9 | 10 | function update(): void { 11 | clearTimeout(updateTimeout); 12 | 13 | const selection = figma.currentPage.selection; 14 | const node = selection.length === 1 ? selection[0] : null; 15 | const msg = { 16 | node: node === null || node.type !== 'VECTOR' ? null : { 17 | id: node.id, 18 | vectorNetwork: node.vectorNetwork, 19 | }, 20 | }; 21 | 22 | const msgStr = JSON.stringify(msg); 23 | if (msgStr !== prevMsg) { 24 | prevMsg = msgStr; 25 | figma.ui.postMessage(msg); 26 | updateCounter = 0; 27 | } 28 | 29 | const timeout = updateCounter++ < 20 ? 16 : 250; 30 | updateTimeout = setTimeout(update, timeout); 31 | } 32 | 33 | figma.on('selectionchange', update); 34 | update(); 35 | 36 | figma.ui.onmessage = msg => { 37 | if (msg.node) { 38 | const node = figma.getNodeById(msg.node.id); 39 | if (node !== null && node.type === 'VECTOR') { 40 | node.vectorNetwork = msg.node.vectorNetwork; 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 5 2 | 3 | // Global variable with Figma's plugin API. 4 | declare const figma: PluginAPI 5 | declare const __html__: string 6 | 7 | interface PluginAPI { 8 | readonly apiVersion: "1.0.0" 9 | readonly command: string 10 | readonly viewport: ViewportAPI 11 | closePlugin(message?: string): void 12 | 13 | notify(message: string, options?: NotificationOptions): NotificationHandler 14 | 15 | showUI(html: string, options?: ShowUIOptions): void 16 | readonly ui: UIAPI 17 | 18 | readonly clientStorage: ClientStorageAPI 19 | 20 | getNodeById(id: string): BaseNode | null 21 | getStyleById(id: string): BaseStyle | null 22 | 23 | readonly root: DocumentNode 24 | currentPage: PageNode 25 | 26 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void; 27 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void; 28 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void; 29 | 30 | readonly mixed: symbol 31 | 32 | createRectangle(): RectangleNode 33 | createLine(): LineNode 34 | createEllipse(): EllipseNode 35 | createPolygon(): PolygonNode 36 | createStar(): StarNode 37 | createVector(): VectorNode 38 | createText(): TextNode 39 | createFrame(): FrameNode 40 | createComponent(): ComponentNode 41 | createPage(): PageNode 42 | createSlice(): SliceNode 43 | /** 44 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 45 | */ 46 | createBooleanOperation(): BooleanOperationNode 47 | 48 | createPaintStyle(): PaintStyle 49 | createTextStyle(): TextStyle 50 | createEffectStyle(): EffectStyle 51 | createGridStyle(): GridStyle 52 | 53 | // The styles are returned in the same order as displayed in the UI. Only 54 | // local styles are returned. Never styles from team library. 55 | getLocalPaintStyles(): PaintStyle[] 56 | getLocalTextStyles(): TextStyle[] 57 | getLocalEffectStyles(): EffectStyle[] 58 | getLocalGridStyles(): GridStyle[] 59 | 60 | importComponentByKeyAsync(key: string): Promise 61 | importStyleByKeyAsync(key: string): Promise 62 | 63 | listAvailableFontsAsync(): Promise 64 | loadFontAsync(fontName: FontName): Promise 65 | readonly hasMissingFont: boolean 66 | 67 | createNodeFromSvg(svg: string): FrameNode 68 | 69 | createImage(data: Uint8Array): Image 70 | getImageByHash(hash: string): Image 71 | 72 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 73 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 74 | 75 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 76 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | } 80 | 81 | interface ClientStorageAPI { 82 | getAsync(key: string): Promise 83 | setAsync(key: string, value: any): Promise 84 | } 85 | 86 | interface NotificationOptions { 87 | timeout?: number, 88 | } 89 | 90 | interface NotificationHandler { 91 | cancel: () => void, 92 | } 93 | 94 | interface ShowUIOptions { 95 | visible?: boolean, 96 | width?: number, 97 | height?: number, 98 | position?: 'default' | 'last' | 'auto' // PROPOSED API ONLY 99 | } 100 | 101 | interface UIPostMessageOptions { 102 | origin?: string, 103 | } 104 | 105 | interface OnMessageProperties { 106 | origin: string, 107 | } 108 | 109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void 110 | 111 | interface UIAPI { 112 | show(): void 113 | hide(): void 114 | resize(width: number, height: number): void 115 | close(): void 116 | 117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 118 | onmessage: MessageEventHandler | undefined 119 | on(type: "message", callback: MessageEventHandler): void; 120 | once(type: "message", callback: MessageEventHandler): void; 121 | off(type: "message", callback: MessageEventHandler): void; 122 | } 123 | 124 | interface ViewportAPI { 125 | center: { x: number, y: number } 126 | zoom: number 127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void; 128 | } 129 | 130 | //////////////////////////////////////////////////////////////////////////////// 131 | // Datatypes 132 | 133 | type Transform = [ 134 | [number, number, number], 135 | [number, number, number] 136 | ] 137 | 138 | interface Vector { 139 | readonly x: number 140 | readonly y: number 141 | } 142 | 143 | interface RGB { 144 | readonly r: number 145 | readonly g: number 146 | readonly b: number 147 | } 148 | 149 | interface RGBA { 150 | readonly r: number 151 | readonly g: number 152 | readonly b: number 153 | readonly a: number 154 | } 155 | 156 | interface FontName { 157 | readonly family: string 158 | readonly style: string 159 | } 160 | 161 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 162 | 163 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 164 | 165 | interface ArcData { 166 | readonly startingAngle: number 167 | readonly endingAngle: number 168 | readonly innerRadius: number 169 | } 170 | 171 | interface ShadowEffect { 172 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 173 | readonly color: RGBA 174 | readonly offset: Vector 175 | readonly radius: number 176 | readonly visible: boolean 177 | readonly blendMode: BlendMode 178 | } 179 | 180 | interface BlurEffect { 181 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 182 | readonly radius: number 183 | readonly visible: boolean 184 | } 185 | 186 | type Effect = ShadowEffect | BlurEffect 187 | 188 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 189 | 190 | interface Constraints { 191 | readonly horizontal: ConstraintType 192 | readonly vertical: ConstraintType 193 | } 194 | 195 | interface ColorStop { 196 | readonly position: number 197 | readonly color: RGBA 198 | } 199 | 200 | interface ImageFilters { 201 | readonly exposure?: number 202 | readonly contrast?: number 203 | readonly saturation?: number 204 | readonly temperature?: number 205 | readonly tint?: number 206 | readonly highlights?: number 207 | readonly shadows?: number 208 | } 209 | 210 | interface SolidPaint { 211 | readonly type: "SOLID" 212 | readonly color: RGB 213 | 214 | readonly visible?: boolean 215 | readonly opacity?: number 216 | readonly blendMode?: BlendMode 217 | } 218 | 219 | interface GradientPaint { 220 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 221 | readonly gradientTransform: Transform 222 | readonly gradientStops: ReadonlyArray 223 | 224 | readonly visible?: boolean 225 | readonly opacity?: number 226 | readonly blendMode?: BlendMode 227 | } 228 | 229 | interface ImagePaint { 230 | readonly type: "IMAGE" 231 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 232 | readonly imageHash: string | null 233 | readonly imageTransform?: Transform // setting for "CROP" 234 | readonly scalingFactor?: number // setting for "TILE" 235 | readonly filters?: ImageFilters 236 | 237 | readonly visible?: boolean 238 | readonly opacity?: number 239 | readonly blendMode?: BlendMode 240 | } 241 | 242 | type Paint = SolidPaint | GradientPaint | ImagePaint 243 | 244 | interface Guide { 245 | readonly axis: "X" | "Y" 246 | readonly offset: number 247 | } 248 | 249 | interface RowsColsLayoutGrid { 250 | readonly pattern: "ROWS" | "COLUMNS" 251 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 252 | readonly gutterSize: number 253 | 254 | readonly count: number // Infinity when "Auto" is set in the UI 255 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 256 | readonly offset?: number // Not set for alignment: "CENTER" 257 | 258 | readonly visible?: boolean 259 | readonly color?: RGBA 260 | } 261 | 262 | interface GridLayoutGrid { 263 | readonly pattern: "GRID" 264 | readonly sectionSize: number 265 | 266 | readonly visible?: boolean 267 | readonly color?: RGBA 268 | } 269 | 270 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 271 | 272 | interface ExportSettingsConstraints { 273 | type: "SCALE" | "WIDTH" | "HEIGHT" 274 | value: number 275 | } 276 | 277 | interface ExportSettingsImage { 278 | format: "JPG" | "PNG" 279 | contentsOnly?: boolean // defaults to true 280 | suffix?: string 281 | constraint?: ExportSettingsConstraints 282 | } 283 | 284 | interface ExportSettingsSVG { 285 | format: "SVG" 286 | contentsOnly?: boolean // defaults to true 287 | suffix?: string 288 | svgOutlineText?: boolean // defaults to true 289 | svgIdAttribute?: boolean // defaults to false 290 | svgSimplifyStroke?: boolean // defaults to true 291 | } 292 | 293 | interface ExportSettingsPDF { 294 | format: "PDF" 295 | contentsOnly?: boolean // defaults to true 296 | suffix?: string 297 | } 298 | 299 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 300 | 301 | type WindingRule = "NONZERO" | "EVENODD" 302 | 303 | interface VectorVertex { 304 | readonly x: number 305 | readonly y: number 306 | readonly strokeCap?: StrokeCap 307 | readonly strokeJoin?: StrokeJoin 308 | readonly cornerRadius?: number 309 | readonly handleMirroring?: HandleMirroring 310 | } 311 | 312 | interface VectorSegment { 313 | readonly start: number 314 | readonly end: number 315 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 316 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 317 | } 318 | 319 | interface VectorRegion { 320 | readonly windingRule: WindingRule 321 | readonly loops: ReadonlyArray> 322 | } 323 | 324 | interface VectorNetwork { 325 | readonly vertices: ReadonlyArray 326 | readonly segments: ReadonlyArray 327 | readonly regions?: ReadonlyArray // Defaults to [] 328 | } 329 | 330 | interface VectorPath { 331 | readonly windingRule: WindingRule | "NONE" 332 | readonly data: string 333 | } 334 | 335 | type VectorPaths = ReadonlyArray 336 | 337 | interface LetterSpacing { 338 | readonly value: number 339 | readonly unit: "PIXELS" | "PERCENT" 340 | } 341 | 342 | type LineHeight = { 343 | readonly value: number 344 | readonly unit: "PIXELS" | "PERCENT" 345 | } | { 346 | readonly unit: "AUTO" 347 | } 348 | 349 | type BlendMode = 350 | "PASS_THROUGH" | 351 | "NORMAL" | 352 | "DARKEN" | 353 | "MULTIPLY" | 354 | "LINEAR_BURN" | 355 | "COLOR_BURN" | 356 | "LIGHTEN" | 357 | "SCREEN" | 358 | "LINEAR_DODGE" | 359 | "COLOR_DODGE" | 360 | "OVERLAY" | 361 | "SOFT_LIGHT" | 362 | "HARD_LIGHT" | 363 | "DIFFERENCE" | 364 | "EXCLUSION" | 365 | "HUE" | 366 | "SATURATION" | 367 | "COLOR" | 368 | "LUMINOSITY" 369 | 370 | interface Font { 371 | fontName: FontName 372 | } 373 | 374 | //////////////////////////////////////////////////////////////////////////////// 375 | // Mixins 376 | 377 | interface BaseNodeMixin { 378 | readonly id: string 379 | readonly parent: (BaseNode & ChildrenMixin) | null 380 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 381 | readonly removed: boolean 382 | toString(): string 383 | remove(): void 384 | 385 | getPluginData(key: string): string 386 | setPluginData(key: string, value: string): void 387 | 388 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 389 | // be a name related to your plugin. Other plugins will be able to read this data. 390 | getSharedPluginData(namespace: string, key: string): string 391 | setSharedPluginData(namespace: string, key: string, value: string): void 392 | } 393 | 394 | interface SceneNodeMixin { 395 | visible: boolean 396 | locked: boolean 397 | } 398 | 399 | interface ChildrenMixin { 400 | readonly children: ReadonlyArray 401 | 402 | appendChild(child: SceneNode): void 403 | insertChild(index: number, child: SceneNode): void 404 | 405 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 406 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 407 | } 408 | 409 | interface ConstraintMixin { 410 | constraints: Constraints 411 | } 412 | 413 | interface LayoutMixin { 414 | readonly absoluteTransform: Transform 415 | relativeTransform: Transform 416 | x: number 417 | y: number 418 | rotation: number // In degrees 419 | 420 | readonly width: number 421 | readonly height: number 422 | 423 | resize(width: number, height: number): void 424 | resizeWithoutConstraints(width: number, height: number): void 425 | } 426 | 427 | interface BlendMixin { 428 | opacity: number 429 | blendMode: BlendMode 430 | isMask: boolean 431 | effects: ReadonlyArray 432 | effectStyleId: string 433 | } 434 | 435 | interface FrameMixin { 436 | backgrounds: ReadonlyArray 437 | layoutGrids: ReadonlyArray 438 | clipsContent: boolean 439 | guides: ReadonlyArray 440 | gridStyleId: string 441 | backgroundStyleId: string 442 | } 443 | 444 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 445 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 446 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 447 | 448 | interface GeometryMixin { 449 | fills: ReadonlyArray | symbol 450 | strokes: ReadonlyArray 451 | strokeWeight: number 452 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 453 | strokeCap: StrokeCap | symbol 454 | strokeJoin: StrokeJoin | symbol 455 | dashPattern: ReadonlyArray 456 | fillStyleId: string | symbol 457 | strokeStyleId: string 458 | } 459 | 460 | interface CornerMixin { 461 | cornerRadius: number | symbol 462 | cornerSmoothing: number 463 | } 464 | 465 | interface ExportMixin { 466 | exportSettings: ReadonlyArray 467 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 468 | } 469 | 470 | interface DefaultShapeMixin extends 471 | BaseNodeMixin, SceneNodeMixin, 472 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 473 | } 474 | 475 | interface DefaultContainerMixin extends 476 | BaseNodeMixin, SceneNodeMixin, 477 | ChildrenMixin, FrameMixin, 478 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 479 | } 480 | 481 | //////////////////////////////////////////////////////////////////////////////// 482 | // Nodes 483 | 484 | interface DocumentNode extends BaseNodeMixin { 485 | readonly type: "DOCUMENT" 486 | 487 | readonly children: ReadonlyArray 488 | 489 | appendChild(child: PageNode): void 490 | insertChild(index: number, child: PageNode): void 491 | 492 | findAll(callback?: (node: (PageNode | SceneNode)) => boolean): Array 493 | findOne(callback: (node: (PageNode | SceneNode)) => boolean): PageNode | SceneNode | null 494 | } 495 | 496 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 497 | readonly type: "PAGE" 498 | clone(): PageNode 499 | 500 | guides: ReadonlyArray 501 | selection: ReadonlyArray 502 | 503 | backgrounds: ReadonlyArray 504 | } 505 | 506 | interface FrameNode extends DefaultContainerMixin { 507 | readonly type: "FRAME" | "GROUP" 508 | clone(): FrameNode 509 | } 510 | 511 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 512 | readonly type: "SLICE" 513 | clone(): SliceNode 514 | } 515 | 516 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 517 | readonly type: "RECTANGLE" 518 | clone(): RectangleNode 519 | topLeftRadius: number 520 | topRightRadius: number 521 | bottomLeftRadius: number 522 | bottomRightRadius: number 523 | } 524 | 525 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 526 | readonly type: "LINE" 527 | clone(): LineNode 528 | } 529 | 530 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 531 | readonly type: "ELLIPSE" 532 | clone(): EllipseNode 533 | arcData: ArcData 534 | } 535 | 536 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 537 | readonly type: "POLYGON" 538 | clone(): PolygonNode 539 | pointCount: number 540 | } 541 | 542 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 543 | readonly type: "STAR" 544 | clone(): StarNode 545 | pointCount: number 546 | innerRadius: number 547 | } 548 | 549 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 550 | readonly type: "VECTOR" 551 | clone(): VectorNode 552 | vectorNetwork: VectorNetwork 553 | vectorPaths: VectorPaths 554 | handleMirroring: HandleMirroring | symbol 555 | } 556 | 557 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 558 | readonly type: "TEXT" 559 | clone(): TextNode 560 | characters: string 561 | readonly hasMissingFont: boolean 562 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 563 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 564 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 565 | paragraphIndent: number 566 | paragraphSpacing: number 567 | autoRename: boolean 568 | 569 | textStyleId: string | symbol 570 | fontSize: number | symbol 571 | fontName: FontName | symbol 572 | textCase: TextCase | symbol 573 | textDecoration: TextDecoration | symbol 574 | letterSpacing: LetterSpacing | symbol 575 | lineHeight: LineHeight | symbol 576 | 577 | getRangeFontSize(start: number, end: number): number | symbol 578 | setRangeFontSize(start: number, end: number, value: number): void 579 | getRangeFontName(start: number, end: number): FontName | symbol 580 | setRangeFontName(start: number, end: number, value: FontName): void 581 | getRangeTextCase(start: number, end: number): TextCase | symbol 582 | setRangeTextCase(start: number, end: number, value: TextCase): void 583 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 584 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 585 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 586 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 587 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 588 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 589 | getRangeFills(start: number, end: number): Paint[] | symbol 590 | setRangeFills(start: number, end: number, value: Paint[]): void 591 | getRangeTextStyleId(start: number, end: number): string | symbol 592 | setRangeTextStyleId(start: number, end: number, value: string): void 593 | getRangeFillStyleId(start: number, end: number): string | symbol 594 | setRangeFillStyleId(start: number, end: number, value: string): void 595 | } 596 | 597 | interface ComponentNode extends DefaultContainerMixin { 598 | readonly type: "COMPONENT" 599 | clone(): ComponentNode 600 | 601 | createInstance(): InstanceNode 602 | description: string 603 | readonly remote: boolean 604 | readonly key: string // The key to use with "importComponentByKeyAsync" 605 | } 606 | 607 | interface InstanceNode extends DefaultContainerMixin { 608 | readonly type: "INSTANCE" 609 | clone(): InstanceNode 610 | masterComponent: ComponentNode 611 | } 612 | 613 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 614 | readonly type: "BOOLEAN_OPERATION" 615 | clone(): BooleanOperationNode 616 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 617 | } 618 | 619 | type BaseNode = 620 | DocumentNode | 621 | PageNode | 622 | SceneNode 623 | 624 | type SceneNode = 625 | SliceNode | 626 | FrameNode | 627 | ComponentNode | 628 | InstanceNode | 629 | BooleanOperationNode | 630 | VectorNode | 631 | StarNode | 632 | LineNode | 633 | EllipseNode | 634 | PolygonNode | 635 | RectangleNode | 636 | TextNode 637 | 638 | type NodeType = 639 | "DOCUMENT" | 640 | "PAGE" | 641 | "SLICE" | 642 | "FRAME" | 643 | "GROUP" | 644 | "COMPONENT" | 645 | "INSTANCE" | 646 | "BOOLEAN_OPERATION" | 647 | "VECTOR" | 648 | "STAR" | 649 | "LINE" | 650 | "ELLIPSE" | 651 | "POLYGON" | 652 | "RECTANGLE" | 653 | "TEXT" 654 | 655 | //////////////////////////////////////////////////////////////////////////////// 656 | // Styles 657 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 658 | 659 | interface BaseStyle { 660 | readonly id: string 661 | readonly type: StyleType 662 | name: string 663 | description: string 664 | remote: boolean 665 | readonly key: string // The key to use with "importStyleByKeyAsync" 666 | remove(): void 667 | } 668 | 669 | interface PaintStyle extends BaseStyle { 670 | type: "PAINT" 671 | paints: ReadonlyArray 672 | } 673 | 674 | interface TextStyle extends BaseStyle { 675 | type: "TEXT" 676 | fontSize: number 677 | textDecoration: TextDecoration 678 | fontName: FontName 679 | letterSpacing: LetterSpacing 680 | lineHeight: LineHeight 681 | paragraphIndent: number 682 | paragraphSpacing: number 683 | textCase: TextCase 684 | } 685 | 686 | interface EffectStyle extends BaseStyle { 687 | type: "EFFECT" 688 | effects: ReadonlyArray 689 | } 690 | 691 | interface GridStyle extends BaseStyle { 692 | type: "GRID" 693 | layoutGrids: ReadonlyArray 694 | } 695 | 696 | //////////////////////////////////////////////////////////////////////////////// 697 | // Other 698 | 699 | interface Image { 700 | readonly hash: string 701 | getBytesAsync(): Promise 702 | } 703 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fill Rule Editor", 3 | "id": "771155994770327940", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html" 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es6" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ui.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | This plugin lets you edit the fill rules of a vector object. Fill rules 26 | determine which parts of a vector outline are filled. The fill 27 | rules have different behavior when a vector outline overlaps: 28 |  Select a vector object. Click on a fill to toggle between 29 | non-zero and even-odd. Click on a loop to reverse the 30 | orientation, which is sometimes necessary to get holes to look 31 | correct. 32 |  Certain export formats (e.g. TrueType 33 | fonts, Android VectorDrawable) only support the non-zero fill 34 | rule. You can use this plugin to manually convert even-odd to 35 | non-zero to make the exporters for these formats work. 36 | 37 | To use: 38 | Why is this useful? 39 | 40 | 41 | 42 | Non-zero rule 43 | Even-odd rule 44 | 45 | 554 | --------------------------------------------------------------------------------