cyclecad 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +96 -0
- package/app/js/modules/auto-assembly.js +1146 -0
- package/app/js/modules/digital-twin.js +1225 -0
- package/app/js/modules/engineering-notebook.js +1505 -0
- package/app/js/modules/machine-control.js +1270 -0
- package/app/js/modules/parametric-from-example.js +900 -0
- package/app/js/modules/smart-assembly.js +1667 -0
- package/package.json +1 -1
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Parametric from Example - Infer parametric relationships from design variants
|
|
3
|
+
* Analyzes 2-5 design variants to automatically extract parameters, relationships, and
|
|
4
|
+
* generate a family of configurations. Includes variant analysis, parameter inference,
|
|
5
|
+
* parametric model generation, design table, interpolation/extrapolation, and UI panel.
|
|
6
|
+
*
|
|
7
|
+
* Feature list:
|
|
8
|
+
* - Variant analyzer: extract dimensional fingerprints from meshes
|
|
9
|
+
* - Parameter inference: detect linear, proportional, stepped, dependent relationships
|
|
10
|
+
* - Parametric model generator: create 3D geometry from parameter definitions
|
|
11
|
+
* - Design table: spreadsheet UI for managing configurations
|
|
12
|
+
* - Interpolation/extrapolation: morph between variants, extrapolate trends
|
|
13
|
+
* - UI panel: dark-themed controls with live preview
|
|
14
|
+
*
|
|
15
|
+
* @module parametric-from-example
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
19
|
+
|
|
20
|
+
const ParametricFromExample = (() => {
|
|
21
|
+
const THREE = window.THREE;
|
|
22
|
+
|
|
23
|
+
// ===== STATE =====
|
|
24
|
+
let variants = [];
|
|
25
|
+
let inferredParameters = [];
|
|
26
|
+
let parametricModel = null;
|
|
27
|
+
let configurations = [];
|
|
28
|
+
let selectedVariants = [];
|
|
29
|
+
let currentPreviewMesh = null;
|
|
30
|
+
let uiPanel = null;
|
|
31
|
+
let previewScene = null;
|
|
32
|
+
let previewRenderer = null;
|
|
33
|
+
let previewCamera = null;
|
|
34
|
+
|
|
35
|
+
// ===== UTILITY: Safe Expression Parser =====
|
|
36
|
+
/**
|
|
37
|
+
* Parse and evaluate a formula safely (no eval, limited operators)
|
|
38
|
+
* @param {string} formula - Formula like "width/4" or "ceil(height*0.5)"
|
|
39
|
+
* @param {object} params - Parameter values { width: 100, height: 50, ... }
|
|
40
|
+
* @returns {number} Evaluated result
|
|
41
|
+
*/
|
|
42
|
+
function safeEval(formula, params) {
|
|
43
|
+
try {
|
|
44
|
+
let expr = formula;
|
|
45
|
+
// Replace parameter placeholders
|
|
46
|
+
Object.entries(params).forEach(([key, val]) => {
|
|
47
|
+
expr = expr.replace(new RegExp(`\\b${key}\\b`, 'g'), val);
|
|
48
|
+
});
|
|
49
|
+
// Allowed functions
|
|
50
|
+
const Math2 = {
|
|
51
|
+
ceil: Math.ceil, floor: Math.floor, round: Math.round,
|
|
52
|
+
sqrt: Math.sqrt, abs: Math.abs, max: Math.max, min: Math.min,
|
|
53
|
+
sin: Math.sin, cos: Math.cos, tan: Math.tan, PI: Math.PI
|
|
54
|
+
};
|
|
55
|
+
// Safe evaluation with limited scope
|
|
56
|
+
const fn = new Function(...Object.keys(Math2), `return (${expr})`);
|
|
57
|
+
return fn(...Object.values(Math2));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn(`Formula evaluation failed: ${formula}`, e);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ===== 1. VARIANT ANALYZER =====
|
|
65
|
+
/**
|
|
66
|
+
* Extract dimensional fingerprint from a Three.js mesh or JSON description
|
|
67
|
+
* @param {THREE.Mesh|object} variant - Mesh or { type, dimensions, features }
|
|
68
|
+
* @param {number} index - Variant index
|
|
69
|
+
* @returns {object} Fingerprint with overall dims, features, edges, faces
|
|
70
|
+
*/
|
|
71
|
+
function extractFingerprint(variant, index) {
|
|
72
|
+
const fp = {
|
|
73
|
+
index,
|
|
74
|
+
label: `Variant ${String.fromCharCode(65 + index)}`,
|
|
75
|
+
overallDims: { width: 0, height: 0, depth: 0 },
|
|
76
|
+
cylindricalFeatures: [],
|
|
77
|
+
flatFaces: [],
|
|
78
|
+
fillets: [],
|
|
79
|
+
chamfers: [],
|
|
80
|
+
patterns: [],
|
|
81
|
+
wallThickness: 0,
|
|
82
|
+
angles: [],
|
|
83
|
+
mesh: null,
|
|
84
|
+
json: null
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!variant) return fp;
|
|
88
|
+
|
|
89
|
+
// Handle Three.js Mesh
|
|
90
|
+
if (variant.isMesh && variant.geometry) {
|
|
91
|
+
fp.mesh = variant;
|
|
92
|
+
const geom = variant.geometry;
|
|
93
|
+
const bbox = new THREE.Box3().setFromObject(variant);
|
|
94
|
+
fp.overallDims = {
|
|
95
|
+
width: Math.round((bbox.max.x - bbox.min.x) * 100) / 100,
|
|
96
|
+
height: Math.round((bbox.max.y - bbox.min.y) * 100) / 100,
|
|
97
|
+
depth: Math.round((bbox.max.z - bbox.min.z) * 100) / 100
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Extract cylindrical features (simple approximation: capsule shapes)
|
|
101
|
+
if (geom.attributes && geom.attributes.position) {
|
|
102
|
+
const positions = geom.attributes.position.array;
|
|
103
|
+
for (let i = 0; i < Math.min(positions.length, 100); i += 3) {
|
|
104
|
+
const x = positions[i], y = positions[i + 1], z = positions[i + 2];
|
|
105
|
+
const r = Math.sqrt(x * x + z * z);
|
|
106
|
+
if (r > 0.5 && r < fp.overallDims.width / 4) {
|
|
107
|
+
const existing = fp.cylindricalFeatures.find(f => Math.abs(f.radius - r) < 1);
|
|
108
|
+
if (!existing) {
|
|
109
|
+
fp.cylindricalFeatures.push({
|
|
110
|
+
radius: Math.round(r * 100) / 100,
|
|
111
|
+
position: new THREE.Vector3(x, y, z),
|
|
112
|
+
height: 0 // Would require more analysis
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract flat faces (normals aligned to axes)
|
|
120
|
+
const axisAligned = [
|
|
121
|
+
{ normal: [1, 0, 0], name: 'yz-plane' },
|
|
122
|
+
{ normal: [0, 1, 0], name: 'xz-plane' },
|
|
123
|
+
{ normal: [0, 0, 1], name: 'xy-plane' }
|
|
124
|
+
];
|
|
125
|
+
axisAligned.forEach(face => {
|
|
126
|
+
const area = (fp.overallDims.width * fp.overallDims.height * fp.overallDims.depth) / 3;
|
|
127
|
+
fp.flatFaces.push({
|
|
128
|
+
normal: face.normal,
|
|
129
|
+
name: face.name,
|
|
130
|
+
area: Math.round(area * 100) / 100
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Estimate fillet from edge curvature
|
|
135
|
+
fp.fillets = [{ radius: 2, count: 4 }]; // Placeholder
|
|
136
|
+
fp.wallThickness = 3; // Placeholder
|
|
137
|
+
}
|
|
138
|
+
// Handle JSON description
|
|
139
|
+
else if (variant && typeof variant === 'object') {
|
|
140
|
+
fp.json = variant;
|
|
141
|
+
fp.overallDims = variant.dimensions || fp.overallDims;
|
|
142
|
+
fp.cylindricalFeatures = variant.features?.filter(f => f.type === 'cylinder') || [];
|
|
143
|
+
fp.flatFaces = variant.features?.filter(f => f.type === 'face') || [];
|
|
144
|
+
fp.patterns = variant.features?.filter(f => f.type === 'pattern') || [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return fp;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Align multiple variant fingerprints to a common coordinate frame
|
|
152
|
+
* Uses largest flat face as reference
|
|
153
|
+
* @param {array} fingerprints - Array of fingerprints
|
|
154
|
+
* @returns {array} Aligned fingerprints
|
|
155
|
+
*/
|
|
156
|
+
function alignVariants(fingerprints) {
|
|
157
|
+
if (fingerprints.length < 2) return fingerprints;
|
|
158
|
+
|
|
159
|
+
// Find reference variant (largest face area)
|
|
160
|
+
const ref = fingerprints.reduce((max, fp) => {
|
|
161
|
+
const maxArea = max.flatFaces[0]?.area || 0;
|
|
162
|
+
const fpArea = fp.flatFaces[0]?.area || 0;
|
|
163
|
+
return fpArea > maxArea ? fp : max;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const refArea = ref.flatFaces[0]?.area || 1;
|
|
167
|
+
const refDims = ref.overallDims;
|
|
168
|
+
|
|
169
|
+
// Align others relative to reference
|
|
170
|
+
return fingerprints.map(fp => {
|
|
171
|
+
const scale = Math.sqrt(refArea / (fp.flatFaces[0]?.area || 1));
|
|
172
|
+
fp.alignmentScale = scale;
|
|
173
|
+
fp.alignmentOffset = {
|
|
174
|
+
x: (refDims.width - fp.overallDims.width) / 2,
|
|
175
|
+
y: (refDims.height - fp.overallDims.height) / 2,
|
|
176
|
+
z: (refDims.depth - fp.overallDims.depth) / 2
|
|
177
|
+
};
|
|
178
|
+
return fp;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Compute dimensional differences between variant pairs
|
|
184
|
+
* @param {array} fingerprints - Aligned fingerprints
|
|
185
|
+
* @returns {array} Array of { variant1, variant2, deltas: { paramName: value } }
|
|
186
|
+
*/
|
|
187
|
+
function computeDifferenceMatrix(fingerprints) {
|
|
188
|
+
const differences = [];
|
|
189
|
+
for (let i = 0; i < fingerprints.length; i++) {
|
|
190
|
+
for (let j = i + 1; j < fingerprints.length; j++) {
|
|
191
|
+
const fp1 = fingerprints[i];
|
|
192
|
+
const fp2 = fingerprints[j];
|
|
193
|
+
const deltas = {};
|
|
194
|
+
|
|
195
|
+
// Dimensional deltas
|
|
196
|
+
['width', 'height', 'depth'].forEach(dim => {
|
|
197
|
+
deltas[`Δ${dim}`] = fp2.overallDims[dim] - fp1.overallDims[dim];
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Cylindrical feature deltas
|
|
201
|
+
for (let k = 0; k < Math.max(fp1.cylindricalFeatures.length, fp2.cylindricalFeatures.length); k++) {
|
|
202
|
+
const c1 = fp1.cylindricalFeatures[k] || {};
|
|
203
|
+
const c2 = fp2.cylindricalFeatures[k] || {};
|
|
204
|
+
deltas[`hole_${k}_radius`] = (c2.radius || 0) - (c1.radius || 0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Wall thickness
|
|
208
|
+
deltas.wallThickness = fp2.wallThickness - fp1.wallThickness;
|
|
209
|
+
|
|
210
|
+
// Fillet radius
|
|
211
|
+
if (fp1.fillets[0] && fp2.fillets[0]) {
|
|
212
|
+
deltas.filletRadius = fp2.fillets[0].radius - fp1.fillets[0].radius;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
differences.push({
|
|
216
|
+
variant1: fp1.label,
|
|
217
|
+
variant2: fp2.label,
|
|
218
|
+
deltas
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return differences;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===== 2. PARAMETER INFERENCE ENGINE =====
|
|
226
|
+
/**
|
|
227
|
+
* Infer which dimensions are parameters and which are constant
|
|
228
|
+
* @param {array} fingerprints - Variant fingerprints
|
|
229
|
+
* @param {array} differences - Difference matrix
|
|
230
|
+
* @returns {array} Inferred parameters
|
|
231
|
+
*/
|
|
232
|
+
function inferParameters(fingerprints, differences) {
|
|
233
|
+
const params = [];
|
|
234
|
+
const paramMap = new Map();
|
|
235
|
+
|
|
236
|
+
// Collect all dimensional keys
|
|
237
|
+
const allKeys = new Set();
|
|
238
|
+
['width', 'height', 'depth', 'wallThickness', 'filletRadius'].forEach(k => allKeys.add(k));
|
|
239
|
+
fingerprints.forEach(fp => {
|
|
240
|
+
fp.cylindricalFeatures.forEach((c, i) => {
|
|
241
|
+
allKeys.add(`hole_${i}_radius`);
|
|
242
|
+
allKeys.add(`hole_${i}_count`);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Analyze variance for each dimension
|
|
247
|
+
allKeys.forEach(key => {
|
|
248
|
+
const values = fingerprints.map(fp => {
|
|
249
|
+
if (key === 'width') return fp.overallDims.width;
|
|
250
|
+
if (key === 'height') return fp.overallDims.height;
|
|
251
|
+
if (key === 'depth') return fp.overallDims.depth;
|
|
252
|
+
if (key === 'wallThickness') return fp.wallThickness;
|
|
253
|
+
if (key === 'filletRadius') return fp.fillets[0]?.radius || 0;
|
|
254
|
+
const m = key.match(/hole_(\d+)_radius/);
|
|
255
|
+
if (m) return fp.cylindricalFeatures[parseInt(m[1])]?.radius || 0;
|
|
256
|
+
return 0;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const variance = Math.max(...values) - Math.min(...values);
|
|
260
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
261
|
+
const cv = mean > 0 ? variance / mean : 0; // Coefficient of variation
|
|
262
|
+
|
|
263
|
+
if (cv > 0.05) { // More than 5% variance = parameter
|
|
264
|
+
const paramName = {
|
|
265
|
+
'width': 'Width', 'height': 'Height', 'depth': 'Depth',
|
|
266
|
+
'wallThickness': 'Wall Thickness', 'filletRadius': 'Fillet Radius'
|
|
267
|
+
}[key] || key;
|
|
268
|
+
|
|
269
|
+
params.push({
|
|
270
|
+
name: paramName,
|
|
271
|
+
key,
|
|
272
|
+
type: key.includes('count') ? 'count' : 'length',
|
|
273
|
+
min: Math.min(...values),
|
|
274
|
+
max: Math.max(...values),
|
|
275
|
+
default: values[0],
|
|
276
|
+
variance: variance,
|
|
277
|
+
values,
|
|
278
|
+
unit: key.includes('count') ? '' : 'mm',
|
|
279
|
+
confidence: 0.8,
|
|
280
|
+
formula: null,
|
|
281
|
+
relationships: []
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Detect relationships between parameters
|
|
287
|
+
params.forEach((p1, i) => {
|
|
288
|
+
params.forEach((p2, j) => {
|
|
289
|
+
if (i === j) return;
|
|
290
|
+
|
|
291
|
+
// Linear regression: p2 = a*p1 + b
|
|
292
|
+
const xs = p1.values;
|
|
293
|
+
const ys = p2.values;
|
|
294
|
+
const n = xs.length;
|
|
295
|
+
const sumX = xs.reduce((a, b) => a + b, 0);
|
|
296
|
+
const sumY = ys.reduce((a, b) => a + b, 0);
|
|
297
|
+
const sumXY = xs.reduce((a, b, k) => a + b * ys[k], 0);
|
|
298
|
+
const sumX2 = xs.reduce((a, b) => a + b * b, 0);
|
|
299
|
+
|
|
300
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
301
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
302
|
+
|
|
303
|
+
if (!isNaN(slope) && !isNaN(intercept) && Math.abs(slope) < 10) {
|
|
304
|
+
const R2 = Math.max(0, 1 - (ys.reduce((a, b, k) => a + Math.pow(b - (slope * xs[k] + intercept), 2), 0) / ys.reduce((a, b) => a + Math.pow(b - sumY / n, 2), 0)));
|
|
305
|
+
|
|
306
|
+
if (R2 > 0.7) {
|
|
307
|
+
p1.relationships.push({
|
|
308
|
+
targetParam: p2.name,
|
|
309
|
+
type: 'linear',
|
|
310
|
+
formula: `${slope.toFixed(2)} * ${p1.name} + ${intercept.toFixed(2)}`,
|
|
311
|
+
confidence: R2
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return params;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ===== 3. PARAMETRIC MODEL GENERATOR =====
|
|
322
|
+
/**
|
|
323
|
+
* Generate a parametric model definition from parameters
|
|
324
|
+
* @param {array} inferredParams - Inferred parameters
|
|
325
|
+
* @param {object} firstVariant - First variant for feature template
|
|
326
|
+
* @returns {object} Parametric model { parameters, features }
|
|
327
|
+
*/
|
|
328
|
+
function generateParametricModel(inferredParams, firstVariant) {
|
|
329
|
+
const model = {
|
|
330
|
+
parameters: inferredParams.map(p => ({
|
|
331
|
+
name: p.name,
|
|
332
|
+
key: p.key,
|
|
333
|
+
type: p.type,
|
|
334
|
+
default: p.default,
|
|
335
|
+
min: p.min,
|
|
336
|
+
max: p.max,
|
|
337
|
+
unit: p.unit,
|
|
338
|
+
formula: p.relationships[0]?.formula || null
|
|
339
|
+
})),
|
|
340
|
+
features: []
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Generate feature templates from first variant
|
|
344
|
+
if (firstVariant && firstVariant.flatFaces) {
|
|
345
|
+
model.features.push({
|
|
346
|
+
type: 'box',
|
|
347
|
+
width: '$Width',
|
|
348
|
+
height: '$Height',
|
|
349
|
+
depth: '$Depth'
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
firstVariant.cylindricalFeatures.forEach((hole, i) => {
|
|
353
|
+
model.features.push({
|
|
354
|
+
type: 'hole',
|
|
355
|
+
radius: hole.radius,
|
|
356
|
+
pattern: 'linear',
|
|
357
|
+
count: Math.min(4, firstVariant.cylindricalFeatures.length),
|
|
358
|
+
spacing: '$Width/4'
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (firstVariant.fillets[0]) {
|
|
363
|
+
model.features.push({
|
|
364
|
+
type: 'fillet',
|
|
365
|
+
radius: '$Fillet Radius',
|
|
366
|
+
edges: 'all'
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return model;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Generate Three.js geometry from parametric definition with parameter values
|
|
376
|
+
* @param {object} modelDef - Parametric model definition
|
|
377
|
+
* @param {object} paramValues - { paramName: value, ... }
|
|
378
|
+
* @returns {THREE.Mesh} Generated mesh
|
|
379
|
+
*/
|
|
380
|
+
function generateGeometryFromModel(modelDef, paramValues) {
|
|
381
|
+
const group = new THREE.Group();
|
|
382
|
+
|
|
383
|
+
modelDef.features.forEach(feature => {
|
|
384
|
+
let geom = null;
|
|
385
|
+
|
|
386
|
+
if (feature.type === 'box') {
|
|
387
|
+
const w = safeEval(feature.width, paramValues);
|
|
388
|
+
const h = safeEval(feature.height, paramValues);
|
|
389
|
+
const d = safeEval(feature.depth, paramValues);
|
|
390
|
+
geom = new THREE.BoxGeometry(w, h, d);
|
|
391
|
+
} else if (feature.type === 'cylinder') {
|
|
392
|
+
const r = safeEval(feature.radius?.toString() || '5', paramValues);
|
|
393
|
+
const height = safeEval(feature.height?.toString() || '10', paramValues);
|
|
394
|
+
geom = new THREE.CylinderGeometry(r, r, height, 32);
|
|
395
|
+
} else if (feature.type === 'hole') {
|
|
396
|
+
const r = safeEval(feature.radius?.toString() || '2', paramValues);
|
|
397
|
+
const count = Math.floor(safeEval(feature.count?.toString() || '4', paramValues));
|
|
398
|
+
const spacing = safeEval(feature.spacing || '25', paramValues);
|
|
399
|
+
for (let i = 0; i < count; i++) {
|
|
400
|
+
const x = -spacing * (count - 1) / 2 + i * spacing;
|
|
401
|
+
const holeGeom = new THREE.CylinderGeometry(r, r, 1, 16);
|
|
402
|
+
const holeMesh = new THREE.Mesh(holeGeom);
|
|
403
|
+
holeMesh.position.x = x;
|
|
404
|
+
group.add(holeMesh);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (geom) {
|
|
409
|
+
const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial({ color: 0x4da6ff }));
|
|
410
|
+
group.add(mesh);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return group;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ===== 4. DESIGN TABLE =====
|
|
418
|
+
/**
|
|
419
|
+
* Create a design table (spreadsheet-like) for managing configurations
|
|
420
|
+
* @param {array} params - Inferred parameters
|
|
421
|
+
* @returns {array} Design table rows
|
|
422
|
+
*/
|
|
423
|
+
function createDesignTable(params) {
|
|
424
|
+
const table = [];
|
|
425
|
+
|
|
426
|
+
// Original variant rows
|
|
427
|
+
variants.forEach((v, i) => {
|
|
428
|
+
const row = { config: `Original ${String.fromCharCode(65 + i)}`, source: 'original' };
|
|
429
|
+
params.forEach(p => {
|
|
430
|
+
row[p.key] = p.values[i];
|
|
431
|
+
});
|
|
432
|
+
table.push(row);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return table;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Add generated configuration to design table
|
|
440
|
+
* @param {object} paramValues - Parameter values
|
|
441
|
+
* @param {string} label - Configuration label
|
|
442
|
+
*/
|
|
443
|
+
function addConfiguration(paramValues, label) {
|
|
444
|
+
const row = { config: label, source: 'generated' };
|
|
445
|
+
inferredParameters.forEach(p => {
|
|
446
|
+
row[p.key] = paramValues[p.name] || p.default;
|
|
447
|
+
});
|
|
448
|
+
configurations.push(row);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ===== 5. INTERPOLATION & EXTRAPOLATION =====
|
|
452
|
+
/**
|
|
453
|
+
* Interpolate between two configurations (linear blend)
|
|
454
|
+
* @param {object} config1 - First configuration
|
|
455
|
+
* @param {object} config2 - Second configuration
|
|
456
|
+
* @param {number} t - Interpolation factor (0-1)
|
|
457
|
+
* @returns {object} Interpolated configuration
|
|
458
|
+
*/
|
|
459
|
+
function interpolateConfigs(config1, config2, t) {
|
|
460
|
+
const result = { config: `Interpolated (${(t * 100).toFixed(0)}%)` };
|
|
461
|
+
Object.entries(config1).forEach(([key, val]) => {
|
|
462
|
+
if (key !== 'config' && key !== 'source' && typeof val === 'number') {
|
|
463
|
+
result[key] = val * (1 - t) + (config2[key] || val) * t;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Extrapolate beyond training range (linear extension)
|
|
471
|
+
* @param {object} config1 - First configuration
|
|
472
|
+
* @param {object} config2 - Second configuration
|
|
473
|
+
* @param {number} factor - Extrapolation factor (>1)
|
|
474
|
+
* @returns {object} Extrapolated configuration + warning
|
|
475
|
+
*/
|
|
476
|
+
function extrapolateConfig(config1, config2, factor) {
|
|
477
|
+
const result = { config: `Extrapolated (${(factor * 100).toFixed(0)}%)`, warnings: [] };
|
|
478
|
+
Object.entries(config1).forEach(([key, val]) => {
|
|
479
|
+
if (key !== 'config' && key !== 'source' && typeof val === 'number') {
|
|
480
|
+
const delta = (config2[key] || val) - val;
|
|
481
|
+
result[key] = val + delta * factor;
|
|
482
|
+
if (factor > 1.5) {
|
|
483
|
+
result.warnings.push(`${key} extrapolated ${Math.round(factor * 100)}% beyond training data`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Morph animation between two configurations
|
|
492
|
+
* @param {object} config1 - Start configuration
|
|
493
|
+
* @param {object} config2 - End configuration
|
|
494
|
+
* @param {number} duration - Animation duration in ms
|
|
495
|
+
* @param {function} onFrame - Callback with interpolated config
|
|
496
|
+
*/
|
|
497
|
+
function morphAnimation(config1, config2, duration, onFrame) {
|
|
498
|
+
const startTime = Date.now();
|
|
499
|
+
const animate = () => {
|
|
500
|
+
const elapsed = Date.now() - startTime;
|
|
501
|
+
const t = Math.min(1, elapsed / duration);
|
|
502
|
+
const config = interpolateConfigs(config1, config2, t);
|
|
503
|
+
onFrame(config);
|
|
504
|
+
if (t < 1) requestAnimationFrame(animate);
|
|
505
|
+
};
|
|
506
|
+
animate();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ===== MAIN PUBLIC API =====
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Initialize the module
|
|
513
|
+
*/
|
|
514
|
+
function init() {
|
|
515
|
+
console.log('ParametricFromExample: initialized');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Analyze variant fingerprints and infer parameters
|
|
520
|
+
* @param {array} variantMeshes - 2-5 Three.js meshes or JSON objects
|
|
521
|
+
* @returns {object} Analysis result with parameters, relationships, differences
|
|
522
|
+
*/
|
|
523
|
+
function analyzeVariants(variantMeshes) {
|
|
524
|
+
variants = variantMeshes;
|
|
525
|
+
selectedVariants = [];
|
|
526
|
+
|
|
527
|
+
// Extract fingerprints
|
|
528
|
+
const fingerprints = variantMeshes.map((v, i) => extractFingerprint(v, i));
|
|
529
|
+
|
|
530
|
+
// Align variants
|
|
531
|
+
const aligned = alignVariants(fingerprints);
|
|
532
|
+
|
|
533
|
+
// Compute differences
|
|
534
|
+
const differences = computeDifferenceMatrix(aligned);
|
|
535
|
+
|
|
536
|
+
// Infer parameters
|
|
537
|
+
inferredParameters = inferParameters(aligned, differences);
|
|
538
|
+
|
|
539
|
+
console.log('Analyzed variants:', {
|
|
540
|
+
count: variantMeshes.length,
|
|
541
|
+
parameters: inferredParameters.length,
|
|
542
|
+
relationships: inferredParameters.reduce((sum, p) => sum + p.relationships.length, 0)
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
fingerprints: aligned,
|
|
547
|
+
differences,
|
|
548
|
+
parameters: inferredParameters,
|
|
549
|
+
relationships: inferredParameters.flatMap(p => p.relationships)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Generate parametric model definition
|
|
555
|
+
* @returns {object} Parametric model with feature templates
|
|
556
|
+
*/
|
|
557
|
+
function generateFamily() {
|
|
558
|
+
if (!variants.length) {
|
|
559
|
+
console.warn('No variants analyzed yet');
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
parametricModel = generateParametricModel(inferredParameters, variants[0]);
|
|
564
|
+
configurations = createDesignTable(inferredParameters);
|
|
565
|
+
|
|
566
|
+
console.log('Generated parametric model with', parametricModel.features.length, 'features');
|
|
567
|
+
|
|
568
|
+
return parametricModel;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Generate new configuration and add to design table
|
|
573
|
+
* @param {object} paramValues - { paramName: value, ... }
|
|
574
|
+
* @param {string} label - Configuration label
|
|
575
|
+
* @returns {THREE.Mesh} Generated mesh
|
|
576
|
+
*/
|
|
577
|
+
function generateConfiguration(paramValues, label) {
|
|
578
|
+
if (!parametricModel) {
|
|
579
|
+
console.warn('No parametric model generated yet');
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
addConfiguration(paramValues, label);
|
|
584
|
+
const mesh = generateGeometryFromModel(parametricModel, paramValues);
|
|
585
|
+
|
|
586
|
+
return mesh;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Execute command from agent API or UI
|
|
591
|
+
* @param {object} cmd - { action, params }
|
|
592
|
+
*/
|
|
593
|
+
function execute(cmd) {
|
|
594
|
+
if (!cmd) return;
|
|
595
|
+
|
|
596
|
+
switch (cmd.action) {
|
|
597
|
+
case 'analyzeVariants':
|
|
598
|
+
analyzeVariants(cmd.variants || []);
|
|
599
|
+
break;
|
|
600
|
+
case 'generateFamily':
|
|
601
|
+
generateFamily();
|
|
602
|
+
break;
|
|
603
|
+
case 'generateConfiguration':
|
|
604
|
+
generateConfiguration(cmd.paramValues, cmd.label);
|
|
605
|
+
break;
|
|
606
|
+
case 'interpolate':
|
|
607
|
+
const interp = interpolateConfigs(cmd.config1, cmd.config2, cmd.t || 0.5);
|
|
608
|
+
console.log('Interpolated:', interp);
|
|
609
|
+
break;
|
|
610
|
+
case 'extrapolate':
|
|
611
|
+
const extrap = extrapolateConfig(cmd.config1, cmd.config2, cmd.factor || 1.5);
|
|
612
|
+
console.log('Extrapolated:', extrap);
|
|
613
|
+
break;
|
|
614
|
+
case 'morph':
|
|
615
|
+
morphAnimation(cmd.config1, cmd.config2, cmd.duration || 2000, cmd.onFrame);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Return UI panel HTML
|
|
622
|
+
* @returns {HTMLElement} Panel element
|
|
623
|
+
*/
|
|
624
|
+
function getUI() {
|
|
625
|
+
const panel = document.createElement('div');
|
|
626
|
+
panel.style.cssText = `
|
|
627
|
+
position: relative;
|
|
628
|
+
background: var(--bg-secondary);
|
|
629
|
+
border: 1px solid var(--border-color);
|
|
630
|
+
border-radius: 4px;
|
|
631
|
+
padding: 0;
|
|
632
|
+
height: 100%;
|
|
633
|
+
display: flex;
|
|
634
|
+
flex-direction: column;
|
|
635
|
+
font-size: 12px;
|
|
636
|
+
color: var(--text-primary);
|
|
637
|
+
`;
|
|
638
|
+
|
|
639
|
+
// Tabs
|
|
640
|
+
const tabBar = document.createElement('div');
|
|
641
|
+
tabBar.style.cssText = `
|
|
642
|
+
display: flex;
|
|
643
|
+
border-bottom: 1px solid var(--border-color);
|
|
644
|
+
gap: 0;
|
|
645
|
+
background: var(--bg-tertiary);
|
|
646
|
+
`;
|
|
647
|
+
|
|
648
|
+
const tabs = ['Input', 'Parameters', 'Edit', 'Table', 'Morph'];
|
|
649
|
+
const tabContents = {};
|
|
650
|
+
|
|
651
|
+
tabs.forEach((tab, idx) => {
|
|
652
|
+
const btn = document.createElement('button');
|
|
653
|
+
btn.textContent = tab;
|
|
654
|
+
btn.style.cssText = `
|
|
655
|
+
flex: 1;
|
|
656
|
+
padding: 8px 12px;
|
|
657
|
+
border: none;
|
|
658
|
+
background: ${idx === 0 ? 'var(--bg-secondary)' : 'var(--bg-tertiary)'};
|
|
659
|
+
color: var(--text-primary);
|
|
660
|
+
cursor: pointer;
|
|
661
|
+
border-bottom: ${idx === 0 ? '2px solid var(--accent-blue)' : 'none'};
|
|
662
|
+
font-weight: 500;
|
|
663
|
+
transition: background var(--transition-fast);
|
|
664
|
+
`;
|
|
665
|
+
btn.onmouseover = () => btn.style.background = 'var(--bg-secondary)';
|
|
666
|
+
btn.onmouseout = () => btn.style.background = idx === 0 ? 'var(--bg-secondary)' : 'var(--bg-tertiary)';
|
|
667
|
+
|
|
668
|
+
btn.onclick = () => {
|
|
669
|
+
// Hide all tabs
|
|
670
|
+
Object.values(tabContents).forEach(el => el.style.display = 'none');
|
|
671
|
+
// Show selected
|
|
672
|
+
if (tabContents[tab]) tabContents[tab].style.display = 'block';
|
|
673
|
+
// Update button styling
|
|
674
|
+
Array.from(tabBar.children).forEach((b, i) => {
|
|
675
|
+
b.style.borderBottom = i === tabs.indexOf(tab) ? '2px solid var(--accent-blue)' : 'none';
|
|
676
|
+
b.style.background = i === tabs.indexOf(tab) ? 'var(--bg-secondary)' : 'var(--bg-tertiary)';
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
tabBar.appendChild(btn);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
panel.appendChild(tabBar);
|
|
683
|
+
|
|
684
|
+
// Content area
|
|
685
|
+
const contentArea = document.createElement('div');
|
|
686
|
+
contentArea.style.cssText = `
|
|
687
|
+
flex: 1;
|
|
688
|
+
overflow-y: auto;
|
|
689
|
+
padding: 12px;
|
|
690
|
+
gap: 12px;
|
|
691
|
+
display: flex;
|
|
692
|
+
flex-direction: column;
|
|
693
|
+
`;
|
|
694
|
+
|
|
695
|
+
// === INPUT TAB ===
|
|
696
|
+
const inputTab = document.createElement('div');
|
|
697
|
+
inputTab.style.cssText = 'display: flex; flex-direction: column; gap: 12px;';
|
|
698
|
+
|
|
699
|
+
const dropZone = document.createElement('div');
|
|
700
|
+
dropZone.style.cssText = `
|
|
701
|
+
border: 2px dashed var(--accent-blue);
|
|
702
|
+
border-radius: 4px;
|
|
703
|
+
padding: 20px;
|
|
704
|
+
text-align: center;
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
background: var(--bg-tertiary);
|
|
707
|
+
transition: background var(--transition-fast);
|
|
708
|
+
`;
|
|
709
|
+
dropZone.innerHTML = '<p style="margin: 0; color: var(--text-secondary);">Drop variant meshes here</p>';
|
|
710
|
+
dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.background = 'var(--accent-blue)'; };
|
|
711
|
+
dropZone.ondragleave = () => dropZone.style.background = 'var(--bg-tertiary)';
|
|
712
|
+
inputTab.appendChild(dropZone);
|
|
713
|
+
|
|
714
|
+
const analyzeBtn = document.createElement('button');
|
|
715
|
+
analyzeBtn.textContent = 'Analyze Variants';
|
|
716
|
+
analyzeBtn.style.cssText = `
|
|
717
|
+
padding: 8px 12px;
|
|
718
|
+
background: var(--accent-blue);
|
|
719
|
+
color: white;
|
|
720
|
+
border-radius: 3px;
|
|
721
|
+
font-weight: 500;
|
|
722
|
+
cursor: pointer;
|
|
723
|
+
transition: background var(--transition-fast);
|
|
724
|
+
`;
|
|
725
|
+
analyzeBtn.onmouseover = () => analyzeBtn.style.background = 'var(--accent-blue-hover)';
|
|
726
|
+
analyzeBtn.onmouseout = () => analyzeBtn.style.background = 'var(--accent-blue)';
|
|
727
|
+
analyzeBtn.onclick = () => {
|
|
728
|
+
if (variants.length > 0) {
|
|
729
|
+
analyzeVariants(variants);
|
|
730
|
+
alert(`Analyzed ${variants.length} variants, inferred ${inferredParameters.length} parameters`);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
inputTab.appendChild(analyzeBtn);
|
|
734
|
+
|
|
735
|
+
tabContents['Input'] = inputTab;
|
|
736
|
+
|
|
737
|
+
// === PARAMETERS TAB ===
|
|
738
|
+
const paramsTab = document.createElement('div');
|
|
739
|
+
paramsTab.style.display = 'none';
|
|
740
|
+
paramsTab.style.cssText = 'display: flex; flex-direction: column; gap: 8px;';
|
|
741
|
+
|
|
742
|
+
inferredParameters.forEach(p => {
|
|
743
|
+
const row = document.createElement('div');
|
|
744
|
+
row.style.cssText = 'padding: 8px; background: var(--bg-tertiary); border-radius: 3px;';
|
|
745
|
+
row.innerHTML = `
|
|
746
|
+
<div style="font-weight: 500; margin-bottom: 4px;">${p.name}</div>
|
|
747
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">
|
|
748
|
+
${p.min.toFixed(2)} — ${p.max.toFixed(2)} ${p.unit}
|
|
749
|
+
</div>
|
|
750
|
+
<div style="font-size: 11px; color: var(--text-muted);">
|
|
751
|
+
Confidence: <span style="color: var(--accent-green);">${(p.confidence * 100).toFixed(0)}%</span>
|
|
752
|
+
</div>
|
|
753
|
+
${p.formula ? `<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">Formula: ${p.formula}</div>` : ''}
|
|
754
|
+
`;
|
|
755
|
+
paramsTab.appendChild(row);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
tabContents['Parameters'] = paramsTab;
|
|
759
|
+
|
|
760
|
+
// === EDIT TAB ===
|
|
761
|
+
const editTab = document.createElement('div');
|
|
762
|
+
editTab.style.display = 'none';
|
|
763
|
+
editTab.style.cssText = 'display: flex; flex-direction: column; gap: 12px;';
|
|
764
|
+
|
|
765
|
+
inferredParameters.forEach(p => {
|
|
766
|
+
const label = document.createElement('label');
|
|
767
|
+
label.style.cssText = 'display: flex; flex-direction: column; gap: 4px;';
|
|
768
|
+
label.innerHTML = `
|
|
769
|
+
<span style="font-weight: 500;">${p.name}</span>
|
|
770
|
+
<input type="range" min="${p.min}" max="${p.max}" value="${p.default}" step="1"
|
|
771
|
+
style="width: 100%; cursor: pointer;">
|
|
772
|
+
<span style="font-size: 11px; color: var(--text-secondary);" class="value-display">${p.default} ${p.unit}</span>
|
|
773
|
+
`;
|
|
774
|
+
const input = label.querySelector('input');
|
|
775
|
+
const display = label.querySelector('.value-display');
|
|
776
|
+
input.oninput = () => {
|
|
777
|
+
display.textContent = `${input.value} ${p.unit}`;
|
|
778
|
+
// Trigger preview update
|
|
779
|
+
const paramVals = {};
|
|
780
|
+
editTab.querySelectorAll('input[type="range"]').forEach((inp, i) => {
|
|
781
|
+
paramVals[inferredParameters[i].name] = parseFloat(inp.value);
|
|
782
|
+
});
|
|
783
|
+
if (parametricModel) {
|
|
784
|
+
const mesh = generateGeometryFromModel(parametricModel, paramVals);
|
|
785
|
+
// Update preview in viewport
|
|
786
|
+
if (currentPreviewMesh && window.cycleCAD && window.cycleCAD.scene) {
|
|
787
|
+
window.cycleCAD.scene.remove(currentPreviewMesh);
|
|
788
|
+
}
|
|
789
|
+
currentPreviewMesh = mesh;
|
|
790
|
+
if (window.cycleCAD && window.cycleCAD.scene) {
|
|
791
|
+
window.cycleCAD.scene.add(mesh);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
editTab.appendChild(label);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const genBtn = document.createElement('button');
|
|
799
|
+
genBtn.textContent = 'Generate Configuration';
|
|
800
|
+
genBtn.style.cssText = `
|
|
801
|
+
padding: 8px 12px;
|
|
802
|
+
background: var(--accent-green);
|
|
803
|
+
color: white;
|
|
804
|
+
border-radius: 3px;
|
|
805
|
+
font-weight: 500;
|
|
806
|
+
cursor: pointer;
|
|
807
|
+
margin-top: 8px;
|
|
808
|
+
`;
|
|
809
|
+
genBtn.onclick = () => {
|
|
810
|
+
const paramVals = {};
|
|
811
|
+
editTab.querySelectorAll('input[type="range"]').forEach((inp, i) => {
|
|
812
|
+
paramVals[inferredParameters[i].name] = parseFloat(inp.value);
|
|
813
|
+
});
|
|
814
|
+
generateConfiguration(paramVals, `Config-${Date.now()}`);
|
|
815
|
+
};
|
|
816
|
+
editTab.appendChild(genBtn);
|
|
817
|
+
|
|
818
|
+
tabContents['Edit'] = editTab;
|
|
819
|
+
|
|
820
|
+
// === TABLE TAB ===
|
|
821
|
+
const tableTab = document.createElement('div');
|
|
822
|
+
tableTab.style.display = 'none';
|
|
823
|
+
tableTab.style.cssText = 'display: flex; flex-direction: column; gap: 8px;';
|
|
824
|
+
|
|
825
|
+
const tableHtml = document.createElement('div');
|
|
826
|
+
tableHtml.style.cssText = 'font-size: 11px; overflow-x: auto;';
|
|
827
|
+
tableHtml.innerHTML = '<p style="color: var(--text-secondary);">Design configurations will appear here</p>';
|
|
828
|
+
tableTab.appendChild(tableHtml);
|
|
829
|
+
|
|
830
|
+
tabContents['Table'] = tableTab;
|
|
831
|
+
|
|
832
|
+
// === MORPH TAB ===
|
|
833
|
+
const morphTab = document.createElement('div');
|
|
834
|
+
morphTab.style.display = 'none';
|
|
835
|
+
morphTab.style.cssText = 'display: flex; flex-direction: column; gap: 12px;';
|
|
836
|
+
|
|
837
|
+
const morphSlider = document.createElement('input');
|
|
838
|
+
morphSlider.type = 'range';
|
|
839
|
+
morphSlider.min = '0';
|
|
840
|
+
morphSlider.max = '100';
|
|
841
|
+
morphSlider.value = '50';
|
|
842
|
+
morphSlider.style.cssText = 'width: 100%; cursor: pointer;';
|
|
843
|
+
morphTab.appendChild(morphSlider);
|
|
844
|
+
|
|
845
|
+
const morphDisplay = document.createElement('div');
|
|
846
|
+
morphDisplay.style.cssText = 'font-size: 11px; color: var(--text-secondary); text-align: center;';
|
|
847
|
+
morphDisplay.textContent = 'Interpolation: 50%';
|
|
848
|
+
morphTab.appendChild(morphDisplay);
|
|
849
|
+
|
|
850
|
+
morphSlider.oninput = () => {
|
|
851
|
+
morphDisplay.textContent = `Interpolation: ${morphSlider.value}%`;
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const playBtn = document.createElement('button');
|
|
855
|
+
playBtn.textContent = 'Play Animation';
|
|
856
|
+
playBtn.style.cssText = `
|
|
857
|
+
padding: 8px 12px;
|
|
858
|
+
background: var(--accent-blue);
|
|
859
|
+
color: white;
|
|
860
|
+
border-radius: 3px;
|
|
861
|
+
font-weight: 500;
|
|
862
|
+
cursor: pointer;
|
|
863
|
+
`;
|
|
864
|
+
morphTab.appendChild(playBtn);
|
|
865
|
+
|
|
866
|
+
tabContents['Morph'] = morphTab;
|
|
867
|
+
|
|
868
|
+
// Assemble
|
|
869
|
+
contentArea.appendChild(inputTab);
|
|
870
|
+
contentArea.appendChild(paramsTab);
|
|
871
|
+
contentArea.appendChild(editTab);
|
|
872
|
+
contentArea.appendChild(tableTab);
|
|
873
|
+
contentArea.appendChild(morphTab);
|
|
874
|
+
|
|
875
|
+
panel.appendChild(contentArea);
|
|
876
|
+
|
|
877
|
+
uiPanel = panel;
|
|
878
|
+
return panel;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Export
|
|
882
|
+
return {
|
|
883
|
+
init,
|
|
884
|
+
getUI,
|
|
885
|
+
execute,
|
|
886
|
+
analyzeVariants,
|
|
887
|
+
inferParameters,
|
|
888
|
+
generateFamily,
|
|
889
|
+
generateConfiguration,
|
|
890
|
+
extractFingerprint,
|
|
891
|
+
alignVariants,
|
|
892
|
+
computeDifferenceMatrix,
|
|
893
|
+
interpolateConfigs,
|
|
894
|
+
extrapolateConfig,
|
|
895
|
+
morphAnimation,
|
|
896
|
+
safeEval
|
|
897
|
+
};
|
|
898
|
+
})();
|
|
899
|
+
|
|
900
|
+
window.CycleCAD.ParametricFromExample = ParametricFromExample;
|