Skip to content

Commit

Permalink
feat!: export Group, Layer, Slice as types only
Browse files Browse the repository at this point in the history
We only want these to exist as types that users can refer to.
We don't want users to directly instantiate these classes.

This change breaks the original `traverseNode()` example, which was
using `instanceof` to narrow a `Node` into `Psd`, `Layer`, `Group`.
From now on, users now have to check the `type` field to do so.
To make `traverseNode()` work, we replaced `Node` with a union type of
`Psd`, `Layer`, and `Group`. We also added `NodeParent` and `NodeChild`
types to better identify which types can be parents and which can be
children. The original `Node` interface has been renamed to `NodeBase`.
  • Loading branch information
pastelmind committed Jun 17, 2022
1 parent bcee878 commit 564b5a5
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 40 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,27 +128,28 @@ const psdFile = Psd.parse(myBuffer);
A `Psd` object contains a tree of `Layer` and `Group` (i.e. layer group) objects.

- The `Psd` object provides a `children` property, which is an array of top-level `Layer`s and `Group`s.
- Each `Group` object provides a `children` property, which is an array of `Layers` and `Group`s that belong immediately under the current layer group .
- Each `Group` object provides a `children` property, which is an array of `Layers` and `Group`s that belong immediately under the current layer group.
- `Psd`, `Group`, and `Layer` objects provide a `type` field, which can be used to discriminate each type:

```ts
import Psd, {Group, Layer, Node} from "@webtoon/psd";
import Psd, {Node} from "@webtoon/psd";

// Recursively traverse layers and layer groups
function traverseNode(node: Node) {
if (node instanceof Group) {
for (const child of node.children) {
traverseNode(child);
}
} else if (node instanceof Layer) {
// Do something with layer
if (node.type === "Layer") {
// Do something with Layer
} else if (node.type === "Group") {
// Do something with Group
} else if (node.type === "Psd") {
// Do something with Psd
} else {
throw new Error("Invalid node type");
}
}

for (const node of psdFile.children) {
traverseNode(node);
node.children?.forEach((child) => traverseNode(child));
}

traverseNode(psd);
```

The `Psd` object also provides the `layers` property, which is an array of all `Layer`s in the image (including nested).
Expand Down
16 changes: 10 additions & 6 deletions packages/psd/src/classes/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
// MIT License

import {GroupFrame} from "../sections";
import {Node} from "./Node";
import {NodeChild, NodeParent} from "./Node";
import {NodeBase} from "./NodeBase";

/**
* A layer group, which may contain layers and other layer groups.
* @alpha
*/
export class Group implements Node {
export class Group implements NodeBase<NodeParent, NodeChild> {
readonly type = "Group";
readonly children: Node[] = [];
readonly children: NodeChild[] = [];

/** @internal */
constructor(private layerFrame: GroupFrame, public readonly parent: Node) {}
constructor(
private layerFrame: GroupFrame,
public readonly parent: NodeParent
) {}

get name(): string {
return this.layerFrame.layerProperties.name;
Expand All @@ -26,15 +30,15 @@ export class Group implements Node {
return this.parent.composedOpacity * (this.opacity / 255);
}

addChild(node: Node): void {
addChild(node: NodeChild): void {
this.children.push(node);
}
hasChildren(): boolean {
return this.children.length !== 0;
}

freeze(): void {
this.children.forEach((node) => node.freeze && node.freeze());
this.children.forEach((node) => (node as NodeBase).freeze?.());
Object.freeze(this.children);
}
}
14 changes: 11 additions & 3 deletions packages/psd/src/classes/Layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@

import {ImageData} from "../interfaces";
import {LayerFrame} from "../sections";
import {Node} from "./Node";
import {NodeParent} from "./Node";
import {NodeBase} from "./NodeBase";
import {Synthesizable} from "./Synthesizable";

/**
* A layer in a PSD file.
* @alpha
*/
export class Layer extends Synthesizable implements Node {
export class Layer
extends Synthesizable
implements NodeBase<NodeParent, never>
{
readonly type = "Layer";
readonly children?: undefined;

/** @internal */
constructor(private layerFrame: LayerFrame, public readonly parent: Node) {
constructor(
private layerFrame: LayerFrame,
public readonly parent: NodeParent
) {
super();
}

Expand Down
41 changes: 31 additions & 10 deletions packages/psd/src/classes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,35 @@
// Copyright 2021-present NAVER WEBTOON
// MIT License

export interface Node {
type: "Group" | "Layer" | "Psd";
name: string;
parent?: Node;
children?: Node[];
opacity: number;
composedOpacity: number;
addChild?: (node: Node) => void;
hasChildren?: () => boolean;
freeze?: () => void;
import {PsdError} from "../utils";
import {Group} from "./Group";
import {Layer} from "./Layer";
import {Psd} from "./Psd";

export type Node = Psd | Group | Layer;
export type NodeParent = Psd | Group;
export type NodeChild = Group | Layer;

export function isNodeParent(node: Node): node is NodeParent {
return node.type === "Psd" || node.type === "Group";
}

export function isNodeChild(node: Node): node is NodeChild {
return node.type === "Group" || node.type === "Layer";
}

export function assertIsNodeParent(node: Node): asserts node is NodeParent {
if (!isNodeParent(node)) {
throw new PsdError(
`Node (name = '${node.name}', type: '${node.type}') cannot be a parent node`
);
}
}

export function assertIsNodeChild(node: Node): asserts node is NodeChild {
if (!isNodeChild(node)) {
throw new PsdError(
`Node (name = '${node.name}', type: '${node.type}') cannot be a child node`
);
}
}
20 changes: 20 additions & 0 deletions packages/psd/src/classes/NodeBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @webtoon/psd
// Copyright 2021-present NAVER WEBTOON
// MIT License

import {NodeChild, NodeParent} from "./Node";

export interface NodeBase<
Parent extends NodeParent = NodeParent,
Child extends NodeChild = NodeChild
> {
type: "Psd" | "Group" | "Layer";
name: string;
parent?: Parent;
children?: Child[];
opacity: number;
composedOpacity: number;
addChild?: (node: Child) => void;
hasChildren?: () => boolean;
freeze?: () => void;
}
22 changes: 13 additions & 9 deletions packages/psd/src/classes/Psd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,30 @@
import {
ColorMode,
Depth,
ParsingResult,
ImageData,
Guide,
ImageData,
ParsingResult,
ResourceType,
} from "../interfaces";
import {parse} from "../methods";
import {loadSlicesFromResourceBlock, Slice} from "./Slice";
import {Group} from "./Group";
import {Layer} from "./Layer";
import {Node} from "./Node";
import {assertIsNodeParent, Node, NodeChild} from "./Node";
import {NodeBase} from "./NodeBase";
import {loadSlicesFromResourceBlock, Slice} from "./Slice";
import {Synthesizable} from "./Synthesizable";

/**
* A parsed PSD file.
* @alpha
*/
export class Psd extends Synthesizable implements Node {
export class Psd extends Synthesizable implements NodeBase<never, NodeChild> {
public readonly name = "ROOT";
public readonly type = "Psd";
public readonly opacity = 255;
public readonly composedOpacity = 1;
public readonly children: Node[] = [];
public readonly parent?: undefined;
public readonly children: NodeChild[] = [];
public readonly layers: Layer[] = [];
public readonly guides: Guide[] = [];
public readonly slices: Slice[] = [];
Expand Down Expand Up @@ -94,20 +96,22 @@ export class Psd extends Synthesizable implements Node {
switch (e) {
case "G": {
const layerFrame = groups[groupIndex];
assertIsNodeParent(parent);
const group = new Group(layerFrame, parent);

stack.push(group);
parent.children?.push(group);
parent.children.push(group);
groupIndex += 1;

break;
}
case "L": {
const layerFrame = layers[layerIndex];
assertIsNodeParent(parent);
const layer = new Layer(layerFrame, parent);

this.layers.push(layer);
parent.children?.push(layer);
parent.children.push(layer);
layerIndex += 1;

break;
Expand All @@ -122,7 +126,7 @@ export class Psd extends Synthesizable implements Node {
stack.length = 0;

// Freeze children
this.children.forEach((node) => node.freeze && node.freeze());
this.children.forEach((node) => (node as NodeBase).freeze?.());
Object.freeze(this.children);
}
}
2 changes: 1 addition & 1 deletion packages/psd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import {Psd} from "./classes";

export {Group, Layer, Slice} from "./classes";
export type {Group, Layer, Node, NodeChild, NodeParent, Slice} from "./classes";
export {ColorMode, Depth, GuideDirection, SliceOrigin} from "./interfaces";
export type {Guide} from "./interfaces";
export {parse} from "./methods";
Expand Down

0 comments on commit 564b5a5

Please sign in to comment.