@pinepaper.studio/mcp-server 1.5.1 → 1.5.3

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.
Files changed (58) hide show
  1. package/dist/agent/types.d.ts +6 -6
  2. package/dist/browser/puppeteer-controller.d.ts +5 -1
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +11 -3
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +80 -106
  8. package/dist/index.js.map +1 -1
  9. package/dist/ontology/design-graph.d.ts +85 -0
  10. package/dist/ontology/design-graph.d.ts.map +1 -0
  11. package/dist/ontology/design-graph.js +944 -0
  12. package/dist/ontology/design-graph.js.map +1 -0
  13. package/dist/ontology/index.d.ts +11 -0
  14. package/dist/ontology/index.d.ts.map +1 -0
  15. package/dist/ontology/index.js +14 -0
  16. package/dist/ontology/index.js.map +1 -0
  17. package/dist/ontology/semantic-vocabulary.d.ts +12 -0
  18. package/dist/ontology/semantic-vocabulary.d.ts.map +1 -0
  19. package/dist/ontology/semantic-vocabulary.js +26 -0
  20. package/dist/ontology/semantic-vocabulary.js.map +1 -0
  21. package/dist/ontology/types.d.ts +255 -0
  22. package/dist/ontology/types.d.ts.map +1 -0
  23. package/dist/ontology/types.js +10 -0
  24. package/dist/ontology/types.js.map +1 -0
  25. package/dist/ontology/validator.d.ts +36 -0
  26. package/dist/ontology/validator.d.ts.map +1 -0
  27. package/dist/ontology/validator.js +374 -0
  28. package/dist/ontology/validator.js.map +1 -0
  29. package/dist/ontology/vocabulary.d.ts +34 -0
  30. package/dist/ontology/vocabulary.d.ts.map +1 -0
  31. package/dist/ontology/vocabulary.js +395 -0
  32. package/dist/ontology/vocabulary.js.map +1 -0
  33. package/dist/tools/compact-descriptions.d.ts +2 -2
  34. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  35. package/dist/tools/compact-descriptions.js +45 -233
  36. package/dist/tools/compact-descriptions.js.map +1 -1
  37. package/dist/tools/definitions.d.ts +1 -1
  38. package/dist/tools/definitions.d.ts.map +1 -1
  39. package/dist/tools/definitions.js +638 -31
  40. package/dist/tools/definitions.js.map +1 -1
  41. package/dist/tools/handlers.d.ts.map +1 -1
  42. package/dist/tools/handlers.js +290 -145
  43. package/dist/tools/handlers.js.map +1 -1
  44. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  45. package/dist/tools/minimal-descriptions.js +22 -2
  46. package/dist/tools/minimal-descriptions.js.map +1 -1
  47. package/dist/tools/toolkits.d.ts.map +1 -1
  48. package/dist/tools/toolkits.js +31 -1
  49. package/dist/tools/toolkits.js.map +1 -1
  50. package/dist/types/code-generator.d.ts +14 -2
  51. package/dist/types/code-generator.d.ts.map +1 -1
  52. package/dist/types/code-generator.js +635 -6
  53. package/dist/types/code-generator.js.map +1 -1
  54. package/dist/types/schemas.d.ts +387 -82
  55. package/dist/types/schemas.d.ts.map +1 -1
  56. package/dist/types/schemas.js +136 -2
  57. package/dist/types/schemas.js.map +1 -1
  58. 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