@pinepaper.studio/mcp-server 1.5.1 → 1.5.4
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 +6 -4
- package/dist/agent/types.d.ts +16 -16
- package/dist/browser/puppeteer-controller.d.ts +5 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +11 -3
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/browser/url-target.d.ts +26 -0
- package/dist/browser/url-target.d.ts.map +1 -0
- package/dist/browser/url-target.js +50 -0
- package/dist/browser/url-target.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -109
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.d.ts +85 -0
- package/dist/ontology/design-graph.d.ts.map +1 -0
- package/dist/ontology/design-graph.js +944 -0
- package/dist/ontology/design-graph.js.map +1 -0
- package/dist/ontology/hints.d.ts +71 -0
- package/dist/ontology/hints.d.ts.map +1 -0
- package/dist/ontology/hints.js +189 -0
- package/dist/ontology/hints.js.map +1 -0
- package/dist/ontology/index.d.ts +11 -0
- package/dist/ontology/index.d.ts.map +1 -0
- package/dist/ontology/index.js +14 -0
- package/dist/ontology/index.js.map +1 -0
- package/dist/ontology/semantic-vocabulary.d.ts +12 -0
- package/dist/ontology/semantic-vocabulary.d.ts.map +1 -0
- package/dist/ontology/semantic-vocabulary.js +26 -0
- package/dist/ontology/semantic-vocabulary.js.map +1 -0
- package/dist/ontology/types.d.ts +255 -0
- package/dist/ontology/types.d.ts.map +1 -0
- package/dist/ontology/types.js +10 -0
- package/dist/ontology/types.js.map +1 -0
- package/dist/ontology/validator.d.ts +36 -0
- package/dist/ontology/validator.d.ts.map +1 -0
- package/dist/ontology/validator.js +374 -0
- package/dist/ontology/validator.js.map +1 -0
- package/dist/ontology/vocabulary.d.ts +34 -0
- package/dist/ontology/vocabulary.d.ts.map +1 -0
- package/dist/ontology/vocabulary.js +429 -0
- package/dist/ontology/vocabulary.js.map +1 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +146 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts +5 -2
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +56 -235
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +1842 -1784
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers/camera.d.ts +19 -0
- package/dist/tools/handlers/camera.d.ts.map +1 -0
- package/dist/tools/handlers/camera.js +94 -0
- package/dist/tools/handlers/camera.js.map +1 -0
- package/dist/tools/handlers/export.d.ts +12 -0
- package/dist/tools/handlers/export.d.ts.map +1 -0
- package/dist/tools/handlers/export.js +30 -0
- package/dist/tools/handlers/export.js.map +1 -0
- package/dist/tools/handlers/font.d.ts +12 -0
- package/dist/tools/handlers/font.d.ts.map +1 -0
- package/dist/tools/handlers/font.js +135 -0
- package/dist/tools/handlers/font.js.map +1 -0
- package/dist/tools/handlers/maps.d.ts +19 -0
- package/dist/tools/handlers/maps.d.ts.map +1 -0
- package/dist/tools/handlers/maps.js +184 -0
- package/dist/tools/handlers/maps.js.map +1 -0
- package/dist/tools/handlers/ontology.d.ts +20 -0
- package/dist/tools/handlers/ontology.d.ts.map +1 -0
- package/dist/tools/handlers/ontology.js +185 -0
- package/dist/tools/handlers/ontology.js.map +1 -0
- package/dist/tools/handlers/tool-guide.d.ts +14 -0
- package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
- package/dist/tools/handlers/tool-guide.js +52 -0
- package/dist/tools/handlers/tool-guide.js.map +1 -0
- package/dist/tools/handlers.d.ts +18 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +321 -574
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts +4 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +65 -54
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +67 -46
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +49 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1456 -39
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +1381 -404
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +426 -20
- package/dist/types/schemas.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DesignGraph — Design Knowledge Graph for PinePaper templates
|
|
3
|
+
*
|
|
4
|
+
* Formalizes every template as a typed, annotated graph with a Schema.org-aligned
|
|
5
|
+
* vocabulary (pp: namespace). Provides vocabulary definition, graph extraction from
|
|
6
|
+
* template definitions, JSON-LD export, and structural similarity scoring.
|
|
7
|
+
*
|
|
8
|
+
* Ported from FxTool/js/DesignGraph.js to typed TypeScript.
|
|
9
|
+
* extractFromCanvas() is omitted (requires live browser app instance).
|
|
10
|
+
*
|
|
11
|
+
* @module ontology/design-graph
|
|
12
|
+
* @version 0.4.0
|
|
13
|
+
*/
|
|
14
|
+
import { PP_VOCABULARY, ITEM_TYPE_MAP, RELATION_TYPE_MAP, DIAGRAM_SHAPE_MAP, CONNECTOR_STYLE_MAP, EASING_TO_MATH } from './vocabulary.js';
|
|
15
|
+
import { PP_SEMANTIC_VOCABULARY } from './semantic-vocabulary.js';
|
|
16
|
+
export class DesignGraph {
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Vocabulary Access
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
20
|
+
getVocabulary() {
|
|
21
|
+
return PP_VOCABULARY;
|
|
22
|
+
}
|
|
23
|
+
/** Map a PinePaper item type string to its pp: type. */
|
|
24
|
+
getNodeType(itemType) {
|
|
25
|
+
if (!itemType)
|
|
26
|
+
return 'pp:Group';
|
|
27
|
+
const lower = itemType.toLowerCase();
|
|
28
|
+
return ITEM_TYPE_MAP[lower] || 'pp:Group';
|
|
29
|
+
}
|
|
30
|
+
/** Map a PinePaper relation name to its pp: edge type. */
|
|
31
|
+
getEdgeType(relationName) {
|
|
32
|
+
if (!relationName)
|
|
33
|
+
return null;
|
|
34
|
+
return RELATION_TYPE_MAP[relationName] || null;
|
|
35
|
+
}
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Hierarchy Traversal
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
39
|
+
/** Get ordered ancestry chain for a type: [type, parent, grandparent, ...] */
|
|
40
|
+
getTypeHierarchy(ppType) {
|
|
41
|
+
return this._getHierarchy(ppType, PP_VOCABULARY.types);
|
|
42
|
+
}
|
|
43
|
+
/** Get direct children of a type. */
|
|
44
|
+
getTypeChildren(ppType) {
|
|
45
|
+
return Object.keys(PP_VOCABULARY.types).filter(key => PP_VOCABULARY.types[key].parentType === ppType);
|
|
46
|
+
}
|
|
47
|
+
/** Get ordered ancestry chain for an edge type. */
|
|
48
|
+
getEdgeHierarchy(ppEdge) {
|
|
49
|
+
return this._getHierarchy(ppEdge, PP_VOCABULARY.edges);
|
|
50
|
+
}
|
|
51
|
+
/** Get ordered ancestry chain for a generator. */
|
|
52
|
+
getGeneratorHierarchy(genKey) {
|
|
53
|
+
return this._getHierarchy(genKey, PP_VOCABULARY.generators);
|
|
54
|
+
}
|
|
55
|
+
/** Get properties applicable to a type, including inherited from parents. */
|
|
56
|
+
getPropertiesFor(ppType) {
|
|
57
|
+
const hierarchy = this.getTypeHierarchy(ppType);
|
|
58
|
+
const result = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const [propName, propDef] of Object.entries(PP_VOCABULARY.properties)) {
|
|
61
|
+
if (seen.has(propName))
|
|
62
|
+
continue;
|
|
63
|
+
if (Array.isArray(propDef.appliesTo)) {
|
|
64
|
+
for (const target of propDef.appliesTo) {
|
|
65
|
+
if (hierarchy.includes(target)) {
|
|
66
|
+
result.push({ name: propName, ...propDef });
|
|
67
|
+
seen.add(propName);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** Get only animatable properties for a type. */
|
|
76
|
+
getAnimatableProperties(ppType) {
|
|
77
|
+
return this.getPropertiesFor(ppType)
|
|
78
|
+
.filter(p => p.animatable === true)
|
|
79
|
+
.map(p => p.name);
|
|
80
|
+
}
|
|
81
|
+
/** Check if typeA is a subtype of typeB (walks ancestry). */
|
|
82
|
+
isSubtypeOf(typeA, typeB) {
|
|
83
|
+
if (typeA === typeB)
|
|
84
|
+
return true;
|
|
85
|
+
const hierarchy = this.getTypeHierarchy(typeA);
|
|
86
|
+
return hierarchy.includes(typeB);
|
|
87
|
+
}
|
|
88
|
+
/** Walk parentType chain in a vocabulary section. */
|
|
89
|
+
_getHierarchy(key, section) {
|
|
90
|
+
const chain = [];
|
|
91
|
+
let current = key;
|
|
92
|
+
const visited = new Set();
|
|
93
|
+
while (current && section[current] && !visited.has(current)) {
|
|
94
|
+
visited.add(current);
|
|
95
|
+
chain.push(current);
|
|
96
|
+
current = section[current].parentType || null;
|
|
97
|
+
}
|
|
98
|
+
return chain;
|
|
99
|
+
}
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
101
|
+
// Static Analysis — from template definition object
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
103
|
+
/** Extract a TemplateGraph from a template definition object (static analysis). */
|
|
104
|
+
extractFromDefinition(template) {
|
|
105
|
+
if (!template)
|
|
106
|
+
throw new Error('Template is required');
|
|
107
|
+
const graph = {
|
|
108
|
+
templateId: template.id || 'unknown',
|
|
109
|
+
templateName: template.name || 'Untitled',
|
|
110
|
+
category: template.category || 'uncategorized',
|
|
111
|
+
dimensions: template.dimensions || { width: 800, height: 600 },
|
|
112
|
+
duration: template.duration || 0,
|
|
113
|
+
nodes: [],
|
|
114
|
+
edges: [],
|
|
115
|
+
mathFunctions: [],
|
|
116
|
+
patterns: [],
|
|
117
|
+
generator: null,
|
|
118
|
+
fingerprint: null,
|
|
119
|
+
};
|
|
120
|
+
const data = template.data || {};
|
|
121
|
+
// --- Extract nodes from items ---
|
|
122
|
+
if (Array.isArray(data.items)) {
|
|
123
|
+
for (const item of data.items) {
|
|
124
|
+
graph.nodes.push(this._itemToNode(item));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// --- Extract edges from relations ---
|
|
128
|
+
if (Array.isArray(data.relations)) {
|
|
129
|
+
for (const rel of data.relations) {
|
|
130
|
+
const edge = this._relationToEdge(rel);
|
|
131
|
+
if (edge)
|
|
132
|
+
graph.edges.push(edge);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// --- Extract mask edges (inline mask definitions on items) ---
|
|
136
|
+
if (Array.isArray(data.items)) {
|
|
137
|
+
for (const item of data.items) {
|
|
138
|
+
if (item.mask) {
|
|
139
|
+
const itemId = item.id || item.name || null;
|
|
140
|
+
graph.edges.push({
|
|
141
|
+
source: itemId ?? null,
|
|
142
|
+
target: null,
|
|
143
|
+
type: 'pp:maskedBy',
|
|
144
|
+
params: {
|
|
145
|
+
maskType: item.mask.type || 'rectangle',
|
|
146
|
+
animated: !!item.mask.animated,
|
|
147
|
+
animation: item.mask.animation || null,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
// Update node type to MaskedItem
|
|
151
|
+
const node = graph.nodes.find(n => n.id === itemId);
|
|
152
|
+
if (node)
|
|
153
|
+
node.type = 'pp:MaskedItem';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// --- Extract connector edges (sourceId/targetId on connector items) ---
|
|
158
|
+
if (Array.isArray(data.items)) {
|
|
159
|
+
for (const item of data.items) {
|
|
160
|
+
if (item.type === 'connector' && item.sourceId && item.targetId) {
|
|
161
|
+
graph.edges.push({
|
|
162
|
+
source: item.sourceId,
|
|
163
|
+
target: item.targetId,
|
|
164
|
+
type: 'pp:connectsTo',
|
|
165
|
+
params: {
|
|
166
|
+
routing: item.routing || null,
|
|
167
|
+
lineStyle: item.lineStyle || null,
|
|
168
|
+
headStyle: item.headStyle || null,
|
|
169
|
+
label: item.label || null,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// --- Extract generator info ---
|
|
176
|
+
if (data.backgroundGenerator) {
|
|
177
|
+
const genName = data.backgroundGenerator;
|
|
178
|
+
const genDef = PP_VOCABULARY.generators[genName];
|
|
179
|
+
graph.generator = {
|
|
180
|
+
name: genName,
|
|
181
|
+
type: genDef ? genDef.category || 'unknown' : 'unknown',
|
|
182
|
+
mathFunctions: genDef && genDef.mathFunctions ? genDef.mathFunctions : [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// --- Detect math functions ---
|
|
186
|
+
graph.mathFunctions = this.detectMathFunctions(graph);
|
|
187
|
+
// --- Detect patterns ---
|
|
188
|
+
graph.patterns = this.detectPatterns(graph);
|
|
189
|
+
// --- Semantic inference + merge ---
|
|
190
|
+
graph.semantics = this._mergeSemantics(template, graph);
|
|
191
|
+
// --- Compute fingerprint ---
|
|
192
|
+
graph.fingerprint = this.computeFingerprint(graph);
|
|
193
|
+
return graph;
|
|
194
|
+
}
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
196
|
+
// Math Function Detection
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
198
|
+
/** Analyze a TemplateGraph to identify which mathematical functions are used. */
|
|
199
|
+
detectMathFunctions(graph) {
|
|
200
|
+
const funcs = new Set();
|
|
201
|
+
// 1. Detect from keyframe easings on nodes
|
|
202
|
+
for (const node of graph.nodes) {
|
|
203
|
+
if (node.properties && node.properties.hasKeyframes) {
|
|
204
|
+
funcs.add('keyframeLerp');
|
|
205
|
+
funcs.add('lerp');
|
|
206
|
+
if (Array.isArray(node.properties.easings)) {
|
|
207
|
+
for (const easing of node.properties.easings) {
|
|
208
|
+
if (Array.isArray(easing)) {
|
|
209
|
+
funcs.add('customCubicBezier');
|
|
210
|
+
}
|
|
211
|
+
else if (EASING_TO_MATH[easing]) {
|
|
212
|
+
funcs.add(EASING_TO_MATH[easing]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Trim path
|
|
218
|
+
if (node.properties && node.properties.trimPath) {
|
|
219
|
+
funcs.add('trimPath');
|
|
220
|
+
}
|
|
221
|
+
// Color animation detection
|
|
222
|
+
if (node.properties && node.properties.hasKeyframes && node.properties.hasColorAnimation) {
|
|
223
|
+
funcs.add('colorLerp');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 2. Detect from edges (relations)
|
|
227
|
+
for (const edge of graph.edges) {
|
|
228
|
+
const edgeDef = PP_VOCABULARY.edges[edge.type];
|
|
229
|
+
if (edgeDef && Array.isArray(edgeDef.mathFunctions)) {
|
|
230
|
+
for (const f of edgeDef.mathFunctions) {
|
|
231
|
+
funcs.add(f);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// 3. Detect from generator
|
|
236
|
+
if (graph.generator && Array.isArray(graph.generator.mathFunctions)) {
|
|
237
|
+
for (const f of graph.generator.mathFunctions) {
|
|
238
|
+
funcs.add(f);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return Array.from(funcs).sort();
|
|
242
|
+
}
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Pattern Detection
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
246
|
+
/** Match graph structure against known design patterns. */
|
|
247
|
+
detectPatterns(graph) {
|
|
248
|
+
const detected = [];
|
|
249
|
+
const edgeTypes = new Set(graph.edges.map(e => e.type));
|
|
250
|
+
const nodeTypes = new Set(graph.nodes.map(n => n.type));
|
|
251
|
+
const mathFuncs = new Set(graph.mathFunctions);
|
|
252
|
+
for (const [patternKey, patternDef] of Object.entries(PP_VOCABULARY.patterns)) {
|
|
253
|
+
let matched = false;
|
|
254
|
+
// Check required edges
|
|
255
|
+
if (Array.isArray(patternDef.requiredEdges)) {
|
|
256
|
+
matched = patternDef.requiredEdges.some(e => edgeTypes.has(e));
|
|
257
|
+
}
|
|
258
|
+
// Check required node types (hierarchy-aware: subtypes match)
|
|
259
|
+
if (!matched && Array.isArray(patternDef.nodeTypes)) {
|
|
260
|
+
matched = patternDef.nodeTypes.some(requiredType => nodeTypes.has(requiredType) ||
|
|
261
|
+
Array.from(nodeTypes).some(nt => this.isSubtypeOf(nt, requiredType)));
|
|
262
|
+
}
|
|
263
|
+
// Check required math functions
|
|
264
|
+
if (!matched && Array.isArray(patternDef.mathFunctions)) {
|
|
265
|
+
matched = patternDef.mathFunctions.some(f => mathFuncs.has(f));
|
|
266
|
+
}
|
|
267
|
+
// Special case: proceduralBackground (detected by generator presence)
|
|
268
|
+
if (patternKey === 'proceduralBackground' && graph.generator) {
|
|
269
|
+
matched = true;
|
|
270
|
+
}
|
|
271
|
+
if (matched)
|
|
272
|
+
detected.push(patternKey);
|
|
273
|
+
}
|
|
274
|
+
return detected.sort();
|
|
275
|
+
}
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
277
|
+
// Fingerprint
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
279
|
+
/** Compute a structural fingerprint for similarity comparison. */
|
|
280
|
+
computeFingerprint(graph) {
|
|
281
|
+
const nodeTypeCounts = {};
|
|
282
|
+
for (const node of graph.nodes) {
|
|
283
|
+
nodeTypeCounts[node.type] = (nodeTypeCounts[node.type] || 0) + 1;
|
|
284
|
+
}
|
|
285
|
+
const edgeTypeCounts = {};
|
|
286
|
+
for (const edge of graph.edges) {
|
|
287
|
+
edgeTypeCounts[edge.type] = (edgeTypeCounts[edge.type] || 0) + 1;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
nodeTypeCounts,
|
|
291
|
+
edgeTypeCounts,
|
|
292
|
+
mathFunctions: graph.mathFunctions || [],
|
|
293
|
+
patterns: graph.patterns || [],
|
|
294
|
+
totalNodes: graph.nodes.length,
|
|
295
|
+
totalEdges: graph.edges.length,
|
|
296
|
+
hasGenerator: !!graph.generator,
|
|
297
|
+
generatorType: graph.generator ? graph.generator.name : null,
|
|
298
|
+
semantics: graph.semantics || null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
302
|
+
// JSON-LD Export
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
304
|
+
/** Produce a Schema.org + pp: annotated JSON-LD document from a TemplateGraph. */
|
|
305
|
+
toJsonLd(graph) {
|
|
306
|
+
const duration = graph.duration || 0;
|
|
307
|
+
const durationISO = duration > 0 ? `PT${duration}S` : null;
|
|
308
|
+
const jsonLd = {
|
|
309
|
+
'@context': {
|
|
310
|
+
'schema': 'https://schema.org/',
|
|
311
|
+
'pp': 'https://pinepaper.studio/ontology/',
|
|
312
|
+
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
|
|
313
|
+
'skos': 'http://www.w3.org/2004/02/skos/core#',
|
|
314
|
+
'owl': 'http://www.w3.org/2002/07/owl#',
|
|
315
|
+
'bpmn': 'https://www.omg.org/spec/BPMN/20100524/MODEL/',
|
|
316
|
+
},
|
|
317
|
+
'@type': 'pp:Template',
|
|
318
|
+
'@id': `pp:template/${graph.templateId}`,
|
|
319
|
+
'schema:name': graph.templateName,
|
|
320
|
+
'schema:artform': 'motion graphic',
|
|
321
|
+
'schema:artMedium': 'digital',
|
|
322
|
+
'pp:category': graph.category,
|
|
323
|
+
'pp:dimensions': {
|
|
324
|
+
'schema:width': graph.dimensions.width,
|
|
325
|
+
'schema:height': graph.dimensions.height,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
if (durationISO) {
|
|
329
|
+
jsonLd['pp:duration'] = { '@type': 'schema:Duration', '@value': durationISO };
|
|
330
|
+
}
|
|
331
|
+
// Nodes
|
|
332
|
+
jsonLd['pp:nodes'] = graph.nodes.map(node => {
|
|
333
|
+
const nodeDoc = {
|
|
334
|
+
'@id': `pp:node/${node.id || 'anonymous'}`,
|
|
335
|
+
'@type': node.type,
|
|
336
|
+
};
|
|
337
|
+
if (node.label)
|
|
338
|
+
nodeDoc['schema:name'] = node.label;
|
|
339
|
+
const typeDef = PP_VOCABULARY.types[node.type];
|
|
340
|
+
if (typeDef && typeDef.parentType) {
|
|
341
|
+
nodeDoc['rdfs:subClassOf'] = typeDef.parentType;
|
|
342
|
+
}
|
|
343
|
+
if (node.properties) {
|
|
344
|
+
if (node.properties.hasKeyframes)
|
|
345
|
+
nodeDoc['pp:hasKeyframes'] = true;
|
|
346
|
+
if (node.properties.keyframeCount > 0)
|
|
347
|
+
nodeDoc['pp:keyframeCount'] = node.properties.keyframeCount;
|
|
348
|
+
if (node.properties.easings && node.properties.easings.length > 0) {
|
|
349
|
+
nodeDoc['pp:easings'] = node.properties.easings.filter(e => typeof e === 'string');
|
|
350
|
+
}
|
|
351
|
+
if (node.properties.trimPath)
|
|
352
|
+
nodeDoc['pp:trimPath'] = true;
|
|
353
|
+
if (node.properties.blendMode)
|
|
354
|
+
nodeDoc['pp:blendMode'] = node.properties.blendMode;
|
|
355
|
+
if (node.properties.masked)
|
|
356
|
+
nodeDoc['pp:masked'] = true;
|
|
357
|
+
if (node.properties.maskType)
|
|
358
|
+
nodeDoc['pp:maskType'] = node.properties.maskType;
|
|
359
|
+
if (node.properties.animationType && node.properties.animationType !== 'keyframe') {
|
|
360
|
+
nodeDoc['pp:animationType'] = node.properties.animationType;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return nodeDoc;
|
|
364
|
+
});
|
|
365
|
+
// Edges
|
|
366
|
+
jsonLd['pp:edges'] = graph.edges.map(edge => {
|
|
367
|
+
const edgeDoc = {
|
|
368
|
+
'@type': edge.type,
|
|
369
|
+
'pp:source': edge.source ? `pp:node/${edge.source}` : null,
|
|
370
|
+
'pp:target': edge.target ? `pp:node/${edge.target}` : null,
|
|
371
|
+
};
|
|
372
|
+
const edgeDef = PP_VOCABULARY.edges[edge.type];
|
|
373
|
+
if (edgeDef && edgeDef.parentType) {
|
|
374
|
+
edgeDoc['rdfs:subClassOf'] = edgeDef.parentType;
|
|
375
|
+
}
|
|
376
|
+
if (edge.params && Object.keys(edge.params).length > 0) {
|
|
377
|
+
edgeDoc['pp:params'] = edge.params;
|
|
378
|
+
}
|
|
379
|
+
return edgeDoc;
|
|
380
|
+
});
|
|
381
|
+
// Math functions
|
|
382
|
+
if (graph.mathFunctions.length > 0) {
|
|
383
|
+
jsonLd['pp:mathFunctions'] = graph.mathFunctions;
|
|
384
|
+
}
|
|
385
|
+
// Patterns
|
|
386
|
+
if (graph.patterns.length > 0) {
|
|
387
|
+
jsonLd['pp:patterns'] = graph.patterns;
|
|
388
|
+
}
|
|
389
|
+
// Fingerprint
|
|
390
|
+
if (graph.fingerprint) {
|
|
391
|
+
jsonLd['pp:fingerprint'] = {
|
|
392
|
+
'pp:totalNodes': graph.fingerprint.totalNodes,
|
|
393
|
+
'pp:totalEdges': graph.fingerprint.totalEdges,
|
|
394
|
+
'pp:nodeTypes': graph.fingerprint.nodeTypeCounts,
|
|
395
|
+
'pp:edgeTypes': graph.fingerprint.edgeTypeCounts,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// Generator
|
|
399
|
+
if (graph.generator) {
|
|
400
|
+
jsonLd['pp:generator'] = {
|
|
401
|
+
'pp:name': graph.generator.name,
|
|
402
|
+
'pp:generatorCategory': graph.generator.type,
|
|
403
|
+
'pp:mathFunctions': graph.generator.mathFunctions,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// Semantics
|
|
407
|
+
if (graph.semantics) {
|
|
408
|
+
const semDoc = {
|
|
409
|
+
'pp:mood': graph.semantics.mood,
|
|
410
|
+
'pp:visualStyle': graph.semantics.visualStyle,
|
|
411
|
+
'pp:colorScheme': graph.semantics.colorScheme,
|
|
412
|
+
'schema:audience': graph.semantics.audience,
|
|
413
|
+
'pp:intent': graph.semantics.intent,
|
|
414
|
+
'pp:animationComplexity': graph.semantics.animationComplexity,
|
|
415
|
+
'pp:contentType': graph.semantics.contentType,
|
|
416
|
+
};
|
|
417
|
+
if (graph.semantics.platforms && graph.semantics.platforms.length > 0) {
|
|
418
|
+
semDoc['pp:platforms'] = graph.semantics.platforms;
|
|
419
|
+
}
|
|
420
|
+
jsonLd['pp:semantics'] = semDoc;
|
|
421
|
+
}
|
|
422
|
+
return jsonLd;
|
|
423
|
+
}
|
|
424
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
425
|
+
// Similarity
|
|
426
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
427
|
+
/** Find similar templates by comparing fingerprints. */
|
|
428
|
+
findSimilar(fingerprint, candidates) {
|
|
429
|
+
if (!fingerprint || !Array.isArray(candidates))
|
|
430
|
+
return [];
|
|
431
|
+
const results = [];
|
|
432
|
+
for (const candidate of candidates) {
|
|
433
|
+
if (candidate.templateId === fingerprint.templateId)
|
|
434
|
+
continue;
|
|
435
|
+
const score = this._computeSimilarity(fingerprint, candidate);
|
|
436
|
+
results.push({ templateId: candidate.templateId || 'unknown', score });
|
|
437
|
+
}
|
|
438
|
+
results.sort((a, b) => b.score - a.score);
|
|
439
|
+
return results;
|
|
440
|
+
}
|
|
441
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
442
|
+
// Static Validation
|
|
443
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
444
|
+
/** Validate a template definition against the DesignGraph schema. */
|
|
445
|
+
static validateTemplateSchema(template, vocab, semVocab) {
|
|
446
|
+
const v = vocab || PP_VOCABULARY;
|
|
447
|
+
const sv = semVocab || PP_SEMANTIC_VOCABULARY;
|
|
448
|
+
const errors = [];
|
|
449
|
+
const warnings = [];
|
|
450
|
+
if (!template) {
|
|
451
|
+
return { valid: false, errors: ['Template definition is null or undefined'], warnings };
|
|
452
|
+
}
|
|
453
|
+
// Required fields
|
|
454
|
+
if (!template.id || typeof template.id !== 'string') {
|
|
455
|
+
errors.push('Missing or invalid "id" (must be a non-empty string)');
|
|
456
|
+
}
|
|
457
|
+
if (!template.name || typeof template.name !== 'string') {
|
|
458
|
+
errors.push('Missing or invalid "name" (must be a non-empty string)');
|
|
459
|
+
}
|
|
460
|
+
if (!template.category || typeof template.category !== 'string') {
|
|
461
|
+
errors.push('Missing or invalid "category" (must be a non-empty string)');
|
|
462
|
+
}
|
|
463
|
+
if (!template.data || typeof template.data !== 'object') {
|
|
464
|
+
errors.push('Missing or invalid "data" (must be an object)');
|
|
465
|
+
return { valid: false, errors, warnings };
|
|
466
|
+
}
|
|
467
|
+
// Item type validation
|
|
468
|
+
const items = template.data.items;
|
|
469
|
+
if (Array.isArray(items)) {
|
|
470
|
+
for (let i = 0; i < items.length; i++) {
|
|
471
|
+
const item = items[i];
|
|
472
|
+
if (!item || typeof item !== 'object')
|
|
473
|
+
continue;
|
|
474
|
+
if (item.type && !ITEM_TYPE_MAP[item.type] && !v.types[item.type]) {
|
|
475
|
+
warnings.push(`data.items[${i}]: unknown type "${item.type}"`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Relation type validation
|
|
480
|
+
const relations = template.data.relations;
|
|
481
|
+
if (Array.isArray(relations)) {
|
|
482
|
+
for (let i = 0; i < relations.length; i++) {
|
|
483
|
+
const rel = relations[i];
|
|
484
|
+
if (!rel || typeof rel !== 'object')
|
|
485
|
+
continue;
|
|
486
|
+
if (rel.type && !RELATION_TYPE_MAP[rel.type] && !v.edges[rel.type]) {
|
|
487
|
+
warnings.push(`data.relations[${i}]: unknown relation type "${rel.type}"`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Semantics validation
|
|
492
|
+
if (template.semantics) {
|
|
493
|
+
const sem = template.semantics;
|
|
494
|
+
const arrayChecks = {
|
|
495
|
+
mood: sv.moods,
|
|
496
|
+
visualStyle: sv.visualStyles,
|
|
497
|
+
audience: sv.audiences,
|
|
498
|
+
};
|
|
499
|
+
for (const [field, allowed] of Object.entries(arrayChecks)) {
|
|
500
|
+
if (sem[field]) {
|
|
501
|
+
const vals = Array.isArray(sem[field]) ? sem[field] : [sem[field]];
|
|
502
|
+
for (const val of vals) {
|
|
503
|
+
if (!allowed.includes(val)) {
|
|
504
|
+
warnings.push(`semantics.${field}: unknown value "${val}"`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const stringChecks = {
|
|
510
|
+
colorScheme: sv.colorSchemes,
|
|
511
|
+
intent: sv.intents,
|
|
512
|
+
animationComplexity: sv.animationComplexities,
|
|
513
|
+
compositionStyle: sv.compositionStyles,
|
|
514
|
+
contentType: sv.contentTypes,
|
|
515
|
+
};
|
|
516
|
+
for (const [field, allowed] of Object.entries(stringChecks)) {
|
|
517
|
+
if (sem[field] && !allowed.includes(sem[field])) {
|
|
518
|
+
warnings.push(`semantics.${field}: unknown value "${sem[field]}"`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
523
|
+
}
|
|
524
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
525
|
+
// Private Helpers
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
527
|
+
/** Convert a template item definition into a graph node. */
|
|
528
|
+
_itemToNode(item) {
|
|
529
|
+
const itemId = item.id || item.name || null;
|
|
530
|
+
let type = this.getNodeType(item.type);
|
|
531
|
+
// Refine diagram shapes to specific subtypes
|
|
532
|
+
if (type === 'pp:DiagramShape' && item.shapeType) {
|
|
533
|
+
type = DIAGRAM_SHAPE_MAP[item.shapeType] || type;
|
|
534
|
+
}
|
|
535
|
+
// Refine connectors to specific subtypes
|
|
536
|
+
if (type === 'pp:Connector') {
|
|
537
|
+
type = CONNECTOR_STYLE_MAP[item.lineStyle || ''] || 'pp:SequenceConnector';
|
|
538
|
+
}
|
|
539
|
+
// Check for mask → MaskedItem
|
|
540
|
+
if (item.mask)
|
|
541
|
+
type = 'pp:MaskedItem';
|
|
542
|
+
// Refine path types based on closed property from template definition
|
|
543
|
+
let pathClosed = null;
|
|
544
|
+
let pathCurveType = null;
|
|
545
|
+
if (type === 'pp:Path') {
|
|
546
|
+
pathClosed = item.closed != null ? !!item.closed : null;
|
|
547
|
+
if (pathClosed === true)
|
|
548
|
+
type = 'pp:ClosedPath';
|
|
549
|
+
else if (pathClosed === false)
|
|
550
|
+
type = 'pp:OpenPath';
|
|
551
|
+
pathCurveType = this._detectCurveTypeFromDef(item);
|
|
552
|
+
}
|
|
553
|
+
const hasKeyframes = item.animationType === 'keyframe' && Array.isArray(item.keyframes);
|
|
554
|
+
const easings = this._extractEasings(item.keyframes);
|
|
555
|
+
const hasTrimPath = this._hasTrimPath(item);
|
|
556
|
+
const hasColorAnimation = this._hasColorAnimation(item.keyframes);
|
|
557
|
+
const properties = {
|
|
558
|
+
position: item.position || (item.x != null && item.y != null ? [item.x, item.y] : null),
|
|
559
|
+
hasKeyframes,
|
|
560
|
+
animationType: item.animationType || null,
|
|
561
|
+
keyframeCount: Array.isArray(item.keyframes) ? item.keyframes.length : 0,
|
|
562
|
+
easings,
|
|
563
|
+
trimPath: hasTrimPath,
|
|
564
|
+
blendMode: item.blendMode && item.blendMode !== 'normal' ? item.blendMode : null,
|
|
565
|
+
masked: !!item.mask,
|
|
566
|
+
maskType: item.mask ? (item.mask.type || null) : null,
|
|
567
|
+
hasColorAnimation,
|
|
568
|
+
closed: pathClosed !== null ? pathClosed : undefined,
|
|
569
|
+
curveType: pathCurveType || undefined,
|
|
570
|
+
segmentCount: item.segments ? (Array.isArray(item.segments) ? item.segments.length : 0) : undefined,
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
id: itemId,
|
|
574
|
+
type,
|
|
575
|
+
label: item.content || item.label || itemId || item.type || 'item',
|
|
576
|
+
properties,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/** Convert a template relation definition into a graph edge. */
|
|
580
|
+
_relationToEdge(rel) {
|
|
581
|
+
const relType = rel.type;
|
|
582
|
+
const edgeType = this.getEdgeType(relType);
|
|
583
|
+
if (!edgeType)
|
|
584
|
+
return null;
|
|
585
|
+
return {
|
|
586
|
+
source: rel.source || rel.from || null,
|
|
587
|
+
target: rel.target || rel.to || null,
|
|
588
|
+
type: edgeType,
|
|
589
|
+
params: rel.params || {},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/** Extract unique easing function names from a keyframes array. */
|
|
593
|
+
_extractEasings(keyframes) {
|
|
594
|
+
if (!Array.isArray(keyframes))
|
|
595
|
+
return [];
|
|
596
|
+
const easings = new Set();
|
|
597
|
+
for (const kf of keyframes) {
|
|
598
|
+
if (kf.easing) {
|
|
599
|
+
if (Array.isArray(kf.easing)) {
|
|
600
|
+
easings.add('customCubicBezier');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
easings.add(kf.easing);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (kf.propertyEasings) {
|
|
607
|
+
for (const val of Object.values(kf.propertyEasings)) {
|
|
608
|
+
if (Array.isArray(val)) {
|
|
609
|
+
easings.add('customCubicBezier');
|
|
610
|
+
}
|
|
611
|
+
else if (typeof val === 'string') {
|
|
612
|
+
easings.add(val);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return Array.from(easings);
|
|
618
|
+
}
|
|
619
|
+
/** Check if an item definition uses trim path properties. */
|
|
620
|
+
_hasTrimPath(item) {
|
|
621
|
+
if (item.trimStart != null || item.trimEnd != null || item.trimOffset != null)
|
|
622
|
+
return true;
|
|
623
|
+
if (Array.isArray(item.keyframes)) {
|
|
624
|
+
for (const kf of item.keyframes) {
|
|
625
|
+
if (kf.properties) {
|
|
626
|
+
const props = kf.properties;
|
|
627
|
+
if (props.trimStart != null || props.trimEnd != null || props.trimOffset != null) {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
/** Check if keyframes contain color property animations. */
|
|
636
|
+
_hasColorAnimation(keyframes) {
|
|
637
|
+
if (!Array.isArray(keyframes))
|
|
638
|
+
return false;
|
|
639
|
+
for (const kf of keyframes) {
|
|
640
|
+
if (kf.properties) {
|
|
641
|
+
const props = kf.properties;
|
|
642
|
+
if (props.fillColor || props.strokeColor || props.fontColor || props.color) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
/** Detect curve type from a template item definition. */
|
|
650
|
+
_detectCurveTypeFromDef(item) {
|
|
651
|
+
if (item.smooth)
|
|
652
|
+
return 'cubicBezier';
|
|
653
|
+
if (!item.segments || !Array.isArray(item.segments))
|
|
654
|
+
return null;
|
|
655
|
+
let hasComplex = false;
|
|
656
|
+
for (const seg of item.segments) {
|
|
657
|
+
if (Array.isArray(seg) && seg.length === 3 && Array.isArray(seg[0])) {
|
|
658
|
+
hasComplex = true;
|
|
659
|
+
}
|
|
660
|
+
else if (seg && typeof seg === 'object' && !Array.isArray(seg)) {
|
|
661
|
+
const s = seg;
|
|
662
|
+
if (s.handleIn || s.handleOut)
|
|
663
|
+
hasComplex = true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return hasComplex ? 'cubicBezier' : 'linear';
|
|
667
|
+
}
|
|
668
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
669
|
+
// Semantic Inference
|
|
670
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
671
|
+
/** Infer semantic metadata from template structure. */
|
|
672
|
+
_inferSemantics(template, graph) {
|
|
673
|
+
const sem = {};
|
|
674
|
+
const data = template.data || {};
|
|
675
|
+
const items = data.items || [];
|
|
676
|
+
const nodeTypes = new Set(graph.nodes.map(n => n.type));
|
|
677
|
+
const edgeTypes = new Set(graph.edges.map(e => e.type));
|
|
678
|
+
// --- animationComplexity ---
|
|
679
|
+
const hasKeyframes = graph.nodes.some(n => n.properties && n.properties.hasKeyframes);
|
|
680
|
+
const hasRelations = graph.edges.some(e => e.type !== 'pp:contains' && e.type !== 'pp:maskedBy');
|
|
681
|
+
const hasMasks = nodeTypes.has('pp:MaskedItem');
|
|
682
|
+
const hasRigging = nodeTypes.has('pp:Skeleton') || nodeTypes.has('pp:Bone');
|
|
683
|
+
const hasPrecomp = nodeTypes.has('pp:Precomp');
|
|
684
|
+
const hasOnLoad = typeof template.onLoad === 'function';
|
|
685
|
+
const hasSimpleAnim = items.some(i => i.animationType && i.animationType !== 'keyframe');
|
|
686
|
+
if (hasRigging || hasPrecomp || (hasOnLoad && hasKeyframes && hasRelations)) {
|
|
687
|
+
sem.animationComplexity = 'advanced';
|
|
688
|
+
}
|
|
689
|
+
else if (hasMasks && (hasRelations || hasKeyframes)) {
|
|
690
|
+
sem.animationComplexity = 'complex';
|
|
691
|
+
}
|
|
692
|
+
else if (hasKeyframes || (hasRelations && items.length > 1)) {
|
|
693
|
+
sem.animationComplexity = 'moderate';
|
|
694
|
+
}
|
|
695
|
+
else if (hasSimpleAnim || items.length > 0) {
|
|
696
|
+
sem.animationComplexity = 'simple';
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
sem.animationComplexity = 'static';
|
|
700
|
+
}
|
|
701
|
+
// --- contentType ---
|
|
702
|
+
const dominant = this._getDominantTypes(graph);
|
|
703
|
+
const hasDiagram = Array.from(nodeTypes).some(nt => this.isSubtypeOf(nt, 'pp:DiagramShape') || this.isSubtypeOf(nt, 'pp:Connector'));
|
|
704
|
+
if (hasDiagram) {
|
|
705
|
+
sem.contentType = 'diagram';
|
|
706
|
+
}
|
|
707
|
+
else if (nodeTypes.has('pp:MapRegion') || nodeTypes.has('pp:Marker')) {
|
|
708
|
+
sem.contentType = 'map';
|
|
709
|
+
}
|
|
710
|
+
else if (hasRigging) {
|
|
711
|
+
sem.contentType = 'character';
|
|
712
|
+
}
|
|
713
|
+
else if (dominant[0] === 'pp:Text' || dominant[0] === 'pp:LetterCollage') {
|
|
714
|
+
sem.contentType = 'text-focused';
|
|
715
|
+
}
|
|
716
|
+
else if (dominant[0] && dominant[0] !== 'pp:Text' && !nodeTypes.has('pp:Text')) {
|
|
717
|
+
sem.contentType = 'shape-focused';
|
|
718
|
+
}
|
|
719
|
+
else if (graph.generator && graph.nodes.length <= 1) {
|
|
720
|
+
sem.contentType = 'scene';
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
sem.contentType = 'mixed';
|
|
724
|
+
}
|
|
725
|
+
// --- platforms (from dimensions) ---
|
|
726
|
+
const dim = template.dimensions || { width: 0, height: 0 };
|
|
727
|
+
const w = dim.width || 0;
|
|
728
|
+
const h = dim.height || 0;
|
|
729
|
+
sem.platforms = [];
|
|
730
|
+
if (w === 1080 && h === 1920)
|
|
731
|
+
sem.platforms = ['instagram-story', 'tiktok'];
|
|
732
|
+
else if (w === 1080 && h === 1080)
|
|
733
|
+
sem.platforms = ['instagram-post'];
|
|
734
|
+
else if (w === 1920 && h === 1080)
|
|
735
|
+
sem.platforms = ['youtube-thumbnail', 'presentation'];
|
|
736
|
+
else if (w === 1200 && h === 628)
|
|
737
|
+
sem.platforms = ['linkedin', 'twitter'];
|
|
738
|
+
else if (w === 800 && h === 600)
|
|
739
|
+
sem.platforms = ['general'];
|
|
740
|
+
// --- Feature flags ---
|
|
741
|
+
sem.hasLetterCollage = nodeTypes.has('pp:LetterCollage');
|
|
742
|
+
sem.hasRigging = hasRigging;
|
|
743
|
+
sem.hasMasking = hasMasks;
|
|
744
|
+
// --- mood / audience / intent from category + tags ---
|
|
745
|
+
const categoryDefaults = {
|
|
746
|
+
'social-media': { mood: ['energetic', 'bold'], audience: ['creators', 'marketers'], intent: 'sell' },
|
|
747
|
+
'business': { mood: ['professional', 'elegant'], audience: ['business'], intent: 'inform' },
|
|
748
|
+
'education': { mood: ['calm', 'inspiring'], audience: ['students'], intent: 'teach' },
|
|
749
|
+
'creative': { mood: ['inspiring', 'elegant'], audience: ['creators'], intent: 'inspire' },
|
|
750
|
+
'tech': { mood: ['futuristic', 'bold'], audience: ['developers'], intent: 'demonstrate' },
|
|
751
|
+
'meme': { mood: ['playful', 'energetic'], audience: ['general'], intent: 'entertain' },
|
|
752
|
+
'global': { mood: ['warm', 'festive'], audience: ['multilingual', 'general'], intent: 'celebrate' },
|
|
753
|
+
'indigenous': { mood: ['warm', 'solemn'], audience: ['indigenous', 'general'], intent: 'celebrate' },
|
|
754
|
+
'seasonal': { mood: ['festive', 'warm'], audience: ['general'], intent: 'celebrate' },
|
|
755
|
+
'masking': { mood: ['dramatic', 'elegant'], audience: ['creators'], intent: 'demonstrate' },
|
|
756
|
+
'scenes': { mood: ['dramatic', 'inspiring'], audience: ['creators'], intent: 'demonstrate' },
|
|
757
|
+
'diagrams': { mood: ['professional', 'calm'], audience: ['business', 'students'], intent: 'inform' },
|
|
758
|
+
'maps': { mood: ['professional', 'calm'], audience: ['business'], intent: 'inform' },
|
|
759
|
+
'advanced': { mood: ['futuristic', 'bold'], audience: ['developers', 'creators'], intent: 'demonstrate' },
|
|
760
|
+
};
|
|
761
|
+
const catDefaults = categoryDefaults[template.category || ''] || { mood: ['calm'], audience: ['general'], intent: 'inform' };
|
|
762
|
+
sem.mood = catDefaults.mood;
|
|
763
|
+
sem.audience = catDefaults.audience;
|
|
764
|
+
sem.intent = catDefaults.intent;
|
|
765
|
+
// --- visualStyle heuristic ---
|
|
766
|
+
const styles = [];
|
|
767
|
+
if (nodeTypes.has('pp:LetterCollage'))
|
|
768
|
+
styles.push('letter-by-letter');
|
|
769
|
+
if (hasMasks)
|
|
770
|
+
styles.push('mask-reveal');
|
|
771
|
+
if (hasRigging)
|
|
772
|
+
styles.push('skeletal-animation');
|
|
773
|
+
if (graph.generator) {
|
|
774
|
+
const genName = graph.generator.name || '';
|
|
775
|
+
if (genName.includes('Circuit'))
|
|
776
|
+
styles.push('circuit-board');
|
|
777
|
+
else if (genName.includes('Bokeh') || genName.includes('GradientMesh'))
|
|
778
|
+
styles.push('gradient-mesh');
|
|
779
|
+
else if (genName.includes('Wave') || genName.includes('OrganicFlow'))
|
|
780
|
+
styles.push('wave-patterns');
|
|
781
|
+
else if (genName.includes('Geometric'))
|
|
782
|
+
styles.push('geometric');
|
|
783
|
+
else if (genName.includes('Wind') || genName.includes('Fluid'))
|
|
784
|
+
styles.push('particle-effects');
|
|
785
|
+
else
|
|
786
|
+
styles.push('radial-background');
|
|
787
|
+
}
|
|
788
|
+
if (graph.nodes.some(n => n.type === 'pp:Text' && n.properties && n.properties.hasKeyframes)) {
|
|
789
|
+
styles.push('text-reveal');
|
|
790
|
+
}
|
|
791
|
+
if (items.some(i => i.fontSize && i.fontSize >= 48))
|
|
792
|
+
styles.push('bold-typography');
|
|
793
|
+
sem.visualStyle = styles.length > 0 ? styles : ['flat-design'];
|
|
794
|
+
// --- colorScheme heuristic ---
|
|
795
|
+
sem.colorScheme = 'vibrant';
|
|
796
|
+
const tags = template.tags || [];
|
|
797
|
+
if (tags.includes('dark') || tags.includes('cyberpunk'))
|
|
798
|
+
sem.colorScheme = 'dark';
|
|
799
|
+
else if (tags.includes('minimal') || tags.includes('clean'))
|
|
800
|
+
sem.colorScheme = 'monochrome';
|
|
801
|
+
else if (tags.includes('neon') || tags.includes('glow'))
|
|
802
|
+
sem.colorScheme = 'neon';
|
|
803
|
+
else if (tags.includes('pastel') || tags.includes('soft'))
|
|
804
|
+
sem.colorScheme = 'pastel';
|
|
805
|
+
else if (tags.includes('warm') || tags.includes('sunset'))
|
|
806
|
+
sem.colorScheme = 'warm';
|
|
807
|
+
// --- compositionStyle heuristic ---
|
|
808
|
+
if (hasDiagram)
|
|
809
|
+
sem.compositionStyle = 'grid';
|
|
810
|
+
else if (graph.generator)
|
|
811
|
+
sem.compositionStyle = 'layered';
|
|
812
|
+
else if (graph.nodes.length <= 2)
|
|
813
|
+
sem.compositionStyle = 'centered';
|
|
814
|
+
else
|
|
815
|
+
sem.compositionStyle = 'centered';
|
|
816
|
+
return sem;
|
|
817
|
+
}
|
|
818
|
+
/** Merge explicit template.semantics over auto-inferred values. */
|
|
819
|
+
_mergeSemantics(template, graph) {
|
|
820
|
+
const inferred = this._inferSemantics(template, graph);
|
|
821
|
+
if (!template.semantics)
|
|
822
|
+
return inferred;
|
|
823
|
+
return { ...inferred, ...template.semantics };
|
|
824
|
+
}
|
|
825
|
+
/** Return top 3 node types by count from the graph. */
|
|
826
|
+
_getDominantTypes(graph) {
|
|
827
|
+
const counts = {};
|
|
828
|
+
for (const node of graph.nodes) {
|
|
829
|
+
counts[node.type] = (counts[node.type] || 0) + 1;
|
|
830
|
+
}
|
|
831
|
+
return Object.entries(counts)
|
|
832
|
+
.sort((a, b) => b[1] - a[1])
|
|
833
|
+
.slice(0, 3)
|
|
834
|
+
.map(e => e[0]);
|
|
835
|
+
}
|
|
836
|
+
/** Compute similarity score between two fingerprints. */
|
|
837
|
+
_computeSimilarity(fpA, fpB) {
|
|
838
|
+
// Structural weight allocation (0.60 total)
|
|
839
|
+
const W_NODE_TYPES = 0.15;
|
|
840
|
+
const W_EDGE_TYPES = 0.12;
|
|
841
|
+
const W_MATH_FUNCS = 0.12;
|
|
842
|
+
const W_PATTERNS = 0.12;
|
|
843
|
+
const W_SIZE = 0.05;
|
|
844
|
+
const W_GENERATOR = 0.04;
|
|
845
|
+
// Semantic weight allocation (0.40 total)
|
|
846
|
+
const W_MOOD = 0.08;
|
|
847
|
+
const W_VISUAL = 0.08;
|
|
848
|
+
const W_INTENT = 0.06;
|
|
849
|
+
const W_AUDIENCE = 0.05;
|
|
850
|
+
const W_COMPLEXITY = 0.05;
|
|
851
|
+
const W_CONTENT = 0.04;
|
|
852
|
+
const W_PLATFORMS = 0.04;
|
|
853
|
+
const nodeTypesSim = this._jaccardKeys(fpA.nodeTypeCounts, fpB.nodeTypeCounts);
|
|
854
|
+
const edgeTypesSim = this._jaccardKeys(fpA.edgeTypeCounts, fpB.edgeTypeCounts);
|
|
855
|
+
const mathSim = this._jaccardArray(fpA.mathFunctions, fpB.mathFunctions);
|
|
856
|
+
const patternSim = this._jaccardArray(fpA.patterns, fpB.patterns);
|
|
857
|
+
// Size similarity
|
|
858
|
+
const totalA = (fpA.totalNodes || 0) + (fpA.totalEdges || 0);
|
|
859
|
+
const totalB = (fpB.totalNodes || 0) + (fpB.totalEdges || 0);
|
|
860
|
+
const sizeSim = (totalA === 0 && totalB === 0) ? 1.0
|
|
861
|
+
: Math.min(totalA, totalB) / Math.max(totalA, totalB);
|
|
862
|
+
// Generator match
|
|
863
|
+
let genSim = 0;
|
|
864
|
+
if (fpA.generatorType && fpB.generatorType) {
|
|
865
|
+
genSim = fpA.generatorType === fpB.generatorType ? 1.0 : 0.5;
|
|
866
|
+
}
|
|
867
|
+
else if (!fpA.hasGenerator && !fpB.hasGenerator) {
|
|
868
|
+
genSim = 1.0;
|
|
869
|
+
}
|
|
870
|
+
// Semantic dimensions
|
|
871
|
+
const semA = fpA.semantics || null;
|
|
872
|
+
const semB = fpB.semantics || null;
|
|
873
|
+
let moodSim = 0, visualSim = 0, intentSim = 0, audienceSim = 0;
|
|
874
|
+
let complexitySim = 0, contentSim = 0, platformsSim = 0;
|
|
875
|
+
if (semA && semB) {
|
|
876
|
+
moodSim = this._jaccardArray(semA.mood, semB.mood);
|
|
877
|
+
visualSim = this._jaccardArray(semA.visualStyle, semB.visualStyle);
|
|
878
|
+
audienceSim = this._jaccardArray(semA.audience, semB.audience);
|
|
879
|
+
platformsSim = this._jaccardArray(semA.platforms, semB.platforms);
|
|
880
|
+
intentSim = (semA.intent && semA.intent === semB.intent) ? 1.0 : 0.0;
|
|
881
|
+
contentSim = (semA.contentType && semA.contentType === semB.contentType) ? 1.0 : 0.0;
|
|
882
|
+
const complexityOrder = ['static', 'simple', 'moderate', 'complex', 'advanced'];
|
|
883
|
+
const idxA = complexityOrder.indexOf(semA.animationComplexity || '');
|
|
884
|
+
const idxB = complexityOrder.indexOf(semB.animationComplexity || '');
|
|
885
|
+
if (idxA >= 0 && idxB >= 0) {
|
|
886
|
+
const diff = Math.abs(idxA - idxB);
|
|
887
|
+
complexitySim = diff === 0 ? 1.0 : diff === 1 ? 0.5 : 0.0;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else if (!semA && !semB) {
|
|
891
|
+
moodSim = 1.0;
|
|
892
|
+
visualSim = 1.0;
|
|
893
|
+
intentSim = 1.0;
|
|
894
|
+
audienceSim = 1.0;
|
|
895
|
+
complexitySim = 1.0;
|
|
896
|
+
contentSim = 1.0;
|
|
897
|
+
platformsSim = 1.0;
|
|
898
|
+
}
|
|
899
|
+
return (W_NODE_TYPES * nodeTypesSim +
|
|
900
|
+
W_EDGE_TYPES * edgeTypesSim +
|
|
901
|
+
W_MATH_FUNCS * mathSim +
|
|
902
|
+
W_PATTERNS * patternSim +
|
|
903
|
+
W_SIZE * sizeSim +
|
|
904
|
+
W_GENERATOR * genSim +
|
|
905
|
+
W_MOOD * moodSim +
|
|
906
|
+
W_VISUAL * visualSim +
|
|
907
|
+
W_INTENT * intentSim +
|
|
908
|
+
W_AUDIENCE * audienceSim +
|
|
909
|
+
W_COMPLEXITY * complexitySim +
|
|
910
|
+
W_CONTENT * contentSim +
|
|
911
|
+
W_PLATFORMS * platformsSim);
|
|
912
|
+
}
|
|
913
|
+
/** Jaccard similarity between the keys of two count objects. */
|
|
914
|
+
_jaccardKeys(objA, objB) {
|
|
915
|
+
const a = objA || {};
|
|
916
|
+
const b = objB || {};
|
|
917
|
+
const keysA = new Set(Object.keys(a));
|
|
918
|
+
const keysB = new Set(Object.keys(b));
|
|
919
|
+
const union = new Set([...keysA, ...keysB]);
|
|
920
|
+
if (union.size === 0)
|
|
921
|
+
return 1.0;
|
|
922
|
+
let intersection = 0;
|
|
923
|
+
for (const key of union) {
|
|
924
|
+
if (keysA.has(key) && keysB.has(key))
|
|
925
|
+
intersection++;
|
|
926
|
+
}
|
|
927
|
+
return intersection / union.size;
|
|
928
|
+
}
|
|
929
|
+
/** Jaccard similarity between two string arrays. */
|
|
930
|
+
_jaccardArray(arrA, arrB) {
|
|
931
|
+
const a = new Set(arrA || []);
|
|
932
|
+
const b = new Set(arrB || []);
|
|
933
|
+
const union = new Set([...a, ...b]);
|
|
934
|
+
if (union.size === 0)
|
|
935
|
+
return 1.0;
|
|
936
|
+
let intersection = 0;
|
|
937
|
+
for (const val of union) {
|
|
938
|
+
if (a.has(val) && b.has(val))
|
|
939
|
+
intersection++;
|
|
940
|
+
}
|
|
941
|
+
return intersection / union.size;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
//# sourceMappingURL=design-graph.js.map
|