Warning, /firebird/firebird-ng/src/app/utils/three-geometry-editor.spec.ts is written in an unsupported language. File is not indexed.
0001 import * as THREE from 'three';
0002 import { editThreeNodeContent, EditThreeNodeRule, clearGeometryEditingFlags } from './three-geometry-editor';
0003 import { Mesh, BoxGeometry, MeshBasicMaterial, Group, Object3D } from 'three';
0004
0005 /**
0006 * Helper to create a simple test mesh
0007 */
0008 function createTestMesh(name: string): Mesh {
0009 const geometry = new BoxGeometry(1, 1, 1);
0010 const material = new MeshBasicMaterial({ color: 0xffffff });
0011 const mesh = new Mesh(geometry, material);
0012 mesh.name = name;
0013 return mesh;
0014 }
0015
0016 /**
0017 * Helper to create a test node structure like BeamPipe
0018 */
0019 function createBeamPipeStructure(): Group {
0020 const root = new Group();
0021 root.name = 'BeamPipe_assembly';
0022
0023 // Add v_upstream meshes
0024 const upstream1 = createTestMesh('v_upstream_wall_1');
0025 const upstream2 = createTestMesh('v_upstream_wall_2');
0026
0027 // Add other meshes
0028 const downstream1 = createTestMesh('v_downstream_section_1');
0029 const downstream2 = createTestMesh('v_downstream_section_2');
0030 const center = createTestMesh('center_pipe');
0031
0032 root.add(upstream1, upstream2, downstream1, downstream2, center);
0033
0034 return root;
0035 }
0036
0037 /**
0038 * Helper to create a hierarchical test node structure
0039 * BeamPipe_assembly
0040 * └── v_upstream_coating (Group)
0041 * ├── Left (Mesh)
0042 * └── Right (Mesh)
0043 * └── v_downstream_section (Mesh)
0044 * └── center_pipe (Mesh)
0045 */
0046 function createHierarchicalBeamPipe(): Group {
0047 const root = new Group();
0048 root.name = 'BeamPipe_assembly';
0049
0050 // v_upstream_coating group with child meshes
0051 const upstreamGroup = new Group();
0052 upstreamGroup.name = 'v_upstream_coating';
0053 upstreamGroup.add(createTestMesh('Left'));
0054 upstreamGroup.add(createTestMesh('Right'));
0055
0056 // Other meshes at root level
0057 const downstream = createTestMesh('v_downstream_section');
0058 const center = createTestMesh('center_pipe');
0059
0060 root.add(upstreamGroup, downstream, center);
0061
0062 return root;
0063 }
0064
0065 /**
0066 * Helper to create a structure where parent Group and child Mesh have same name pattern
0067 * This simulates real detector geometry where naming can be like:
0068 * BeamPipe_assembly
0069 * └── v_upstream_coating (Group)
0070 * └── v_upstream_coating (Mesh with same name as parent!)
0071 * └── other_pipe (Mesh)
0072 */
0073 function createSameNameHierarchy(): Group {
0074 const root = new Group();
0075 root.name = 'BeamPipe_assembly';
0076
0077 // Group and its child mesh have the same name - this is valid in Three.js
0078 const upstreamGroup = new Group();
0079 upstreamGroup.name = 'v_upstream_coating';
0080 const upstreamMesh = createTestMesh('v_upstream_coating'); // Same name as parent!
0081 upstreamGroup.add(upstreamMesh);
0082
0083 const otherPipe = createTestMesh('other_pipe');
0084
0085 root.add(upstreamGroup, otherPipe);
0086
0087 return root;
0088 }
0089
0090 /**
0091 * Count meshes matching a pattern in the tree
0092 */
0093 function countMeshesWithPattern(root: Object3D, pattern: RegExp): number {
0094 let count = 0;
0095 root.traverse((child) => {
0096 if (child instanceof Mesh && pattern.test(child.name)) {
0097 count++;
0098 }
0099 });
0100 return count;
0101 }
0102
0103 /**
0104 * Count all objects with geometry (meshes and line segments)
0105 */
0106 function countObjectsWithGeometry(root: Object3D): number {
0107 let count = 0;
0108 root.traverse((child) => {
0109 if ((child as any).geometry) {
0110 count++;
0111 }
0112 });
0113 return count;
0114 }
0115
0116 /**
0117 * Get all object names in tree
0118 */
0119 function getAllNames(root: Object3D): string[] {
0120 const names: string[] = [];
0121 root.traverse((child) => {
0122 if (child.name) {
0123 names.push(child.name);
0124 }
0125 });
0126 return names;
0127 }
0128
0129 describe('three-geometry-editor', () => {
0130
0131 describe('clearGeometryEditingFlags', () => {
0132 it('should clear geometryEditingSkipRules flag on all nodes', () => {
0133 const root = createBeamPipeStructure();
0134
0135 // Set flags on some nodes
0136 root.traverse((child) => {
0137 child.userData['geometryEditingSkipRules'] = true;
0138 });
0139
0140 // Verify flags are set
0141 let flaggedCount = 0;
0142 root.traverse((child) => {
0143 if (child.userData['geometryEditingSkipRules']) flaggedCount++;
0144 });
0145 expect(flaggedCount).toBeGreaterThan(0);
0146
0147 // Clear flags
0148 clearGeometryEditingFlags(root);
0149
0150 // Verify all flags are cleared
0151 root.traverse((child) => {
0152 expect(child.userData['geometryEditingSkipRules']).toBeUndefined();
0153 });
0154 });
0155 });
0156
0157 describe('editThreeNodeContent with merge=false', () => {
0158
0159 it('should process only meshes matching pattern when pattern is provided', () => {
0160 const root = createBeamPipeStructure();
0161
0162 const rule: EditThreeNodeRule = {
0163 patterns: ['**/v_upstream*'],
0164 color: 0xff0000,
0165 merge: false,
0166 outline: false
0167 };
0168
0169 editThreeNodeContent(root, rule);
0170
0171 // Check that v_upstream meshes have the new color
0172 root.traverse((child) => {
0173 if (child instanceof Mesh && child.name.includes('v_upstream')) {
0174 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0175 }
0176 });
0177 });
0178
0179 it('should set geometryEditingSkipRules flag on processed meshes', () => {
0180 const root = createBeamPipeStructure();
0181 clearGeometryEditingFlags(root);
0182
0183 const rule: EditThreeNodeRule = {
0184 patterns: ['**/v_upstream*'],
0185 color: 0xff0000,
0186 merge: false,
0187 outline: false
0188 };
0189
0190 editThreeNodeContent(root, rule);
0191
0192 // v_upstream meshes should have the flag set
0193 root.traverse((child) => {
0194 if (child instanceof Mesh && child.name.includes('v_upstream')) {
0195 expect(child.userData['geometryEditingSkipRules']).toBe(true);
0196 }
0197 });
0198
0199 // Other meshes should NOT have the flag
0200 root.traverse((child) => {
0201 if (child instanceof Mesh && !child.name.includes('v_upstream')) {
0202 expect(child.userData['geometryEditingSkipRules']).toBeUndefined();
0203 }
0204 });
0205 });
0206
0207 it('should skip meshes with geometryEditingSkipRules=true when processing without pattern', () => {
0208 const root = createBeamPipeStructure();
0209 clearGeometryEditingFlags(root);
0210
0211 // First rule: process v_upstream meshes
0212 const rule1: EditThreeNodeRule = {
0213 patterns: ['**/v_upstream*'],
0214 color: 0xff0000, // Red
0215 merge: false,
0216 outline: false
0217 };
0218 editThreeNodeContent(root, rule1);
0219
0220 // Second rule: "the rest" (no pattern)
0221 const rule2: EditThreeNodeRule = {
0222 color: 0x00ff00, // Green
0223 merge: false,
0224 outline: false
0225 };
0226 editThreeNodeContent(root, rule2);
0227
0228 // v_upstream meshes should still be red (not overwritten by rule2)
0229 root.traverse((child) => {
0230 if (child instanceof Mesh && child.name.includes('v_upstream')) {
0231 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0232 }
0233 });
0234
0235 // Other meshes should be green
0236 root.traverse((child) => {
0237 if (child instanceof Mesh && !child.name.includes('v_upstream') && !child.name.includes('outline')) {
0238 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0x00ff00);
0239 }
0240 });
0241 });
0242
0243 it('should not create outline of outline', () => {
0244 const root = createBeamPipeStructure();
0245 clearGeometryEditingFlags(root);
0246
0247 const initialCount = countObjectsWithGeometry(root);
0248
0249 // First rule: process v_upstream with outline
0250 const rule1: EditThreeNodeRule = {
0251 patterns: ['**/v_upstream*'],
0252 color: 0xff0000,
0253 merge: false,
0254 outline: true
0255 };
0256 editThreeNodeContent(root, rule1);
0257
0258 // Should have created 2 outlines (for 2 v_upstream meshes)
0259 const afterRule1Count = countObjectsWithGeometry(root);
0260 expect(afterRule1Count).toBe(initialCount + 2);
0261
0262 // Second rule: "the rest" with outline
0263 const rule2: EditThreeNodeRule = {
0264 color: 0x00ff00,
0265 merge: false,
0266 outline: true
0267 };
0268 editThreeNodeContent(root, rule2);
0269
0270 // Should have created outlines only for the 3 remaining original meshes
0271 // NOT for the outline objects created by rule1
0272 const afterRule2Count = countObjectsWithGeometry(root);
0273 expect(afterRule2Count).toBe(initialCount + 2 + 3); // 5 original + 2 outlines from rule1 + 3 outlines from rule2
0274
0275 // Verify no "outline_outline" objects exist
0276 const names = getAllNames(root);
0277 const doubleOutlines = names.filter(n => n.includes('outline_outline') || n.includes('_outline_outlin'));
0278 expect(doubleOutlines).toEqual([]);
0279 });
0280 });
0281
0282 describe('editThreeNodeContent with hierarchical structure', () => {
0283
0284 it('should apply style to descendants when applyToDescendants is true (default)', () => {
0285 const root = createHierarchicalBeamPipe();
0286 clearGeometryEditingFlags(root);
0287
0288 // Rule matching v_upstream_coating (a Group)
0289 const rule: EditThreeNodeRule = {
0290 patterns: ['**/v_upstream*'],
0291 color: 0xff0000, // Red
0292 merge: false,
0293 outline: false
0294 // applyToDescendants defaults to true
0295 };
0296
0297 editThreeNodeContent(root, rule);
0298
0299 // Children of v_upstream_coating should be red
0300 root.traverse((child) => {
0301 if (child instanceof Mesh && (child.name === 'Left' || child.name === 'Right')) {
0302 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0303 }
0304 });
0305 });
0306
0307 it('should skip descendants in "the rest" rule when parent was processed', () => {
0308 const root = createHierarchicalBeamPipe();
0309 clearGeometryEditingFlags(root);
0310
0311 // First rule: match v_upstream_coating
0312 const rule1: EditThreeNodeRule = {
0313 patterns: ['**/v_upstream_coating'],
0314 color: 0xff0000, // Red
0315 merge: false,
0316 outline: false
0317 };
0318 editThreeNodeContent(root, rule1);
0319
0320 // Second rule: "the rest" (no pattern)
0321 const rule2: EditThreeNodeRule = {
0322 color: 0x00ff00, // Green
0323 merge: false,
0324 outline: false
0325 };
0326 editThreeNodeContent(root, rule2);
0327
0328 // v_upstream_coating children (Left, Right) should still be red
0329 // They should not be overwritten by rule2 due to hierarchical skip
0330 root.traverse((child) => {
0331 if (child instanceof Mesh && (child.name === 'Left' || child.name === 'Right')) {
0332 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0333 }
0334 });
0335
0336 // Other meshes should be green
0337 root.traverse((child) => {
0338 if (child instanceof Mesh && child.name === 'center_pipe') {
0339 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0x00ff00);
0340 }
0341 });
0342 });
0343
0344 it('should handle parent and child with same name pattern without duplicates', () => {
0345 const root = createSameNameHierarchy();
0346 clearGeometryEditingFlags(root);
0347
0348 const initialCount = countObjectsWithGeometry(root);
0349 expect(initialCount).toBe(2); // v_upstream_coating mesh + other_pipe mesh
0350
0351 // Rule matching both the Group and its child Mesh (same name pattern)
0352 const rule: EditThreeNodeRule = {
0353 patterns: ['**/v_upstream*'],
0354 color: 0xff0000, // Red
0355 merge: false,
0356 outline: true
0357 };
0358
0359 editThreeNodeContent(root, rule);
0360
0361 // Should create only ONE outline (for the one mesh)
0362 const afterCount = countObjectsWithGeometry(root);
0363 expect(afterCount).toBe(initialCount + 1); // +1 outline for the mesh
0364
0365 // The mesh should be styled
0366 let styledMeshCount = 0;
0367 root.traverse((child) => {
0368 if (child instanceof Mesh && child.name === 'v_upstream_coating') {
0369 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0370 styledMeshCount++;
0371 }
0372 });
0373 expect(styledMeshCount).toBe(1); // Only one mesh with that name
0374 });
0375
0376 it('should skip child in "the rest" when parent was matched even with same name', () => {
0377 const root = createSameNameHierarchy();
0378 clearGeometryEditingFlags(root);
0379
0380 // First rule: match v_upstream pattern
0381 const rule1: EditThreeNodeRule = {
0382 patterns: ['**/v_upstream*'],
0383 color: 0xff0000, // Red
0384 merge: false,
0385 outline: false
0386 };
0387 editThreeNodeContent(root, rule1);
0388
0389 // Second rule: "the rest"
0390 const rule2: EditThreeNodeRule = {
0391 color: 0x00ff00, // Green
0392 merge: false,
0393 outline: false
0394 };
0395 editThreeNodeContent(root, rule2);
0396
0397 // v_upstream_coating mesh should still be red
0398 root.traverse((child) => {
0399 if (child instanceof Mesh && child.name === 'v_upstream_coating') {
0400 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0401 }
0402 });
0403
0404 // other_pipe should be green
0405 root.traverse((child) => {
0406 if (child instanceof Mesh && child.name === 'other_pipe') {
0407 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0x00ff00);
0408 }
0409 });
0410 });
0411
0412 it('should not apply to descendants when applyToDescendants is false', () => {
0413 const root = createHierarchicalBeamPipe();
0414 clearGeometryEditingFlags(root);
0415
0416 // Rule matching v_upstream_coating (a Group) with applyToDescendants=false
0417 const rule: EditThreeNodeRule = {
0418 patterns: ['**/v_upstream*'],
0419 color: 0xff0000, // Red
0420 merge: false,
0421 outline: false,
0422 applyToDescendants: false
0423 };
0424
0425 editThreeNodeContent(root, rule);
0426
0427 // When applyToDescendants is false and the matched node is a Group (not Mesh),
0428 // no styling is applied because Groups don't have geometry.
0429 // Children of v_upstream_coating (Left, Right) should NOT be red
0430 root.traverse((child) => {
0431 if (child instanceof Mesh && (child.name === 'Left' || child.name === 'Right')) {
0432 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xffffff); // Original color
0433 }
0434 });
0435
0436 // v_downstream_section also matches the pattern (**/v_upstream* doesn't match it - it's v_DOWNstream)
0437 // so it should remain original color
0438 root.traverse((child) => {
0439 if (child instanceof Mesh && child.name === 'v_downstream_section') {
0440 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xffffff); // Original color
0441 }
0442 });
0443 });
0444 });
0445
0446 describe('editThreeNodeContent with merge=true', () => {
0447
0448 it('should set geometryEditingSkipRules on merged mesh', () => {
0449 const root = createBeamPipeStructure();
0450 clearGeometryEditingFlags(root);
0451
0452 const rule: EditThreeNodeRule = {
0453 patterns: ['**/v_upstream*'],
0454 merge: true,
0455 outline: false,
0456 newName: 'merged_upstream'
0457 };
0458
0459 editThreeNodeContent(root, rule);
0460
0461 // Find the merged mesh
0462 let mergedMesh: Mesh | null = null;
0463 root.traverse((child) => {
0464 if (child instanceof Mesh && child.name === 'merged_upstream') {
0465 mergedMesh = child;
0466 }
0467 });
0468
0469 expect(mergedMesh).not.toBeNull();
0470 expect(mergedMesh!.userData['geometryEditingSkipRules']).toBe(true);
0471 });
0472
0473 it('should not re-process merged meshes in subsequent rules without pattern', () => {
0474 const root = createBeamPipeStructure();
0475 clearGeometryEditingFlags(root);
0476
0477 // First rule: merge v_upstream meshes
0478 const rule1: EditThreeNodeRule = {
0479 patterns: ['**/v_upstream*'],
0480 merge: true,
0481 outline: false,
0482 newName: 'merged_upstream',
0483 color: 0xff0000
0484 };
0485 editThreeNodeContent(root, rule1);
0486
0487 // Second rule: "the rest"
0488 const rule2: EditThreeNodeRule = {
0489 color: 0x00ff00,
0490 merge: false,
0491 outline: false
0492 };
0493 editThreeNodeContent(root, rule2);
0494
0495 // Merged mesh should still be red
0496 root.traverse((child) => {
0497 if (child instanceof Mesh && child.name === 'merged_upstream') {
0498 expect((child.material as MeshBasicMaterial).color.getHex()).toBe(0xff0000);
0499 }
0500 });
0501 });
0502 });
0503 });