@viewscript/renderer 0.1.0-202605140639
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/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Translator for P/Q Dimension Bridge (Phase 7: Task 20)
|
|
3
|
+
*
|
|
4
|
+
* This module converts raw solver output (VarId -> Rational maps) into
|
|
5
|
+
* entity-level coordinate descriptions suitable for LLM comprehension.
|
|
6
|
+
*
|
|
7
|
+
* ## Purpose
|
|
8
|
+
*
|
|
9
|
+
* The P-dimension solver produces solutions in the form:
|
|
10
|
+
* { "1:x": "100/1", "1:y": "200/1", "2:x": "300/1", ... }
|
|
11
|
+
*
|
|
12
|
+
* This is not LLM-friendly. The SemanticTranslator converts this to:
|
|
13
|
+
* {
|
|
14
|
+
* entities: [
|
|
15
|
+
* { entityId: 1, coordinates: { x: 100, y: 200 }, description: "Point at (100, 200)" },
|
|
16
|
+
* { entityId: 2, coordinates: { x: 300, y: 150 }, description: "Point at (300, 150)" }
|
|
17
|
+
* ],
|
|
18
|
+
* relationships: [
|
|
19
|
+
* { type: "distance", from: 1, to: 2, value: "~212 units" }
|
|
20
|
+
* ]
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* ## Axiom 2: Ouroboros Binding
|
|
24
|
+
*
|
|
25
|
+
* This module sits at the P/Q dimension boundary. It translates deterministic
|
|
26
|
+
* solver output (P) into natural language suitable for non-deterministic
|
|
27
|
+
* oracles (Q/LLM).
|
|
28
|
+
*
|
|
29
|
+
* ## Usage
|
|
30
|
+
*
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const translator = new SemanticTranslator(entityRegistry);
|
|
33
|
+
* const semanticSolution = translator.translateSolution(rawSolution);
|
|
34
|
+
* const diff = translator.compareSolutions(solution1, solution2);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { EntityId, Rational } from '../ast/types';
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Types for Raw Solver Output
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Raw solution from solver: "entity_id:component" -> "numerator/denominator"
|
|
46
|
+
*/
|
|
47
|
+
export type RawSolution = Map<string, string>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parsed VarId key.
|
|
51
|
+
*/
|
|
52
|
+
export interface ParsedVarId {
|
|
53
|
+
entityId: EntityId;
|
|
54
|
+
component: VectorComponent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Vector components matching Rust VectorComponent enum.
|
|
59
|
+
*/
|
|
60
|
+
export type VectorComponent =
|
|
61
|
+
| 'x'
|
|
62
|
+
| 'y'
|
|
63
|
+
| 'z'
|
|
64
|
+
| 't'
|
|
65
|
+
| 'value'
|
|
66
|
+
| 'r'
|
|
67
|
+
| 'g'
|
|
68
|
+
| 'b'
|
|
69
|
+
| 'alpha'
|
|
70
|
+
| 'position';
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Entity Metadata Registry
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Entity type classification for semantic descriptions.
|
|
78
|
+
*/
|
|
79
|
+
export type EntityType =
|
|
80
|
+
| 'point'
|
|
81
|
+
| 'control_point'
|
|
82
|
+
| 'rect'
|
|
83
|
+
| 'circle'
|
|
84
|
+
| 'text'
|
|
85
|
+
| 'color_stop'
|
|
86
|
+
| 'scalar'
|
|
87
|
+
| 'unknown';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Metadata about an entity (provided by the constraint graph).
|
|
91
|
+
*/
|
|
92
|
+
export interface EntityMetadata {
|
|
93
|
+
entityId: EntityId;
|
|
94
|
+
type: EntityType;
|
|
95
|
+
name?: string;
|
|
96
|
+
parentId?: EntityId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Registry of entity metadata for semantic translation.
|
|
101
|
+
*/
|
|
102
|
+
export interface EntityRegistry {
|
|
103
|
+
get(entityId: EntityId): EntityMetadata | undefined;
|
|
104
|
+
getAll(): EntityMetadata[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Semantic Output Types
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Coordinates for a spatial entity.
|
|
113
|
+
*/
|
|
114
|
+
export interface EntityCoordinates {
|
|
115
|
+
x?: number;
|
|
116
|
+
y?: number;
|
|
117
|
+
z?: number;
|
|
118
|
+
t?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Color values for a color stop entity.
|
|
123
|
+
*/
|
|
124
|
+
export interface EntityColor {
|
|
125
|
+
r?: number;
|
|
126
|
+
g?: number;
|
|
127
|
+
b?: number;
|
|
128
|
+
alpha?: number;
|
|
129
|
+
position?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scalar value for value-only entities.
|
|
134
|
+
*/
|
|
135
|
+
export interface EntityScalar {
|
|
136
|
+
value?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A semantic entity description.
|
|
141
|
+
*/
|
|
142
|
+
export interface SemanticEntity {
|
|
143
|
+
entityId: EntityId;
|
|
144
|
+
type: EntityType;
|
|
145
|
+
name?: string;
|
|
146
|
+
|
|
147
|
+
/** Spatial coordinates (for points, rects, etc.) */
|
|
148
|
+
coordinates?: EntityCoordinates;
|
|
149
|
+
|
|
150
|
+
/** Color values (for color stops) */
|
|
151
|
+
color?: EntityColor;
|
|
152
|
+
|
|
153
|
+
/** Scalar value (for radius, angle, etc.) */
|
|
154
|
+
scalar?: EntityScalar;
|
|
155
|
+
|
|
156
|
+
/** Human-readable description for LLM */
|
|
157
|
+
description: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Relationship between entities derived from geometry.
|
|
162
|
+
*/
|
|
163
|
+
export interface SemanticRelationship {
|
|
164
|
+
type: 'distance' | 'alignment' | 'containment' | 'collinear';
|
|
165
|
+
fromEntityId: EntityId;
|
|
166
|
+
toEntityId: EntityId;
|
|
167
|
+
description: string;
|
|
168
|
+
value?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Complete semantic solution.
|
|
173
|
+
*/
|
|
174
|
+
export interface SemanticSolution {
|
|
175
|
+
/** Unique identifier for this solution (index in MultipleSolutions) */
|
|
176
|
+
solutionIndex: number;
|
|
177
|
+
|
|
178
|
+
/** All entities with resolved coordinates */
|
|
179
|
+
entities: SemanticEntity[];
|
|
180
|
+
|
|
181
|
+
/** Derived relationships between entities */
|
|
182
|
+
relationships: SemanticRelationship[];
|
|
183
|
+
|
|
184
|
+
/** High-level summary for LLM consumption */
|
|
185
|
+
summary: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Difference between two solutions.
|
|
190
|
+
*/
|
|
191
|
+
export interface SolutionDiff {
|
|
192
|
+
/** Entities that differ between solutions */
|
|
193
|
+
differingEntities: EntityDiff[];
|
|
194
|
+
|
|
195
|
+
/** Human-readable diff summary */
|
|
196
|
+
summary: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface EntityDiff {
|
|
200
|
+
entityId: EntityId;
|
|
201
|
+
name?: string;
|
|
202
|
+
solution1: SemanticEntity;
|
|
203
|
+
solution2: SemanticEntity;
|
|
204
|
+
description: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// SemanticTranslator Implementation
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Translates raw solver output to LLM-friendly semantic descriptions.
|
|
213
|
+
*/
|
|
214
|
+
export class SemanticTranslator {
|
|
215
|
+
constructor(private readonly registry: EntityRegistry) {}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse a raw VarId key (e.g., "5:x") into its components.
|
|
219
|
+
*/
|
|
220
|
+
parseVarId(key: string): ParsedVarId | null {
|
|
221
|
+
const [entityStr, component] = key.split(':');
|
|
222
|
+
if (!entityStr || !component) return null;
|
|
223
|
+
|
|
224
|
+
const entityId = parseInt(entityStr, 10);
|
|
225
|
+
if (isNaN(entityId)) return null;
|
|
226
|
+
|
|
227
|
+
const validComponents: VectorComponent[] = [
|
|
228
|
+
'x', 'y', 'z', 't', 'value', 'r', 'g', 'b', 'alpha', 'position',
|
|
229
|
+
];
|
|
230
|
+
if (!validComponents.includes(component as VectorComponent)) return null;
|
|
231
|
+
|
|
232
|
+
return { entityId, component: component as VectorComponent };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse a rational string (e.g., "100/1" or "100") to a number.
|
|
237
|
+
*
|
|
238
|
+
* WARNING: This converts P-dimension exact rationals to Q-dimension floats.
|
|
239
|
+
* Only use for display/LLM consumption, NEVER for constraint solving.
|
|
240
|
+
*/
|
|
241
|
+
parseRationalToFloat(value: string): number {
|
|
242
|
+
const parts = value.split('/');
|
|
243
|
+
if (parts.length === 2) {
|
|
244
|
+
const num = parseFloat(parts[0]);
|
|
245
|
+
const denom = parseFloat(parts[1]);
|
|
246
|
+
if (denom === 0) return NaN;
|
|
247
|
+
return num / denom;
|
|
248
|
+
}
|
|
249
|
+
return parseFloat(value);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Translate a raw solution to a semantic solution.
|
|
254
|
+
*/
|
|
255
|
+
translateSolution(rawSolution: RawSolution, solutionIndex: number = 0): SemanticSolution {
|
|
256
|
+
// Group by entity
|
|
257
|
+
const entityMap = new Map<EntityId, Map<VectorComponent, number>>();
|
|
258
|
+
|
|
259
|
+
for (const [key, value] of rawSolution) {
|
|
260
|
+
const parsed = this.parseVarId(key);
|
|
261
|
+
if (!parsed) continue;
|
|
262
|
+
|
|
263
|
+
let components = entityMap.get(parsed.entityId);
|
|
264
|
+
if (!components) {
|
|
265
|
+
components = new Map();
|
|
266
|
+
entityMap.set(parsed.entityId, components);
|
|
267
|
+
}
|
|
268
|
+
components.set(parsed.component, this.parseRationalToFloat(value));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Build semantic entities
|
|
272
|
+
const entities: SemanticEntity[] = [];
|
|
273
|
+
for (const [entityId, components] of entityMap) {
|
|
274
|
+
const metadata = this.registry.get(entityId);
|
|
275
|
+
const entity = this.buildSemanticEntity(
|
|
276
|
+
entityId,
|
|
277
|
+
metadata?.type ?? 'unknown',
|
|
278
|
+
metadata?.name,
|
|
279
|
+
components,
|
|
280
|
+
);
|
|
281
|
+
entities.push(entity);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Sort by entity ID for consistent output
|
|
285
|
+
entities.sort((a, b) => a.entityId - b.entityId);
|
|
286
|
+
|
|
287
|
+
// Derive relationships
|
|
288
|
+
const relationships = this.deriveRelationships(entities);
|
|
289
|
+
|
|
290
|
+
// Generate summary
|
|
291
|
+
const summary = this.generateSummary(entities, relationships);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
solutionIndex,
|
|
295
|
+
entities,
|
|
296
|
+
relationships,
|
|
297
|
+
summary,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Build a semantic entity from raw components.
|
|
303
|
+
*/
|
|
304
|
+
private buildSemanticEntity(
|
|
305
|
+
entityId: EntityId,
|
|
306
|
+
type: EntityType,
|
|
307
|
+
name: string | undefined,
|
|
308
|
+
components: Map<VectorComponent, number>,
|
|
309
|
+
): SemanticEntity {
|
|
310
|
+
const entity: SemanticEntity = {
|
|
311
|
+
entityId,
|
|
312
|
+
type,
|
|
313
|
+
name,
|
|
314
|
+
description: '',
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Build coordinates/color/scalar based on entity type
|
|
318
|
+
if (type === 'color_stop') {
|
|
319
|
+
entity.color = {
|
|
320
|
+
r: components.get('r'),
|
|
321
|
+
g: components.get('g'),
|
|
322
|
+
b: components.get('b'),
|
|
323
|
+
alpha: components.get('alpha'),
|
|
324
|
+
position: components.get('position'),
|
|
325
|
+
};
|
|
326
|
+
entity.description = this.describeColorStop(entity.color, name);
|
|
327
|
+
} else if (type === 'scalar') {
|
|
328
|
+
entity.scalar = {
|
|
329
|
+
value: components.get('value'),
|
|
330
|
+
};
|
|
331
|
+
entity.description = this.describeScalar(entity.scalar, name);
|
|
332
|
+
} else {
|
|
333
|
+
// Spatial entity
|
|
334
|
+
entity.coordinates = {
|
|
335
|
+
x: components.get('x'),
|
|
336
|
+
y: components.get('y'),
|
|
337
|
+
z: components.get('z'),
|
|
338
|
+
t: components.get('t'),
|
|
339
|
+
};
|
|
340
|
+
entity.description = this.describeSpatialEntity(type, entity.coordinates, name);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return entity;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate description for a spatial entity.
|
|
348
|
+
*/
|
|
349
|
+
private describeSpatialEntity(
|
|
350
|
+
type: EntityType,
|
|
351
|
+
coords: EntityCoordinates,
|
|
352
|
+
name?: string,
|
|
353
|
+
): string {
|
|
354
|
+
const nameStr = name ? `"${name}" ` : '';
|
|
355
|
+
const x = coords.x !== undefined ? this.formatCoord(coords.x) : '?';
|
|
356
|
+
const y = coords.y !== undefined ? this.formatCoord(coords.y) : '?';
|
|
357
|
+
|
|
358
|
+
switch (type) {
|
|
359
|
+
case 'point':
|
|
360
|
+
case 'control_point':
|
|
361
|
+
return `${nameStr}${type === 'control_point' ? 'Control point' : 'Point'} at (${x}, ${y})`;
|
|
362
|
+
case 'rect':
|
|
363
|
+
return `${nameStr}Rectangle at (${x}, ${y})`;
|
|
364
|
+
case 'circle':
|
|
365
|
+
return `${nameStr}Circle centered at (${x}, ${y})`;
|
|
366
|
+
case 'text':
|
|
367
|
+
return `${nameStr}Text at (${x}, ${y})`;
|
|
368
|
+
default:
|
|
369
|
+
return `${nameStr}Entity ${type} at (${x}, ${y})`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Generate description for a color stop.
|
|
375
|
+
*/
|
|
376
|
+
private describeColorStop(color: EntityColor, name?: string): string {
|
|
377
|
+
const nameStr = name ? `"${name}" ` : '';
|
|
378
|
+
const r = color.r !== undefined ? Math.round(color.r) : '?';
|
|
379
|
+
const g = color.g !== undefined ? Math.round(color.g) : '?';
|
|
380
|
+
const b = color.b !== undefined ? Math.round(color.b) : '?';
|
|
381
|
+
const pos = color.position !== undefined ? `${(color.position * 100).toFixed(0)}%` : '?';
|
|
382
|
+
|
|
383
|
+
return `${nameStr}Color stop at ${pos}: rgb(${r}, ${g}, ${b})`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Generate description for a scalar entity.
|
|
388
|
+
*/
|
|
389
|
+
private describeScalar(scalar: EntityScalar, name?: string): string {
|
|
390
|
+
const nameStr = name ? `"${name}"` : 'Scalar';
|
|
391
|
+
const val = scalar.value !== undefined ? this.formatCoord(scalar.value) : '?';
|
|
392
|
+
|
|
393
|
+
return `${nameStr} = ${val}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Format a coordinate value for display.
|
|
398
|
+
*/
|
|
399
|
+
private formatCoord(value: number): string {
|
|
400
|
+
// Round to reasonable precision for display
|
|
401
|
+
if (Number.isInteger(value)) {
|
|
402
|
+
return value.toString();
|
|
403
|
+
}
|
|
404
|
+
return value.toFixed(2).replace(/\.?0+$/, '');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Derive geometric relationships between entities.
|
|
409
|
+
*/
|
|
410
|
+
private deriveRelationships(entities: SemanticEntity[]): SemanticRelationship[] {
|
|
411
|
+
const relationships: SemanticRelationship[] = [];
|
|
412
|
+
|
|
413
|
+
// Find horizontally aligned entities (same Y)
|
|
414
|
+
const byY = new Map<number, SemanticEntity[]>();
|
|
415
|
+
for (const entity of entities) {
|
|
416
|
+
if (entity.coordinates?.y !== undefined) {
|
|
417
|
+
const y = Math.round(entity.coordinates.y);
|
|
418
|
+
let list = byY.get(y);
|
|
419
|
+
if (!list) {
|
|
420
|
+
list = [];
|
|
421
|
+
byY.set(y, list);
|
|
422
|
+
}
|
|
423
|
+
list.push(entity);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const [y, aligned] of byY) {
|
|
428
|
+
if (aligned.length >= 2) {
|
|
429
|
+
const ids = aligned.map(e => e.entityId).join(', ');
|
|
430
|
+
relationships.push({
|
|
431
|
+
type: 'alignment',
|
|
432
|
+
fromEntityId: aligned[0].entityId,
|
|
433
|
+
toEntityId: aligned[aligned.length - 1].entityId,
|
|
434
|
+
description: `Entities ${ids} are horizontally aligned at y=${y}`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Find vertically aligned entities (same X)
|
|
440
|
+
const byX = new Map<number, SemanticEntity[]>();
|
|
441
|
+
for (const entity of entities) {
|
|
442
|
+
if (entity.coordinates?.x !== undefined) {
|
|
443
|
+
const x = Math.round(entity.coordinates.x);
|
|
444
|
+
let list = byX.get(x);
|
|
445
|
+
if (!list) {
|
|
446
|
+
list = [];
|
|
447
|
+
byX.set(x, list);
|
|
448
|
+
}
|
|
449
|
+
list.push(entity);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const [x, aligned] of byX) {
|
|
454
|
+
if (aligned.length >= 2) {
|
|
455
|
+
const ids = aligned.map(e => e.entityId).join(', ');
|
|
456
|
+
relationships.push({
|
|
457
|
+
type: 'alignment',
|
|
458
|
+
fromEntityId: aligned[0].entityId,
|
|
459
|
+
toEntityId: aligned[aligned.length - 1].entityId,
|
|
460
|
+
description: `Entities ${ids} are vertically aligned at x=${x}`,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return relationships;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Generate a high-level summary of the solution.
|
|
470
|
+
*/
|
|
471
|
+
private generateSummary(
|
|
472
|
+
entities: SemanticEntity[],
|
|
473
|
+
relationships: SemanticRelationship[],
|
|
474
|
+
): string {
|
|
475
|
+
const entityCounts = new Map<EntityType, number>();
|
|
476
|
+
for (const entity of entities) {
|
|
477
|
+
entityCounts.set(entity.type, (entityCounts.get(entity.type) ?? 0) + 1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const parts: string[] = [];
|
|
481
|
+
for (const [type, count] of entityCounts) {
|
|
482
|
+
parts.push(`${count} ${type}${count > 1 ? 's' : ''}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let summary = `Solution with ${entities.length} entities: ${parts.join(', ')}.`;
|
|
486
|
+
|
|
487
|
+
if (relationships.length > 0) {
|
|
488
|
+
summary += ` ${relationships.length} geometric relationship${relationships.length > 1 ? 's' : ''} detected.`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return summary;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Compare two solutions and generate a diff.
|
|
496
|
+
*/
|
|
497
|
+
compareSolutions(
|
|
498
|
+
solution1: SemanticSolution,
|
|
499
|
+
solution2: SemanticSolution,
|
|
500
|
+
): SolutionDiff {
|
|
501
|
+
const diffs: EntityDiff[] = [];
|
|
502
|
+
|
|
503
|
+
const entities1 = new Map(solution1.entities.map(e => [e.entityId, e]));
|
|
504
|
+
const entities2 = new Map(solution2.entities.map(e => [e.entityId, e]));
|
|
505
|
+
|
|
506
|
+
// Find entities that differ
|
|
507
|
+
for (const [entityId, e1] of entities1) {
|
|
508
|
+
const e2 = entities2.get(entityId);
|
|
509
|
+
if (!e2) continue;
|
|
510
|
+
|
|
511
|
+
if (!this.entitiesEqual(e1, e2)) {
|
|
512
|
+
diffs.push({
|
|
513
|
+
entityId,
|
|
514
|
+
name: e1.name ?? e2.name,
|
|
515
|
+
solution1: e1,
|
|
516
|
+
solution2: e2,
|
|
517
|
+
description: this.describeDiff(e1, e2),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const summary = diffs.length === 0
|
|
523
|
+
? 'Solutions are identical'
|
|
524
|
+
: `${diffs.length} entities differ: ${diffs.map(d => d.entityId).join(', ')}`;
|
|
525
|
+
|
|
526
|
+
return { differingEntities: diffs, summary };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Check if two semantic entities are equal.
|
|
531
|
+
*/
|
|
532
|
+
private entitiesEqual(e1: SemanticEntity, e2: SemanticEntity): boolean {
|
|
533
|
+
const tolerance = 1e-6;
|
|
534
|
+
|
|
535
|
+
if (e1.coordinates && e2.coordinates) {
|
|
536
|
+
for (const key of ['x', 'y', 'z', 't'] as const) {
|
|
537
|
+
const v1 = e1.coordinates[key];
|
|
538
|
+
const v2 = e2.coordinates[key];
|
|
539
|
+
if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (e1.color && e2.color) {
|
|
546
|
+
for (const key of ['r', 'g', 'b', 'alpha', 'position'] as const) {
|
|
547
|
+
const v1 = e1.color[key];
|
|
548
|
+
const v2 = e2.color[key];
|
|
549
|
+
if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (e1.scalar && e2.scalar) {
|
|
556
|
+
const v1 = e1.scalar.value;
|
|
557
|
+
const v2 = e2.scalar.value;
|
|
558
|
+
if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Describe the difference between two entities.
|
|
568
|
+
*/
|
|
569
|
+
private describeDiff(e1: SemanticEntity, e2: SemanticEntity): string {
|
|
570
|
+
if (e1.coordinates && e2.coordinates) {
|
|
571
|
+
const changes: string[] = [];
|
|
572
|
+
if (e1.coordinates.x !== e2.coordinates.x) {
|
|
573
|
+
changes.push(`x: ${this.formatCoord(e1.coordinates.x ?? 0)} -> ${this.formatCoord(e2.coordinates.x ?? 0)}`);
|
|
574
|
+
}
|
|
575
|
+
if (e1.coordinates.y !== e2.coordinates.y) {
|
|
576
|
+
changes.push(`y: ${this.formatCoord(e1.coordinates.y ?? 0)} -> ${this.formatCoord(e2.coordinates.y ?? 0)}`);
|
|
577
|
+
}
|
|
578
|
+
return `Position changes: ${changes.join(', ')}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return 'Values differ';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Translate multiple solutions and generate comparative descriptions.
|
|
586
|
+
*/
|
|
587
|
+
translateMultipleSolutions(rawSolutions: RawSolution[]): {
|
|
588
|
+
solutions: SemanticSolution[];
|
|
589
|
+
comparison: string;
|
|
590
|
+
} {
|
|
591
|
+
const solutions = rawSolutions.map((raw, idx) => this.translateSolution(raw, idx));
|
|
592
|
+
|
|
593
|
+
if (solutions.length < 2) {
|
|
594
|
+
return {
|
|
595
|
+
solutions,
|
|
596
|
+
comparison: 'Only one solution available.',
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Generate pairwise comparison for first two solutions
|
|
601
|
+
const diff = this.compareSolutions(solutions[0], solutions[1]);
|
|
602
|
+
|
|
603
|
+
let comparison = `${solutions.length} solutions found.\n\n`;
|
|
604
|
+
comparison += `Solution 0 vs Solution 1:\n${diff.summary}\n\n`;
|
|
605
|
+
|
|
606
|
+
for (const entityDiff of diff.differingEntities) {
|
|
607
|
+
comparison += ` Entity ${entityDiff.entityId}: ${entityDiff.description}\n`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { solutions, comparison };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// =============================================================================
|
|
615
|
+
// Utility: Create Entity Registry from Array
|
|
616
|
+
// =============================================================================
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Create an EntityRegistry from an array of metadata.
|
|
620
|
+
*/
|
|
621
|
+
export function createEntityRegistry(entities: EntityMetadata[]): EntityRegistry {
|
|
622
|
+
const map = new Map(entities.map(e => [e.entityId, e]));
|
|
623
|
+
return {
|
|
624
|
+
get: (id: EntityId) => map.get(id),
|
|
625
|
+
getAll: () => entities,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Create an empty EntityRegistry (all entities will be "unknown" type).
|
|
631
|
+
*/
|
|
632
|
+
export function createEmptyRegistry(): EntityRegistry {
|
|
633
|
+
return {
|
|
634
|
+
get: () => undefined,
|
|
635
|
+
getAll: () => [],
|
|
636
|
+
};
|
|
637
|
+
}
|