@engine-room/after-effects-mcp 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -185
- package/bin/server.js +12567 -2478
- package/package.json +8 -8
- package/panel/CSXS/manifest.xml +44 -4
- package/panel/META-INF/signatures.xml +182 -0
- package/panel/client/csinterface.js +32 -0
- package/panel/client/main.js +522 -632
- package/panel/client/panelhash.js +79 -0
- package/panel/client/status.html +27 -0
- package/panel/client/status.js +112 -0
- package/panel/jsx/bundle.jsx +9032 -2393
- package/panel/mimetype +1 -0
- package/panel/node_modules/ws/LICENSE +20 -0
- package/panel/node_modules/ws/README.md +548 -0
- package/panel/node_modules/ws/browser.js +8 -0
- package/panel/node_modules/ws/index.js +22 -0
- package/panel/node_modules/ws/lib/buffer-util.js +131 -0
- package/panel/node_modules/ws/lib/constants.js +19 -0
- package/panel/node_modules/ws/lib/event-target.js +292 -0
- package/panel/node_modules/ws/lib/extension.js +203 -0
- package/panel/node_modules/ws/lib/limiter.js +55 -0
- package/panel/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/panel/node_modules/ws/lib/receiver.js +743 -0
- package/panel/node_modules/ws/lib/sender.js +607 -0
- package/panel/node_modules/ws/lib/stream.js +161 -0
- package/panel/node_modules/ws/lib/subprotocol.js +62 -0
- package/panel/node_modules/ws/lib/validation.js +152 -0
- package/panel/node_modules/ws/lib/websocket-server.js +562 -0
- package/panel/node_modules/ws/lib/websocket.js +1421 -0
- package/panel/node_modules/ws/package.json +74 -0
- package/panel/node_modules/ws/wrapper.mjs +21 -0
- package/panel/package.json +2 -2
- package/panel/client/contactsheet.js +0 -347
- package/panel/client/framecache.js +0 -98
- package/panel/client/framereader.js +0 -248
- package/panel/client/mogrt.js +0 -328
- package/panel/client/pngcodec.js +0 -524
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ws",
|
|
3
|
+
"version": "8.22.0",
|
|
4
|
+
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"HyBi",
|
|
7
|
+
"Push",
|
|
8
|
+
"RFC-6455",
|
|
9
|
+
"WebSocket",
|
|
10
|
+
"WebSockets",
|
|
11
|
+
"real-time"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/websockets/ws",
|
|
14
|
+
"bugs": "https://github.com/websockets/ws/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/websockets/ws.git"
|
|
18
|
+
},
|
|
19
|
+
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"browser": "./browser.js",
|
|
25
|
+
"import": "./wrapper.mjs",
|
|
26
|
+
"require": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"browser": "browser.js",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=10.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"browser.js",
|
|
36
|
+
"index.js",
|
|
37
|
+
"lib/*.js",
|
|
38
|
+
"wrapper.mjs"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
|
|
42
|
+
"integration": "mocha --throw-deprecation test/*.integration.js",
|
|
43
|
+
"lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"bufferutil": "^4.0.1",
|
|
47
|
+
"utf-8-validate": ">=5.0.2"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"bufferutil": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"utf-8-validate": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@eslint/js": "^10.0.1",
|
|
59
|
+
"benchmark": "^2.1.4",
|
|
60
|
+
"bufferutil": "^4.0.1",
|
|
61
|
+
"eslint": "^10.0.1",
|
|
62
|
+
"eslint-config-prettier": "^10.0.1",
|
|
63
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
64
|
+
"globals": "^17.0.0",
|
|
65
|
+
"mocha": "^8.4.0",
|
|
66
|
+
"nyc": "^15.0.0",
|
|
67
|
+
"prettier": "^3.0.0",
|
|
68
|
+
"utf-8-validate": "^6.0.0"
|
|
69
|
+
},
|
|
70
|
+
"allowScripts": {
|
|
71
|
+
"bufferutil": true,
|
|
72
|
+
"utf-8-validate": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import createWebSocketStream from './lib/stream.js';
|
|
2
|
+
import extension from './lib/extension.js';
|
|
3
|
+
import PerMessageDeflate from './lib/permessage-deflate.js';
|
|
4
|
+
import Receiver from './lib/receiver.js';
|
|
5
|
+
import Sender from './lib/sender.js';
|
|
6
|
+
import subprotocol from './lib/subprotocol.js';
|
|
7
|
+
import WebSocket from './lib/websocket.js';
|
|
8
|
+
import WebSocketServer from './lib/websocket-server.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createWebSocketStream,
|
|
12
|
+
extension,
|
|
13
|
+
PerMessageDeflate,
|
|
14
|
+
Receiver,
|
|
15
|
+
Sender,
|
|
16
|
+
subprotocol,
|
|
17
|
+
WebSocket,
|
|
18
|
+
WebSocketServer
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default WebSocket;
|
package/panel/package.json
CHANGED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
// contactsheet.js — tile several rendered frames into one labelled sheet.
|
|
2
|
-
//
|
|
3
|
-
// Judging motion is one visual question — "does it move the way I think it
|
|
4
|
-
// does" — and answering it used to cost three `screenshot_frame` calls. Three
|
|
5
|
-
// calls is three image blocks resident in the context for the rest of the
|
|
6
|
-
// session, three chances for After Effects to re-serve a stale buffer, and no
|
|
7
|
-
// guarantee the agent lines the frames up in the right order. One sheet is one
|
|
8
|
-
// image, one op, and the order is in the picture. Issue #56.
|
|
9
|
-
//
|
|
10
|
-
// The compositing happens here rather than in ExtendScript for the obvious
|
|
11
|
-
// reason: `packages/jsx` has no pixels, no PNG encoder and no way to draw a
|
|
12
|
-
// character. The panel already decodes and re-encodes frames for the 16-bit
|
|
13
|
-
// conversion, so the sheet is a few hundred lines on top of machinery that
|
|
14
|
-
// exists.
|
|
15
|
-
//
|
|
16
|
-
// Two rules the layout has to keep:
|
|
17
|
-
//
|
|
18
|
-
// * Every requested time gets a cell, in order, whether or not it rendered.
|
|
19
|
-
// A sheet that silently drops the tile that failed no longer maps onto the
|
|
20
|
-
// times that were asked for, and an agent counting tiles left to right
|
|
21
|
-
// would read the wrong frame as the right one.
|
|
22
|
-
// * The time is burned into the picture. Metadata beside an image is not
|
|
23
|
-
// what a model looks at when it is comparing three frames.
|
|
24
|
-
//
|
|
25
|
-
// Node builtins only, and a CommonJS module, for the same reasons as
|
|
26
|
-
// pngcodec.js: `client/main.js` requires it inside CEP's mixed context and
|
|
27
|
-
// `tests/unit/contact-sheet.mjs` requires the same file under plain Node.
|
|
28
|
-
|
|
29
|
-
"use strict";
|
|
30
|
-
|
|
31
|
-
var pngCodec = require("./pngcodec.js");
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Layout
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
// Columns per tile count. Not a formula, because the two things a formula gets
|
|
38
|
-
// wrong are the ones that matter: three frames of an animation read as a strip
|
|
39
|
-
// and should stay on one row, and five should not leave a hole in the middle of
|
|
40
|
-
// a 2-wide grid. Two to six is the whole domain — the schema caps `times` there
|
|
41
|
-
// — so the table is complete rather than a heuristic.
|
|
42
|
-
var COLUMNS_FOR = { 2: 2, 3: 3, 4: 2, 5: 3, 6: 3 };
|
|
43
|
-
|
|
44
|
-
function layoutFor(count) {
|
|
45
|
-
var cols = COLUMNS_FOR[count];
|
|
46
|
-
if (!cols) cols = Math.ceil(Math.sqrt(count));
|
|
47
|
-
return { cols: cols, rows: Math.ceil(count / cols) };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The gutter is opaque on purpose. A transparent one would be invisible behind
|
|
51
|
-
// a transparent overlay comp — which is exactly the kind of comp somebody
|
|
52
|
-
// screenshots three times — and the tiles would run together.
|
|
53
|
-
var GUTTER = 4;
|
|
54
|
-
var BACKGROUND = [38, 38, 38, 255];
|
|
55
|
-
var TILE_BORDER = [150, 150, 150, 255];
|
|
56
|
-
|
|
57
|
-
// Flat blocks for the cells that have no picture. Each carries its status word
|
|
58
|
-
// in the label chip as well; the colour alone is a hint, never the statement.
|
|
59
|
-
var STATUS_FILL = {
|
|
60
|
-
empty: [22, 22, 28, 255],
|
|
61
|
-
stale: [74, 30, 30, 255],
|
|
62
|
-
failed: [74, 50, 20, 255],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// A 5x7 bitmap font, five column bytes per glyph, bit 0 = top row.
|
|
67
|
-
//
|
|
68
|
-
// Burning text into a frame needs a font, and a font file would be a second
|
|
69
|
-
// thing to ship into the CEP extension and keep in sync — the problem `ws`
|
|
70
|
-
// already causes once. Fifty glyphs of pixels weigh nothing and render
|
|
71
|
-
// identically on both platforms, which a system font would not.
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
var FONT = {
|
|
74
|
-
"0": [0x3e, 0x51, 0x49, 0x45, 0x3e],
|
|
75
|
-
"1": [0x00, 0x42, 0x7f, 0x40, 0x00],
|
|
76
|
-
"2": [0x42, 0x61, 0x51, 0x49, 0x46],
|
|
77
|
-
"3": [0x21, 0x41, 0x45, 0x4b, 0x31],
|
|
78
|
-
"4": [0x18, 0x14, 0x12, 0x7f, 0x10],
|
|
79
|
-
"5": [0x27, 0x45, 0x45, 0x45, 0x39],
|
|
80
|
-
"6": [0x3c, 0x4a, 0x49, 0x49, 0x30],
|
|
81
|
-
"7": [0x01, 0x71, 0x09, 0x05, 0x03],
|
|
82
|
-
"8": [0x36, 0x49, 0x49, 0x49, 0x36],
|
|
83
|
-
"9": [0x06, 0x49, 0x49, 0x29, 0x1e],
|
|
84
|
-
".": [0x00, 0x60, 0x60, 0x00, 0x00],
|
|
85
|
-
"-": [0x08, 0x08, 0x08, 0x08, 0x08],
|
|
86
|
-
":": [0x00, 0x36, 0x36, 0x00, 0x00],
|
|
87
|
-
" ": [0x00, 0x00, 0x00, 0x00, 0x00],
|
|
88
|
-
s: [0x48, 0x54, 0x54, 0x54, 0x20],
|
|
89
|
-
A: [0x7e, 0x11, 0x11, 0x11, 0x7e],
|
|
90
|
-
B: [0x7f, 0x49, 0x49, 0x49, 0x36],
|
|
91
|
-
C: [0x3e, 0x41, 0x41, 0x41, 0x22],
|
|
92
|
-
D: [0x7f, 0x41, 0x41, 0x22, 0x1c],
|
|
93
|
-
E: [0x7f, 0x49, 0x49, 0x49, 0x41],
|
|
94
|
-
F: [0x7f, 0x09, 0x09, 0x09, 0x01],
|
|
95
|
-
G: [0x3e, 0x41, 0x49, 0x49, 0x7a],
|
|
96
|
-
H: [0x7f, 0x08, 0x08, 0x08, 0x7f],
|
|
97
|
-
I: [0x00, 0x41, 0x7f, 0x41, 0x00],
|
|
98
|
-
J: [0x20, 0x40, 0x41, 0x3f, 0x01],
|
|
99
|
-
K: [0x7f, 0x08, 0x14, 0x22, 0x41],
|
|
100
|
-
L: [0x7f, 0x40, 0x40, 0x40, 0x40],
|
|
101
|
-
M: [0x7f, 0x02, 0x0c, 0x02, 0x7f],
|
|
102
|
-
N: [0x7f, 0x04, 0x08, 0x10, 0x7f],
|
|
103
|
-
O: [0x3e, 0x41, 0x41, 0x41, 0x3e],
|
|
104
|
-
P: [0x7f, 0x09, 0x09, 0x09, 0x06],
|
|
105
|
-
Q: [0x3e, 0x41, 0x51, 0x21, 0x5e],
|
|
106
|
-
R: [0x7f, 0x09, 0x19, 0x29, 0x46],
|
|
107
|
-
S: [0x46, 0x49, 0x49, 0x49, 0x31],
|
|
108
|
-
T: [0x01, 0x01, 0x7f, 0x01, 0x01],
|
|
109
|
-
U: [0x3f, 0x40, 0x40, 0x40, 0x3f],
|
|
110
|
-
V: [0x1f, 0x20, 0x40, 0x20, 0x1f],
|
|
111
|
-
W: [0x3f, 0x40, 0x38, 0x40, 0x3f],
|
|
112
|
-
X: [0x63, 0x14, 0x08, 0x14, 0x63],
|
|
113
|
-
Y: [0x07, 0x08, 0x70, 0x08, 0x07],
|
|
114
|
-
Z: [0x61, 0x51, 0x49, 0x45, 0x43],
|
|
115
|
-
};
|
|
116
|
-
var GLYPH_W = 5;
|
|
117
|
-
var GLYPH_H = 7;
|
|
118
|
-
var ADVANCE = GLYPH_W + 1;
|
|
119
|
-
|
|
120
|
-
function glyphFor(ch) {
|
|
121
|
-
if (FONT[ch]) return FONT[ch];
|
|
122
|
-
var up = ch.toUpperCase();
|
|
123
|
-
if (FONT[up]) return FONT[up];
|
|
124
|
-
return FONT[" "];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Width in pixels of `text` at `scale`, with no trailing inter-glyph gap. */
|
|
128
|
-
function textWidth(text, scale) {
|
|
129
|
-
if (!text.length) return 0;
|
|
130
|
-
return (text.length * ADVANCE - 1) * scale;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Format a time for the label: shortest form that is still unambiguous.
|
|
135
|
-
* 0 -> "0s", 1.5 -> "1.5s", 2.25 -> "2.25s", 0.041666 -> "0.042s".
|
|
136
|
-
*
|
|
137
|
-
* Three decimals because a 30fps frame is 0.033s apart from its neighbour and
|
|
138
|
-
* two would print two adjacent frames identically — the one case where the
|
|
139
|
-
* label would actively mislead.
|
|
140
|
-
*/
|
|
141
|
-
function formatTime(t) {
|
|
142
|
-
if (typeof t !== "number" || !isFinite(t)) return "?s";
|
|
143
|
-
var s = t.toFixed(3);
|
|
144
|
-
// Trim the zeros the fixed form adds, but never the digit before the point.
|
|
145
|
-
s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
146
|
-
return s + "s";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// Drawing
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
function setPixel(sheet, sheetW, sheetH, x, y, rgba) {
|
|
154
|
-
if (x < 0 || y < 0 || x >= sheetW || y >= sheetH) return;
|
|
155
|
-
var o = (y * sheetW + x) * 4;
|
|
156
|
-
sheet[o] = rgba[0];
|
|
157
|
-
sheet[o + 1] = rgba[1];
|
|
158
|
-
sheet[o + 2] = rgba[2];
|
|
159
|
-
sheet[o + 3] = rgba[3];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function fillRect(sheet, sheetW, sheetH, x, y, w, h, rgba) {
|
|
163
|
-
for (var j = 0; j < h; j++) {
|
|
164
|
-
for (var i = 0; i < w; i++) setPixel(sheet, sheetW, sheetH, x + i, y + j, rgba);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function strokeRect(sheet, sheetW, sheetH, x, y, w, h, rgba) {
|
|
169
|
-
for (var i = 0; i < w; i++) {
|
|
170
|
-
setPixel(sheet, sheetW, sheetH, x + i, y, rgba);
|
|
171
|
-
setPixel(sheet, sheetW, sheetH, x + i, y + h - 1, rgba);
|
|
172
|
-
}
|
|
173
|
-
for (var j = 0; j < h; j++) {
|
|
174
|
-
setPixel(sheet, sheetW, sheetH, x, y + j, rgba);
|
|
175
|
-
setPixel(sheet, sheetW, sheetH, x + w - 1, y + j, rgba);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Draw `text` in an opaque chip whose top-left corner is (x, y).
|
|
181
|
-
*
|
|
182
|
-
* Opaque rather than blended: the chip sits on top of whatever the frame
|
|
183
|
-
* happens to be, and a translucent one over a busy frame is the one case where
|
|
184
|
-
* the label becomes unreadable — which defeats the whole point of burning it in.
|
|
185
|
-
*
|
|
186
|
-
* Returns the chip's { x, y, width, height } so a caller can assert where it
|
|
187
|
-
* landed.
|
|
188
|
-
*/
|
|
189
|
-
function drawLabel(sheet, sheetW, sheetH, x, y, text, scale, ink, chip) {
|
|
190
|
-
var pad = 2 * scale;
|
|
191
|
-
var w = textWidth(text, scale) + pad * 2;
|
|
192
|
-
var h = GLYPH_H * scale + pad * 2;
|
|
193
|
-
fillRect(sheet, sheetW, sheetH, x, y, w, h, chip);
|
|
194
|
-
var penX = x + pad;
|
|
195
|
-
for (var c = 0; c < text.length; c++) {
|
|
196
|
-
var glyph = glyphFor(text.charAt(c));
|
|
197
|
-
for (var col = 0; col < GLYPH_W; col++) {
|
|
198
|
-
var bits = glyph[col];
|
|
199
|
-
for (var row = 0; row < GLYPH_H; row++) {
|
|
200
|
-
if (!(bits & (1 << row))) continue;
|
|
201
|
-
fillRect(
|
|
202
|
-
sheet, sheetW, sheetH,
|
|
203
|
-
penX + col * scale, y + pad + row * scale,
|
|
204
|
-
scale, scale, ink
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
penX += ADVANCE * scale;
|
|
209
|
-
}
|
|
210
|
-
return { x: x, y: y, width: w, height: h };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Copy one decoded frame into a cell, converting whatever channel count it
|
|
215
|
-
* arrived with to the sheet's RGBA.
|
|
216
|
-
*
|
|
217
|
-
* Frames larger than the cell are cropped rather than resampled. They should
|
|
218
|
-
* never be — every tile in a sheet is rendered at the same downsample from the
|
|
219
|
-
* same comp — but a crop keeps a surprise visible in the picture, where a
|
|
220
|
-
* silent resample would make a wrong frame look like a right one.
|
|
221
|
-
*/
|
|
222
|
-
function blitTile(sheet, sheetW, sheetH, dstX, dstY, cellW, cellH, tile) {
|
|
223
|
-
var ch = tile.channels;
|
|
224
|
-
var w = Math.min(tile.width, cellW);
|
|
225
|
-
var h = Math.min(tile.height, cellH);
|
|
226
|
-
for (var y = 0; y < h; y++) {
|
|
227
|
-
for (var x = 0; x < w; x++) {
|
|
228
|
-
var si = (y * tile.width + x) * ch;
|
|
229
|
-
var r, g, b, a;
|
|
230
|
-
if (ch === 1) { r = g = b = tile.pixels[si]; a = 255; }
|
|
231
|
-
else if (ch === 2) { r = g = b = tile.pixels[si]; a = tile.pixels[si + 1]; }
|
|
232
|
-
else if (ch === 3) { r = tile.pixels[si]; g = tile.pixels[si + 1]; b = tile.pixels[si + 2]; a = 255; }
|
|
233
|
-
else { r = tile.pixels[si]; g = tile.pixels[si + 1]; b = tile.pixels[si + 2]; a = tile.pixels[si + 3]; }
|
|
234
|
-
var o = ((dstY + y) * sheetW + (dstX + x)) * 4;
|
|
235
|
-
sheet[o] = r;
|
|
236
|
-
sheet[o + 1] = g;
|
|
237
|
-
sheet[o + 2] = b;
|
|
238
|
-
sheet[o + 3] = a;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Compose the sheet.
|
|
245
|
-
*
|
|
246
|
-
* `tiles` is one entry per *requested* time, in order:
|
|
247
|
-
* { time, status, pixels?, width?, height?, channels?, note? }
|
|
248
|
-
* where status is "ok" for a frame that rendered and anything in STATUS_FILL
|
|
249
|
-
* ("empty", "stale", "failed") for one that did not.
|
|
250
|
-
*
|
|
251
|
-
* `opts.cellWidth`/`cellHeight` are what a tile is expected to measure — passed
|
|
252
|
-
* in rather than taken from the first tile, because the first tile is exactly
|
|
253
|
-
* the one that may have failed and have no dimensions at all.
|
|
254
|
-
*
|
|
255
|
-
* Returns { buffer, width, height, cols, rows, cellWidth, cellHeight, tiles },
|
|
256
|
-
* where each entry of `tiles` carries the cell rectangle it was drawn into.
|
|
257
|
-
*/
|
|
258
|
-
function composeContactSheet(tiles, opts) {
|
|
259
|
-
if (!tiles || !tiles.length) throw new Error("composeContactSheet needs at least one tile");
|
|
260
|
-
var o = opts || {};
|
|
261
|
-
var cellW = o.cellWidth;
|
|
262
|
-
var cellH = o.cellHeight;
|
|
263
|
-
for (var t = 0; t < tiles.length; t++) {
|
|
264
|
-
if (tiles[t].status === "ok" && tiles[t].width && tiles[t].height) {
|
|
265
|
-
// A rendered frame is the authority on its own size; the expectation is
|
|
266
|
-
// only a fallback for the cells that have no picture.
|
|
267
|
-
if (tiles[t].width > cellW) cellW = tiles[t].width;
|
|
268
|
-
if (tiles[t].height > cellH) cellH = tiles[t].height;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (!(cellW > 0) || !(cellH > 0)) throw new Error("composeContactSheet needs a positive cell size");
|
|
272
|
-
|
|
273
|
-
var lay = layoutFor(tiles.length);
|
|
274
|
-
var cols = lay.cols;
|
|
275
|
-
var rows = lay.rows;
|
|
276
|
-
var gutter = o.gutter === undefined ? GUTTER : o.gutter;
|
|
277
|
-
var sheetW = cols * cellW + (cols + 1) * gutter;
|
|
278
|
-
var sheetH = rows * cellH + (rows + 1) * gutter;
|
|
279
|
-
|
|
280
|
-
var sheet = Buffer.alloc(sheetW * sheetH * 4);
|
|
281
|
-
fillRect(sheet, sheetW, sheetH, 0, 0, sheetW, sheetH, BACKGROUND);
|
|
282
|
-
|
|
283
|
-
// One label size for the whole sheet, from the cell width, so every tile is
|
|
284
|
-
// annotated identically and a 4K sheet does not get a 6px caption.
|
|
285
|
-
var scale = o.labelScale;
|
|
286
|
-
if (!scale) scale = Math.max(1, Math.min(4, Math.round(cellW / 300)));
|
|
287
|
-
|
|
288
|
-
var out = [];
|
|
289
|
-
for (var i = 0; i < tiles.length; i++) {
|
|
290
|
-
var tile = tiles[i];
|
|
291
|
-
var col = i % cols;
|
|
292
|
-
var row = Math.floor(i / cols);
|
|
293
|
-
var x = gutter + col * (cellW + gutter);
|
|
294
|
-
var y = gutter + row * (cellH + gutter);
|
|
295
|
-
|
|
296
|
-
if (tile.status === "ok" && tile.pixels) {
|
|
297
|
-
blitTile(sheet, sheetW, sheetH, x, y, cellW, cellH, tile);
|
|
298
|
-
} else {
|
|
299
|
-
var fill = STATUS_FILL[tile.status] || STATUS_FILL.failed;
|
|
300
|
-
fillRect(sheet, sheetW, sheetH, x, y, cellW, cellH, fill);
|
|
301
|
-
}
|
|
302
|
-
strokeRect(sheet, sheetW, sheetH, x, y, cellW, cellH, TILE_BORDER);
|
|
303
|
-
|
|
304
|
-
var text = formatTime(tile.time);
|
|
305
|
-
if (tile.status !== "ok") text += " " + String(tile.status).toUpperCase();
|
|
306
|
-
var chip = drawLabel(
|
|
307
|
-
sheet, sheetW, sheetH,
|
|
308
|
-
x + gutter, y + gutter,
|
|
309
|
-
text, scale,
|
|
310
|
-
[255, 255, 255, 255], [0, 0, 0, 255]
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
out.push({
|
|
314
|
-
time: tile.time,
|
|
315
|
-
status: tile.status,
|
|
316
|
-
x: x,
|
|
317
|
-
y: y,
|
|
318
|
-
width: cellW,
|
|
319
|
-
height: cellH,
|
|
320
|
-
label: text,
|
|
321
|
-
labelRect: chip,
|
|
322
|
-
note: tile.note,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
buffer: pngCodec.encodePng8(sheetW, sheetH, 6, sheet),
|
|
328
|
-
width: sheetW,
|
|
329
|
-
height: sheetH,
|
|
330
|
-
cols: cols,
|
|
331
|
-
rows: rows,
|
|
332
|
-
cellWidth: cellW,
|
|
333
|
-
cellHeight: cellH,
|
|
334
|
-
tiles: out,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
module.exports = {
|
|
339
|
-
composeContactSheet: composeContactSheet,
|
|
340
|
-
// Exported for the tests, which have to be able to state the expected sheet
|
|
341
|
-
// size without re-deriving the layout from the code under test.
|
|
342
|
-
layoutFor: layoutFor,
|
|
343
|
-
formatTime: formatTime,
|
|
344
|
-
textWidth: textWidth,
|
|
345
|
-
GLYPH_H: GLYPH_H,
|
|
346
|
-
GUTTER: GUTTER,
|
|
347
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// framecache.js — recent screenshot identity, for spotting a stale render.
|
|
2
|
-
//
|
|
3
|
-
// After Effects' render path sometimes answers a screenshot request with a
|
|
4
|
-
// buffer it produced for an earlier one, and reports success either way. Nothing
|
|
5
|
-
// else in the response distinguishes the two: the temp file is freshly named,
|
|
6
|
-
// the op returns ok, the PNG is well formed. The one signal that survives is
|
|
7
|
-
// content identity — two *different* requests coming back with byte-identical
|
|
8
|
-
// pixels is not a coincidence, it is one buffer handed out twice. Reported and
|
|
9
|
-
// evidenced as issue #29: byte-identical results for unrelated comps at
|
|
10
|
-
// unrelated times, and at different downsample factors, which cannot even be the
|
|
11
|
-
// same number of pixels.
|
|
12
|
-
//
|
|
13
|
-
// This lives in the panel rather than in the MCP server for two reasons. The
|
|
14
|
-
// panel is the only thing that sees every render — the documented workaround for
|
|
15
|
-
// this bug POSTs /op directly, and the check has to hold for that caller too.
|
|
16
|
-
// And it outlives any one MCP client process, which is the right lifetime for
|
|
17
|
-
// "what has this After Effects session rendered recently".
|
|
18
|
-
//
|
|
19
|
-
// Kept in its own file so tests/unit/frame-cache.mjs can require it without a
|
|
20
|
-
// DOM or a running After Effects.
|
|
21
|
-
|
|
22
|
-
"use strict";
|
|
23
|
-
|
|
24
|
-
// Enough history to catch the bug — it shows up within a handful of calls — and
|
|
25
|
-
// small enough that a long session cannot turn a linear scan into a cost.
|
|
26
|
-
var DEFAULT_LIMIT = 24;
|
|
27
|
-
// Long enough to span a working sequence of screenshots, short enough that a
|
|
28
|
-
// comp genuinely edited between two visits is not compared against its own past.
|
|
29
|
-
var DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
30
|
-
|
|
31
|
-
function createFrameCache(options) {
|
|
32
|
-
var opts = options || {};
|
|
33
|
-
var limit = opts.limit || DEFAULT_LIMIT;
|
|
34
|
-
var ttlMs = opts.ttlMs || DEFAULT_TTL_MS;
|
|
35
|
-
var now = opts.now || function () { return Date.now(); };
|
|
36
|
-
var entries = [];
|
|
37
|
-
|
|
38
|
-
function prune() {
|
|
39
|
-
var cutoff = now() - ttlMs;
|
|
40
|
-
var kept = [];
|
|
41
|
-
for (var i = 0; i < entries.length; i++) {
|
|
42
|
-
if (entries[i].at > cutoff) kept.push(entries[i]);
|
|
43
|
-
}
|
|
44
|
-
entries = kept;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
/**
|
|
49
|
-
* The most recent *different* request that produced these exact pixels, or
|
|
50
|
-
* null. Matching the same key proves nothing: repeating a screenshot is
|
|
51
|
-
* allowed to return the same picture, and an agent retrying after a stale
|
|
52
|
-
* report must not be told the retry is stale too.
|
|
53
|
-
*/
|
|
54
|
-
match: function (key, hash) {
|
|
55
|
-
prune();
|
|
56
|
-
for (var i = entries.length - 1; i >= 0; i--) {
|
|
57
|
-
if (entries[i].hash === hash && entries[i].key !== key) {
|
|
58
|
-
return {
|
|
59
|
-
key: entries[i].key,
|
|
60
|
-
label: entries[i].label,
|
|
61
|
-
bytes: entries[i].bytes,
|
|
62
|
-
ageMs: now() - entries[i].at,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Record a frame that was actually delivered. Only successful, non-empty
|
|
71
|
-
* renders belong here: an empty frame is legitimately identical to every
|
|
72
|
-
* other empty frame, and remembering rejected ones would make the first
|
|
73
|
-
* stale buffer poison every later request that happens to repeat it.
|
|
74
|
-
*/
|
|
75
|
-
remember: function (key, hash, meta) {
|
|
76
|
-
prune();
|
|
77
|
-
// One entry per request key, so re-running the same screenshot replaces
|
|
78
|
-
// its own record instead of filling the window with copies of itself.
|
|
79
|
-
var kept = [];
|
|
80
|
-
for (var i = 0; i < entries.length; i++) {
|
|
81
|
-
if (entries[i].key !== key) kept.push(entries[i]);
|
|
82
|
-
}
|
|
83
|
-
entries = kept;
|
|
84
|
-
entries.push({
|
|
85
|
-
key: key,
|
|
86
|
-
hash: hash,
|
|
87
|
-
at: now(),
|
|
88
|
-
label: (meta && meta.label) || key,
|
|
89
|
-
bytes: (meta && meta.bytes) || 0,
|
|
90
|
-
});
|
|
91
|
-
while (entries.length > limit) entries.shift();
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
size: function () { prune(); return entries.length; },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
module.exports = { createFrameCache: createFrameCache };
|