Skip to content

Commit 6da3b69

Browse files
committed
relates to #81: Project validation initial setup with first allCaps check (#98)
Co-authored-by: Daniel Elero <danixeee@gmail.com> Fix after rebase
1 parent cf60617 commit 6da3b69

32 files changed

+803
-68
lines changed

plainly-aescripts/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// <reference path="./node_modules/types-for-adobe/AfterEffects/22.0/index.d.ts" />
1+
/// <reference path="./node_modules/types-for-adobe/AfterEffects/24.3/index.d.ts" />
22

33
// Global XMP types
44
declare global {

plainly-aescripts/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { collectFiles, selectFolder } from './collect';
22
import {
3+
getAfterEffectsVersion,
34
getProjectData,
45
getProjectPath,
56
removeProjectData,
@@ -11,7 +12,10 @@ import './shims';
1112
import {
1213
getInstalledFontsByFamilyNameAndStyleName,
1314
getInstalledFontsByPostScriptName,
15+
selectLayer,
16+
unselectAllLayers,
1417
} from './utils';
18+
import { fixAllIssues, validateProject } from './validation';
1519

1620
const PlainlyAE = () => ({
1721
selectFolder,
@@ -21,9 +25,14 @@ const PlainlyAE = () => ({
2125
removeProjectData,
2226
getProjectPath,
2327
saveProject,
28+
getAfterEffectsVersion,
2429
relinkFootage,
2530
getInstalledFontsByPostScriptName,
2631
getInstalledFontsByFamilyNameAndStyleName,
32+
validateProject,
33+
fixAllIssues,
34+
unselectAllLayers,
35+
selectLayer,
2736
});
2837

2938
if ($['com.plainlyvideos.after-effects-plugin.Panel']) {

plainly-aescripts/src/project.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,15 @@ function saveProject() {
8989
app.project.save();
9090
}
9191

92+
function getAfterEffectsVersion() {
93+
return app.version;
94+
}
95+
9296
export {
9397
setProjectData,
9498
getProjectData,
9599
removeProjectData,
96100
getProjectPath,
97101
saveProject,
102+
getAfterEffectsVersion,
98103
};

plainly-aescripts/src/relink.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ function relinkFootage(relinkData: RelinkData): void {
4646
}
4747

4848
export { relinkFootage };
49+
export type { RelinkData };

plainly-aescripts/src/utils.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,52 @@ function getInstalledFontsByFamilyNameAndStyleName(
114114
return undefined;
115115
}
116116
}
117+
/**
118+
* Deselects every currently selected layer across all compositions in the active project.
119+
*
120+
* Iterates through all `CompItem` instances in `app.project`, aggregates their `selectedLayers`,
121+
* and sets each layer's `selected` property to `false`.
122+
*
123+
* @returns {void}
124+
*/
125+
function unselectAllLayers(): void {
126+
const comps = getAllComps(app.project);
127+
let selectedLayers: Layer[] = [];
128+
for (let i = 0; i < comps.length; i++) {
129+
const comp = comps[i];
130+
selectedLayers = selectedLayers.concat(comp.selectedLayers);
131+
}
132+
133+
for (let i = 0; i < selectedLayers.length; i++) {
134+
const layer = selectedLayers[i];
135+
layer.selected = false;
136+
}
137+
}
138+
139+
/**
140+
* Selects a layer in the active project by its numeric ID.
141+
*
142+
* Uses `app.project.layerByID` after parsing the provided string to base-10 integer. If a layer
143+
* with the given ID exists, its `selected` property is set to `true`; otherwise the function exits silently.
144+
*
145+
* @param {string} layerId - The string representation of the layer's numeric ID (e.g. value from `Layer.id`).
146+
* @returns {void}
147+
* @example
148+
* // Select a layer whose id is 1234
149+
* selectLayer('1234');
150+
*/
151+
function selectLayer(layerId: string): void {
152+
const layer = app.project.layerByID(parseInt(layerId, 10));
153+
if (layer) {
154+
// open the comp that contains the layer, so it is visible to the user in timeline
155+
const comp = layer.containingComp;
156+
comp.time = layer.inPoint;
157+
const viewer = comp.openInViewer();
158+
if (viewer?.active === false) viewer.setActive();
159+
160+
layer.selected = true;
161+
}
162+
}
117163

118164
export {
119165
getAllComps,
@@ -123,4 +169,6 @@ export {
123169
getTextLayersByComp,
124170
isWin,
125171
pathJoin,
172+
unselectAllLayers,
173+
selectLayer,
126174
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { checkTextLayers, fixAllCapsIssue } from './textValidators';
2+
import { type AnyProjectIssue, ProjectIssueType } from './types';
3+
4+
function validateProject(): string {
5+
const textIssues = checkTextLayers();
6+
let issues: AnyProjectIssue[] = [];
7+
8+
if (textIssues.length > 0) {
9+
issues = issues.concat(textIssues);
10+
}
11+
12+
if (issues.length > 0) {
13+
return JSON.stringify(issues);
14+
}
15+
16+
return 'undefined';
17+
}
18+
19+
function fixAllIssues(issues: AnyProjectIssue[]) {
20+
for (let i = 0; i < issues.length; i++) {
21+
const issue = issues[i];
22+
if (issue.type === ProjectIssueType.AllCaps) {
23+
fixAllCapsIssue(issue.layerId);
24+
}
25+
}
26+
27+
validateProject();
28+
}
29+
30+
export { validateProject, fixAllIssues };
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { getAllComps, getTextLayersByComp } from '../utils';
2+
import { ProjectIssueType, type TextLayerIssues } from './types';
3+
4+
function checkTextLayers(): TextLayerIssues[] {
5+
const comps = getAllComps(app.project);
6+
const textLayers: TextLayerIssues[] = [];
7+
8+
for (let i = 0; i < comps.length; i++) {
9+
const comp = comps[i];
10+
const layers = getTextLayersByComp(comp);
11+
if (layers.length === 0) {
12+
continue;
13+
}
14+
15+
for (let j = 0; j < layers.length; j++) {
16+
const layer = layers[j];
17+
if (layer.guideLayer) {
18+
continue;
19+
}
20+
21+
const textDocument = layer.sourceText.value;
22+
23+
// first check allCaps for older After Effects versions
24+
// this checks only the first character of the text layer
25+
// IMPORTANT: with this method, on older versions (< 24.3), we can't fix, but we can at least report it
26+
if (textDocument.allCaps) {
27+
textLayers.push({
28+
type: ProjectIssueType.AllCaps,
29+
layerId: layer.id.toString(),
30+
layerName: layer.name,
31+
text: true,
32+
});
33+
continue;
34+
}
35+
36+
// if we didn't find allCaps above, check for fontCapsOption (After Effects 24.3+)
37+
// check if characterRange exists in textDocument (After Effects 24.3+) first
38+
if (typeof textDocument.characterRange !== 'function') {
39+
continue;
40+
}
41+
42+
let hasCharacterAllCaps = false;
43+
const range = textDocument.characterRange(0, textDocument.text.length);
44+
45+
// `range.fontCapsOption === undefined` this means that there are different attributes in the text per character,
46+
// thus, we can check per-character basis to find if any character is allCaps
47+
if (range.fontCapsOption === undefined) {
48+
for (let k = 0; k < textDocument.text.length; k++) {
49+
const cRange = textDocument.characterRange(k, k + 1);
50+
if (
51+
cRange.isRangeValid &&
52+
cRange.fontCapsOption === FontCapsOption.FONT_ALL_CAPS
53+
) {
54+
hasCharacterAllCaps = true;
55+
textLayers.push({
56+
type: ProjectIssueType.AllCaps,
57+
layerId: layer.id.toString(),
58+
layerName: layer.name,
59+
text: true,
60+
});
61+
break;
62+
}
63+
}
64+
}
65+
66+
// if we already found a character with allCaps, skip further checks
67+
if (hasCharacterAllCaps) {
68+
continue;
69+
}
70+
71+
// if the whole text has the same attribute, we can check it directly
72+
if (
73+
range.isRangeValid &&
74+
range.fontCapsOption === FontCapsOption.FONT_ALL_CAPS
75+
) {
76+
textLayers.push({
77+
type: ProjectIssueType.AllCaps,
78+
layerId: layer.id.toString(),
79+
layerName: layer.name,
80+
text: true,
81+
});
82+
}
83+
}
84+
}
85+
86+
return textLayers;
87+
}
88+
89+
/**
90+
* Fixes all caps issue for a specific text layer.
91+
*
92+
* **NOTE**: Works only on `After Effects 24.3` and later due to the use of `characterRange` method and `fontCapsOption`.
93+
*
94+
* @param layerId string
95+
* @returns void
96+
*/
97+
function fixAllCapsIssue(layerId: string) {
98+
const layer = app.project.layerByID(parseInt(layerId, 10));
99+
if (!(layer && layer instanceof TextLayer)) {
100+
return;
101+
}
102+
103+
const textDocument = layer.sourceText.value;
104+
105+
// check if characterRange exists in textDocument (After Effects 24.3+)
106+
if (typeof textDocument.characterRange !== 'function') {
107+
return;
108+
}
109+
110+
const newValue = textDocument;
111+
112+
// first check if the whole text has the same attribute
113+
const range = newValue.characterRange(0, newValue.text.length);
114+
if (
115+
range.isRangeValid &&
116+
range.fontCapsOption === FontCapsOption.FONT_ALL_CAPS
117+
) {
118+
newValue.fontCapsOption = FontCapsOption.FONT_NORMAL_CAPS;
119+
newValue.text = newValue.text.toUpperCase();
120+
updateLayerTextDocument(layer, newValue);
121+
return;
122+
}
123+
124+
// otherwise, check per-character basis
125+
for (let i = 0; i < newValue.text.length; i++) {
126+
const cRange = newValue.characterRange(i, i + 1);
127+
if (
128+
cRange.isRangeValid &&
129+
cRange.fontCapsOption === FontCapsOption.FONT_ALL_CAPS
130+
) {
131+
newValue.characterRange(i, i + 1).fontCapsOption =
132+
FontCapsOption.FONT_NORMAL_CAPS;
133+
newValue.characterRange(i, i + 1).text = newValue
134+
.characterRange(i, i + 1)
135+
.text.toUpperCase();
136+
}
137+
}
138+
updateLayerTextDocument(layer, newValue);
139+
}
140+
141+
function updateLayerTextDocument(layer: TextLayer, newValue: TextDocument) {
142+
const originalLayerName = layer.name;
143+
layer.sourceText.setValue(newValue);
144+
layer.name = originalLayerName; // Preserve original layer name
145+
}
146+
147+
export { checkTextLayers, fixAllCapsIssue };
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
enum ProjectIssueType {
2+
AllCaps = 'AllCaps',
3+
}
4+
5+
interface ProjectIssue<T extends ProjectIssueType> {
6+
type: T;
7+
}
8+
9+
interface ProjectLayerIssue<T extends ProjectIssueType>
10+
extends ProjectIssue<T> {
11+
layerId: string;
12+
layerName: string;
13+
}
14+
15+
interface TextAllCapsEnabledIssue
16+
extends ProjectLayerIssue<ProjectIssueType.AllCaps> {}
17+
18+
type TextLayerIssues = TextAllCapsEnabledIssue & {
19+
text: true;
20+
};
21+
22+
type AnyProjectIssue = TextLayerIssues;
23+
24+
export type {
25+
ProjectIssue,
26+
ProjectLayerIssue,
27+
TextAllCapsEnabledIssue,
28+
TextLayerIssues,
29+
AnyProjectIssue,
30+
};
31+
32+
export { ProjectIssueType };

plainly-aescripts/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"experimentalDecorators": true,
99
"allowJs": true,
1010
"strictNullChecks": true,
11-
"types": ["types-for-adobe/AfterEffects/24.0", "@types/node"]
11+
"types": ["types-for-adobe/AfterEffects/24.3", "@types/node"]
1212
},
1313
"include": ["src/**/*.ts", "index.d.ts"]
1414
}

plainly-plugin/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"classnames": "^2.5.1",
2222
"date-fns": "^4.1.0",
2323
"form-data": "^4.0.4",
24-
"lucide-react": "^0.456.0",
24+
"lodash-es": "^4.17.22",
25+
"lucide-react": "^0.544.0",
2526
"react": "^18.3.1",
2627
"react-dom": "^18.3.1",
2728
"react-hooks-global-state": "^2.1.0"
@@ -37,6 +38,7 @@
3738
"@types/archiver": "^6.0.3",
3839
"@types/dotenv-webpack": "^7.0.8",
3940
"@types/jest": "^30.0.0",
41+
"@types/lodash-es": "^4.17.12",
4042
"@types/node": "^20.17.6",
4143
"@types/react": "^18.3.12",
4244
"@types/react-dom": "^18.3.1",

0 commit comments

Comments
 (0)