Skip to content

Commit 723df87

Browse files
Bissbertclaude
andcommitted
feat: add twin and modification support to JS geometry engine
Ported from Python crystal_geometry package: - transforms.ts: Matrix operations (Rodrigues rotation, reflection) - twin-laws.ts: 14 twin law definitions with axes and angles - twin-generator.ts: Geometry merging for dual/v-shaped/cyclic twins - modifications.ts: elongate(), flatten(), scale() support Updated: - cdl-parser.ts: Parse twin(name) and modification specs - crystal-geometry.ts: Apply modifications and twins to geometry This enables the playground to render complex CDL with twins like: trigonal[32]:{10-10}@0.5 | elongate(c:2.0) | twin(brazil) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4dd6374 commit 723df87

6 files changed

Lines changed: 790 additions & 2 deletions

File tree

src/lib/cdl-parser.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,23 @@ export interface CrystalForm {
1515
scale: number;
1616
}
1717

18+
export interface TwinSpec {
19+
law: string;
20+
}
21+
22+
export interface ModificationSpec {
23+
type: 'elongate' | 'flatten' | 'scale';
24+
axis: 'a' | 'b' | 'c';
25+
factor: number;
26+
}
27+
1828
export interface CDLParseResult {
1929
system: string;
2030
pointGroup: string;
2131
forms: CrystalForm[];
2232
modifier?: string;
33+
twin?: TwinSpec;
34+
modifications?: ModificationSpec[];
2335
}
2436

2537
export interface ValidationResult {
@@ -180,13 +192,38 @@ export function parseCDL(cdl: string): ValidationResult {
180192
return { valid: false, error: 'At least one crystal form is required' };
181193
}
182194

195+
// Parse twin and modifications from modifier string
196+
let twin: TwinSpec | undefined;
197+
const modifications: ModificationSpec[] = [];
198+
199+
if (modifier) {
200+
// Parse twin: twin(name)
201+
const twinMatch = modifier.match(/twin\s*\(\s*(\w+)\s*\)/i);
202+
if (twinMatch) {
203+
twin = { law: twinMatch[1].toLowerCase() };
204+
}
205+
206+
// Parse modifications: elongate(c:2.0), flatten(a:0.5), scale(b:1.5)
207+
const modRegex = /(elongate|flatten|scale)\s*\(\s*([abc])\s*:\s*([\d.]+)\s*\)/gi;
208+
let modMatch;
209+
while ((modMatch = modRegex.exec(modifier)) !== null) {
210+
modifications.push({
211+
type: modMatch[1].toLowerCase() as ModificationSpec['type'],
212+
axis: modMatch[2].toLowerCase() as ModificationSpec['axis'],
213+
factor: parseFloat(modMatch[3]),
214+
});
215+
}
216+
}
217+
183218
return {
184219
valid: true,
185220
parsed: {
186221
system: system.toLowerCase(),
187222
pointGroup,
188223
forms,
189224
modifier,
225+
twin,
226+
modifications: modifications.length > 0 ? modifications : undefined,
190227
},
191228
};
192229
}

src/lib/crystal-geometry.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* Generates 3D crystal geometry using half-space intersection algorithm
44
*/
55

6-
import type { CDLParseResult, MillerIndex } from './cdl-parser';
6+
import type { CDLParseResult, MillerIndex, ModificationSpec } from './cdl-parser';
7+
import { generateTwinnedGeometry } from './twin-generator';
78

89
export interface Vector3 {
910
x: number;
@@ -401,5 +402,72 @@ export function generateGeometry(parsed: CDLParseResult): CrystalGeometry {
401402
}
402403
}
403404

404-
return { vertices, faces, edges };
405+
let geometry: CrystalGeometry = { vertices, faces, edges };
406+
407+
// Apply modifications (elongate, flatten, scale)
408+
if (parsed.modifications && parsed.modifications.length > 0) {
409+
geometry = applyModifications(geometry, parsed.modifications);
410+
}
411+
412+
// Apply twin transformation
413+
if (parsed.twin) {
414+
geometry = generateTwinnedGeometry(geometry, parsed.twin.law);
415+
}
416+
417+
return geometry;
418+
}
419+
420+
/**
421+
* Apply modifications to geometry (elongate, flatten, scale)
422+
*/
423+
function applyModifications(geom: CrystalGeometry, mods: ModificationSpec[]): CrystalGeometry {
424+
// Calculate scale factors for each axis
425+
const scales = { a: 1, b: 1, c: 1 };
426+
427+
for (const mod of mods) {
428+
let factor = mod.factor;
429+
if (mod.type === 'flatten') {
430+
factor = 1 / factor;
431+
}
432+
scales[mod.axis] *= factor;
433+
}
434+
435+
// Apply scales to vertices
436+
const newVertices = geom.vertices.map(v => ({
437+
x: v.x * scales.a,
438+
y: v.y * scales.b,
439+
z: v.z * scales.c,
440+
}));
441+
442+
// Apply scales to face vertices and recalculate normals
443+
const newFaces = geom.faces.map(face => {
444+
const scaledVertices = face.vertices.map(v => ({
445+
x: v.x * scales.a,
446+
y: v.y * scales.b,
447+
z: v.z * scales.c,
448+
}));
449+
450+
// Recalculate normal after scaling
451+
let normal = face.normal;
452+
if (scaledVertices.length >= 3) {
453+
const v0 = scaledVertices[0];
454+
const v1 = scaledVertices[1];
455+
const v2 = scaledVertices[2];
456+
const edge1 = sub(v1, v0);
457+
const edge2 = sub(v2, v0);
458+
normal = normalize(cross(edge1, edge2));
459+
}
460+
461+
return {
462+
...face,
463+
vertices: scaledVertices,
464+
normal,
465+
};
466+
});
467+
468+
return {
469+
vertices: newVertices,
470+
faces: newFaces,
471+
edges: geom.edges,
472+
};
405473
}

src/lib/modifications.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Crystal modification operations (elongate, flatten, etc.)
3+
* Ported from Python crystal_geometry.modifications
4+
*/
5+
6+
import type { Vec3 } from './transforms';
7+
8+
export interface Modification {
9+
type: 'elongate' | 'flatten' | 'scale';
10+
axis: 'a' | 'b' | 'c';
11+
factor: number;
12+
}
13+
14+
/**
15+
* Parse a modification string like "elongate(c:2.0)" or "flatten(a:0.5)"
16+
*/
17+
export function parseModification(modStr: string): Modification | null {
18+
// Match patterns like: elongate(c:2.0), flatten(a:0.5), scale(b:1.5)
19+
const match = modStr.match(/^(elongate|flatten|scale)\s*\(\s*([abc])\s*:\s*([\d.]+)\s*\)$/i);
20+
if (!match) return null;
21+
22+
const type = match[1].toLowerCase() as Modification['type'];
23+
const axis = match[2].toLowerCase() as Modification['axis'];
24+
const factor = parseFloat(match[3]);
25+
26+
if (isNaN(factor) || factor <= 0) return null;
27+
28+
return { type, axis, factor };
29+
}
30+
31+
/**
32+
* Parse multiple modifications from a modifier string
33+
* Handles: "elongate(c:2.0) | flatten(a:0.5)" or just "elongate(c:2.0)"
34+
*/
35+
export function parseModifications(modifierStr: string): Modification[] {
36+
const modifications: Modification[] = [];
37+
38+
// Split by | and process each part
39+
const parts = modifierStr.split('|').map(s => s.trim());
40+
41+
for (const part of parts) {
42+
// Skip twin modifiers
43+
if (part.startsWith('twin(')) continue;
44+
45+
const mod = parseModification(part);
46+
if (mod) {
47+
modifications.push(mod);
48+
}
49+
}
50+
51+
return modifications;
52+
}
53+
54+
/**
55+
* Apply a modification to a vertex
56+
*/
57+
export function applyModificationToVertex(v: Vec3, mod: Modification): Vec3 {
58+
const result = { ...v };
59+
60+
// Determine effective scale factor
61+
let scale = mod.factor;
62+
if (mod.type === 'flatten') {
63+
scale = 1 / mod.factor; // Flatten reduces the dimension
64+
}
65+
66+
// Apply to appropriate axis
67+
switch (mod.axis) {
68+
case 'a':
69+
result.x *= scale;
70+
break;
71+
case 'b':
72+
result.y *= scale;
73+
break;
74+
case 'c':
75+
result.z *= scale;
76+
break;
77+
}
78+
79+
return result;
80+
}
81+
82+
/**
83+
* Apply all modifications to a vertex
84+
*/
85+
export function applyModificationsToVertex(v: Vec3, mods: Modification[]): Vec3 {
86+
let result = { ...v };
87+
for (const mod of mods) {
88+
result = applyModificationToVertex(result, mod);
89+
}
90+
return result;
91+
}
92+
93+
/**
94+
* Apply modifications to all vertices in an array
95+
*/
96+
export function applyModificationsToVertices(vertices: Vec3[], mods: Modification[]): Vec3[] {
97+
if (mods.length === 0) return vertices;
98+
return vertices.map(v => applyModificationsToVertex(v, mods));
99+
}
100+
101+
/**
102+
* Get the scale factors for a/b/c axes from modifications
103+
*/
104+
export function getScaleFactors(mods: Modification[]): { a: number; b: number; c: number } {
105+
const scales = { a: 1, b: 1, c: 1 };
106+
107+
for (const mod of mods) {
108+
let factor = mod.factor;
109+
if (mod.type === 'flatten') {
110+
factor = 1 / factor;
111+
}
112+
scales[mod.axis] *= factor;
113+
}
114+
115+
return scales;
116+
}

0 commit comments

Comments
 (0)