Skip to main content

raps_cli/commands/
generate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4use clap::{Args, Subcommand};
5use colored::*;
6use rand::Rng;
7use std::fs::{self, File};
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11#[derive(Args)]
12pub struct GenerateArgs {
13    #[command(subcommand)]
14    pub command: GenerateCommands,
15}
16
17#[derive(Subcommand)]
18pub enum GenerateCommands {
19    /// Generate synthetic engineering files for testing
20    Files {
21        /// Number of each file type to generate
22        #[arg(short, long, default_value = "3")]
23        count: u32,
24
25        /// Output directory
26        #[arg(long = "out-dir", default_value = "./generated-files")]
27        out_dir: PathBuf,
28
29        /// Complexity level: simple, medium, complex
30        #[arg(long, default_value = "medium")]
31        complexity: String,
32    },
33}
34
35pub async fn execute(args: GenerateArgs) -> anyhow::Result<()> {
36    match args.command {
37        GenerateCommands::Files {
38            count,
39            out_dir,
40            complexity,
41        } => generate_files(count, out_dir, &complexity).await,
42    }
43}
44
45async fn generate_files(count: u32, output: PathBuf, complexity: &str) -> anyhow::Result<()> {
46    let settings = match complexity {
47        "simple" => ComplexitySettings {
48            vertices: 8,
49            elements: 50,
50            points: 1000,
51        },
52        "complex" => ComplexitySettings {
53            vertices: 200,
54            elements: 1000,
55            points: 100_000,
56        },
57        _ => ComplexitySettings {
58            vertices: 50,
59            elements: 200,
60            points: 10000,
61        }, // medium
62    };
63
64    println!(
65        "\n{}",
66        "╔══════════════════════════════════════════════════════════════╗".cyan()
67    );
68    println!(
69        "{}",
70        "║         Engineering Files Generator (Rust)                   ║".cyan()
71    );
72    println!(
73        "{}",
74        "╚══════════════════════════════════════════════════════════════╝".cyan()
75    );
76    println!("\nOutput:     {}", output.display());
77    println!("Count:      {} of each type", count);
78    println!(
79        "Complexity: {} (vertices: {}, elements: {})",
80        complexity, settings.vertices, settings.elements
81    );
82
83    // Create output directory
84    fs::create_dir_all(&output)?;
85
86    let mut stats = Stats::default();
87    let mut total_bytes: u64 = 0;
88
89    // Generate OBJ files
90    println!(
91        "\n{}",
92        "[1/6] Generating OBJ files (3D mesh geometry)...".yellow()
93    );
94    for i in 1..=count {
95        let (size, mtl_size) = generate_obj(&output, i, &settings)?;
96        total_bytes += size + mtl_size;
97        stats.obj += 1;
98        println!(
99            "  {} building-model-{}.obj ({:.1} KB)",
100            "✓".green(),
101            i,
102            size as f64 / 1024.0
103        );
104    }
105
106    // Generate DXF files
107    println!(
108        "\n{}",
109        "[2/6] Generating DXF files (AutoCAD drawings)...".yellow()
110    );
111    for i in 1..=count {
112        let size = generate_dxf(&output, i)?;
113        total_bytes += size;
114        stats.dxf += 1;
115        println!(
116            "  {} floorplan-{}.dxf ({:.1} KB)",
117            "✓".green(),
118            i,
119            size as f64 / 1024.0
120        );
121    }
122
123    // Generate STL files
124    println!(
125        "\n{}",
126        "[3/6] Generating STL files (3D printing meshes)...".yellow()
127    );
128    for i in 1..=count {
129        let size = generate_stl(&output, i)?;
130        total_bytes += size;
131        stats.stl += 1;
132        println!(
133            "  {} part-{}.stl ({:.1} KB)",
134            "✓".green(),
135            i,
136            size as f64 / 1024.0
137        );
138    }
139
140    // Generate IFC files
141    println!(
142        "\n{}",
143        "[4/6] Generating IFC files (BIM models)...".yellow()
144    );
145    for i in 1..=count {
146        let size = generate_ifc(&output, i, &settings)?;
147        total_bytes += size;
148        stats.ifc += 1;
149        println!(
150            "  {} building-{}.ifc ({:.1} KB)",
151            "✓".green(),
152            i,
153            size as f64 / 1024.0
154        );
155    }
156
157    // Generate JSON metadata
158    println!("\n{}", "[5/6] Generating JSON metadata files...".yellow());
159    for i in 1..=count {
160        let size = generate_json(&output, i, &settings)?;
161        total_bytes += size;
162        stats.json += 1;
163        println!(
164            "  {} project-{}-metadata.json ({:.1} KB)",
165            "✓".green(),
166            i,
167            size as f64 / 1024.0
168        );
169    }
170
171    // Generate point cloud files
172    println!("\n{}", "[6/6] Generating point cloud files...".yellow());
173    for i in 1..=count {
174        let size = generate_xyz(&output, i, &settings)?;
175        total_bytes += size;
176        stats.xyz += 1;
177        println!(
178            "  {} scan-{}.xyz ({:.1} KB)",
179            "✓".green(),
180            i,
181            size as f64 / 1024.0
182        );
183    }
184
185    // Summary
186    println!(
187        "\n{}",
188        "╔══════════════════════════════════════════════════════════════╗".cyan()
189    );
190    println!(
191        "{}",
192        "║                    Generation Complete                        ║".cyan()
193    );
194    println!(
195        "{}",
196        "╚══════════════════════════════════════════════════════════════╝".cyan()
197    );
198
199    println!("\n  Output: {}", fs::canonicalize(&output)?.display());
200    println!("\n  Files Generated:");
201    println!("    OBJ (3D mesh):     {} files", stats.obj);
202    println!("    DXF (AutoCAD):     {} files", stats.dxf);
203    println!("    STL (3D print):    {} files", stats.stl);
204    println!("    IFC (BIM):         {} files", stats.ifc);
205    println!("    JSON (metadata):   {} files", stats.json);
206    println!("    XYZ (point cloud): {} files", stats.xyz);
207    println!("    ────────────────────────────");
208    let total = stats.obj + stats.dxf + stats.stl + stats.ifc + stats.json + stats.xyz;
209    println!(
210        "    Total:             {} files ({:.1} KB)",
211        total,
212        total_bytes as f64 / 1024.0
213    );
214
215    println!("\n  {}", "Compatible with APS Translation:".green());
216    println!("    ✓ OBJ → SVF/SVF2 viewer format");
217    println!("    ✓ DXF → SVF/SVF2 viewer format");
218    println!("    ✓ STL → SVF/SVF2 viewer format");
219    println!("    ✓ IFC → SVF/SVF2 viewer format");
220
221    println!("\n{}", "=== Generation Complete ===".cyan());
222
223    Ok(())
224}
225
226struct ComplexitySettings {
227    vertices: u32,
228    elements: u32,
229    points: u32,
230}
231
232#[derive(Default)]
233struct Stats {
234    obj: u32,
235    dxf: u32,
236    stl: u32,
237    ifc: u32,
238    json: u32,
239    xyz: u32,
240}
241
242fn generate_obj(
243    output: &Path,
244    index: u32,
245    _settings: &ComplexitySettings,
246) -> anyhow::Result<(u64, u64)> {
247    let obj_path = output.join(format!("building-model-{}.obj", index));
248    let mtl_path = output.join(format!("building-model-{}.mtl", index));
249
250    let mut obj_content = format!(
251        "# APS Demo - Building Model {}\n# Generated by raps\n\nmtllib building-model-{}.mtl\n\n",
252        index, index
253    );
254
255    let mut vertex_offset = 0u32;
256
257    // Building components
258    let components = vec![
259        ("Foundation", 20.0, 1.0, 15.0, 0.0, -0.5, 0.0),
260        ("Floor1", 18.0, 3.0, 13.0, 0.0, 2.0, 0.0),
261        ("Floor2", 18.0, 3.0, 13.0, 0.0, 5.5, 0.0),
262        ("Roof", 20.0, 0.5, 15.0, 0.0, 7.5, 0.0),
263    ];
264
265    for (name, w, h, d, cx, cy, cz) in components {
266        obj_content.push_str(&format!("\no {}\nusemtl {}_material\n", name, name));
267
268        // Box vertices
269        let hw = w / 2.0;
270        let hh = h / 2.0;
271        let hd = d / 2.0;
272        let verts = vec![
273            (cx - hw, cy - hh, cz + hd),
274            (cx + hw, cy - hh, cz + hd),
275            (cx + hw, cy + hh, cz + hd),
276            (cx - hw, cy + hh, cz + hd),
277            (cx - hw, cy - hh, cz - hd),
278            (cx + hw, cy - hh, cz - hd),
279            (cx + hw, cy + hh, cz - hd),
280            (cx - hw, cy + hh, cz - hd),
281        ];
282
283        for (x, y, z) in &verts {
284            obj_content.push_str(&format!("v {:.6} {:.6} {:.6}\n", x, y, z));
285        }
286
287        // Faces (1-indexed, offset by vertex_offset)
288        let o = vertex_offset + 1;
289        obj_content.push_str(&format!("f {} {} {} {}\n", o, o + 1, o + 2, o + 3));
290        obj_content.push_str(&format!("f {} {} {} {}\n", o + 7, o + 6, o + 5, o + 4));
291        obj_content.push_str(&format!("f {} {} {} {}\n", o + 3, o + 2, o + 6, o + 7));
292        obj_content.push_str(&format!("f {} {} {} {}\n", o + 4, o + 5, o + 1, o));
293        obj_content.push_str(&format!("f {} {} {} {}\n", o + 1, o + 5, o + 6, o + 2));
294        obj_content.push_str(&format!("f {} {} {} {}\n", o + 4, o, o + 3, o + 7));
295
296        vertex_offset += 8;
297    }
298
299    // Write OBJ file
300    let mut obj_file = File::create(&obj_path)?;
301    obj_file.write_all(obj_content.as_bytes())?;
302
303    // Generate MTL file
304    let mtl_content = format!(
305        "# Material Library for building-model-{}.obj\n\n\
306        newmtl Foundation_material\nKd 0.5 0.5 0.5\nKa 0.1 0.1 0.1\n\n\
307        newmtl Floor1_material\nKd 0.8 0.8 0.7\nKa 0.1 0.1 0.1\n\n\
308        newmtl Floor2_material\nKd 0.8 0.8 0.7\nKa 0.1 0.1 0.1\n\n\
309        newmtl Roof_material\nKd 0.3 0.3 0.4\nKa 0.1 0.1 0.1\n",
310        index
311    );
312
313    let mut mtl_file = File::create(&mtl_path)?;
314    mtl_file.write_all(mtl_content.as_bytes())?;
315
316    Ok((obj_path.metadata()?.len(), mtl_path.metadata()?.len()))
317}
318
319fn generate_dxf(output: &Path, index: u32) -> anyhow::Result<u64> {
320    let mut rng = rand::thread_rng();
321    let width: f64 = rng.gen_range(20.0..40.0);
322    let height: f64 = rng.gen_range(15.0..30.0);
323    let rooms: u32 = rng.gen_range(3..8);
324
325    let path = output.join(format!("floorplan-{}.dxf", index));
326
327    let mut content = String::from(
328        "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$INSUNITS\n70\n4\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n",
329    );
330
331    // Outer walls
332    let walls = vec![
333        (0.0, 0.0, width, 0.0),
334        (width, 0.0, width, height),
335        (width, height, 0.0, height),
336        (0.0, height, 0.0, 0.0),
337    ];
338
339    for (x1, y1, x2, y2) in walls {
340        content.push_str(&format!(
341            "0\nLINE\n8\nWalls\n10\n{:.1}\n20\n{:.1}\n30\n0.0\n11\n{:.1}\n21\n{:.1}\n31\n0.0\n",
342            x1, y1, x2, y2
343        ));
344    }
345
346    // Room divisions
347    for r in 1..rooms {
348        let div_x = width * r as f64 / rooms as f64;
349        content.push_str(&format!(
350            "0\nLINE\n8\nInterior_Walls\n10\n{:.1}\n20\n0.0\n30\n0.0\n11\n{:.1}\n21\n{:.1}\n31\n0.0\n",
351            div_x, div_x, height
352        ));
353    }
354
355    // Doors
356    for d in 0..rooms {
357        let door_x = width * (d as f64 + 0.5) / rooms as f64;
358        content.push_str(&format!(
359            "0\nCIRCLE\n8\nDoors\n10\n{:.1}\n20\n0.5\n30\n0.0\n40\n0.8\n",
360            door_x
361        ));
362    }
363
364    // Dimension text
365    content.push_str(&format!(
366        "0\nTEXT\n8\nDimensions\n10\n{:.1}\n20\n-2.0\n30\n0.0\n40\n1.0\n1\n{:.0}m x {:.0}m\n",
367        width / 2.0,
368        width,
369        height
370    ));
371
372    content.push_str("0\nENDSEC\n0\nEOF\n");
373
374    let mut file = File::create(&path)?;
375    file.write_all(content.as_bytes())?;
376
377    Ok(path.metadata()?.len())
378}
379
380fn generate_stl(output: &Path, index: u32) -> anyhow::Result<u64> {
381    let mut rng = rand::thread_rng();
382    let scale: f64 = rng.gen_range(10.0..30.0);
383
384    let path = output.join(format!("part-{}.stl", index));
385
386    let content = format!(
387        "solid Part_{}\n\
388          facet normal 0 0 1\n    outer loop\n      vertex 0 0 {s}\n      vertex {s} 0 {s}\n      vertex {s} {s} {s}\n    endloop\n  endfacet\n\
389          facet normal 0 0 1\n    outer loop\n      vertex 0 0 {s}\n      vertex {s} {s} {s}\n      vertex 0 {s} {s}\n    endloop\n  endfacet\n\
390          facet normal 0 0 -1\n    outer loop\n      vertex 0 0 0\n      vertex {s} {s} 0\n      vertex {s} 0 0\n    endloop\n  endfacet\n\
391          facet normal 0 0 -1\n    outer loop\n      vertex 0 0 0\n      vertex 0 {s} 0\n      vertex {s} {s} 0\n    endloop\n  endfacet\n\
392          facet normal 0 -1 0\n    outer loop\n      vertex 0 0 0\n      vertex {s} 0 0\n      vertex {s} 0 {s}\n    endloop\n  endfacet\n\
393          facet normal 0 -1 0\n    outer loop\n      vertex 0 0 0\n      vertex {s} 0 {s}\n      vertex 0 0 {s}\n    endloop\n  endfacet\n\
394          facet normal 0 1 0\n    outer loop\n      vertex 0 {s} 0\n      vertex {s} {s} {s}\n      vertex {s} {s} 0\n    endloop\n  endfacet\n\
395          facet normal 0 1 0\n    outer loop\n      vertex 0 {s} 0\n      vertex 0 {s} {s}\n      vertex {s} {s} {s}\n    endloop\n  endfacet\n\
396          facet normal -1 0 0\n    outer loop\n      vertex 0 0 0\n      vertex 0 {s} {s}\n      vertex 0 {s} 0\n    endloop\n  endfacet\n\
397          facet normal -1 0 0\n    outer loop\n      vertex 0 0 0\n      vertex 0 0 {s}\n      vertex 0 {s} {s}\n    endloop\n  endfacet\n\
398          facet normal 1 0 0\n    outer loop\n      vertex {s} 0 0\n      vertex {s} {s} 0\n      vertex {s} {s} {s}\n    endloop\n  endfacet\n\
399          facet normal 1 0 0\n    outer loop\n      vertex {s} 0 0\n      vertex {s} {s} {s}\n      vertex {s} 0 {s}\n    endloop\n  endfacet\n\
400        endsolid Part_{}\n",
401        index,
402        index,
403        s = scale
404    );
405
406    let mut file = File::create(&path)?;
407    file.write_all(content.as_bytes())?;
408
409    Ok(path.metadata()?.len())
410}
411
412fn generate_ifc(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
413    let mut rng = rand::thread_rng();
414    let path = output.join(format!("building-{}.ifc", index));
415
416    let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string();
417    let project_guid = generate_ifc_guid();
418    let site_guid = generate_ifc_guid();
419    let building_guid = generate_ifc_guid();
420
421    let mut content = format!(
422        "ISO-10303-21;\n\
423        HEADER;\n\
424        FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0]'),'2;1');\n\
425        FILE_NAME('building-{}.ifc','{}',('RAPS Generator'),('Demo Organization'),'IFC4','raps','');\n\
426        FILE_SCHEMA(('IFC4'));\n\
427        ENDSEC;\n\n\
428        DATA;\n\
429        #1=IFCPROJECT('{}',#2,'Building Project {}','Demo building model',`$,`$,`$,(#7),#11);\n\
430        #2=IFCOWNERHISTORY(#3,#6,`$,.NOCHANGE.,`$,`$,`$,1234567890);\n\
431        #3=IFCPERSONANDORGANIZATION(#4,#5,`$);\n\
432        #4=IFCPERSON(`$,'Generator','CLI',`$,`$,`$,`$,`$);\n\
433        #5=IFCORGANIZATION(`$,'Demo Corp','Demo Organization',`$,`$);\n\
434        #6=IFCAPPLICATION(#5,'1.0','APS CLI Generator','APSCLI');\n\
435        #7=IFCGEOMETRICREPRESENTATIONCONTEXT(`$,'Model',3,1.E-05,#8,#9);\n\
436        #8=IFCAXIS2PLACEMENT3D(#10,`$,`$);\n\
437        #9=IFCDIRECTION((0.,1.,0.));\n\
438        #10=IFCCARTESIANPOINT((0.,0.,0.));\n\
439        #11=IFCUNITASSIGNMENT((#12,#13,#14,#15));\n\
440        #12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n\
441        #13=IFCSIUNIT(*,.AREAUNIT.,`$,.SQUARE_METRE.);\n\
442        #14=IFCSIUNIT(*,.VOLUMEUNIT.,`$,.CUBIC_METRE.);\n\
443        #15=IFCSIUNIT(*,.PLANEANGLEUNIT.,`$,.RADIAN.);\n\n\
444        #20=IFCSITE('{}',#2,'Site','Building site',`$,#21,`$,`$,.ELEMENT.,`$,`$,`$,`$,`$);\n\
445        #21=IFCLOCALPLACEMENT(`$,#8);\n\n\
446        #30=IFCBUILDING('{}',#2,'Building {}','Main building',`$,#31,`$,`$,.ELEMENT.,`$,`$,`$);\n\
447        #31=IFCLOCALPLACEMENT(#21,#8);\n\n\
448        #40=IFCRELAGGREGATES('{}',#2,`$,`$,#1,(#20));\n\
449        #41=IFCRELAGGREGATES('{}',#2,`$,`$,#20,(#30));\n\n",
450        index,
451        timestamp,
452        project_guid,
453        index,
454        site_guid,
455        building_guid,
456        index,
457        generate_ifc_guid(),
458        generate_ifc_guid()
459    );
460
461    // Add storeys and elements
462    let storey_count = rng.gen_range(2..5);
463    let mut entity_id = 100u32;
464
465    let ifc_categories = [
466        "IfcWall",
467        "IfcDoor",
468        "IfcWindow",
469        "IfcSlab",
470        "IfcColumn",
471        "IfcBeam",
472    ];
473
474    for s in 0..storey_count {
475        let elevation = s * 3000;
476        content.push_str(&format!(
477            "#{}=IFCBUILDINGSTOREY('{}',#2,'Level {}','Storey at {}mm',`$,#{},`$,`$,.ELEMENT.,{}.0);\n\
478            #{}=IFCLOCALPLACEMENT(#31,#8);\n",
479            entity_id, generate_ifc_guid(), s + 1, elevation, entity_id + 1, elevation, entity_id + 1
480        ));
481        entity_id += 2;
482    }
483
484    // Add elements
485    let _elements_per_storey = settings.elements / storey_count;
486    for _ in 0..settings.elements {
487        let cat = ifc_categories[rng.gen_range(0..ifc_categories.len())];
488        content.push_str(&format!(
489            "#{}={}('{}',#2,'{}_{}',' ',`$,`$,`$,`$);\n",
490            entity_id,
491            cat,
492            generate_ifc_guid(),
493            cat,
494            entity_id
495        ));
496        entity_id += 1;
497    }
498
499    content.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
500
501    let mut file = File::create(&path)?;
502    file.write_all(content.as_bytes())?;
503
504    Ok(path.metadata()?.len())
505}
506
507fn generate_json(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
508    let mut rng = rand::thread_rng();
509    let path = output.join(format!("project-{}-metadata.json", index));
510
511    let categories = [
512        "Walls", "Doors", "Windows", "Floors", "Ceilings", "Columns", "Beams",
513    ];
514    let levels = ["Basement", "Level 1", "Level 2", "Level 3", "Roof"];
515    let materials = ["Concrete", "Steel", "Wood", "Glass", "Aluminum", "Brick"];
516
517    let mut elements = Vec::new();
518    let mut total_area = 0.0f64;
519    let mut total_volume = 0.0f64;
520
521    for i in 1..=settings.elements {
522        let area: f64 = rng.gen_range(1.0..500.0);
523        let volume: f64 = rng.gen_range(1.0..200.0);
524        total_area += area;
525        total_volume += volume;
526
527        elements.push(serde_json::json!({
528            "dbId": i,
529            "externalId": uuid::Uuid::new_v4().to_string(),
530            "name": format!("{}_{}", categories[rng.gen_range(0..categories.len())], i),
531            "category": categories[rng.gen_range(0..categories.len())],
532            "level": levels[rng.gen_range(0..levels.len())],
533            "material": materials[rng.gen_range(0..materials.len())],
534            "geometry": {
535                "area": (area * 100.0).round() / 100.0,
536                "volume": (volume * 100.0).round() / 100.0,
537            },
538            "visible": rng.gen_bool(0.9),
539        }));
540    }
541
542    let metadata = serde_json::json!({
543        "projectInfo": {
544            "id": uuid::Uuid::new_v4().to_string(),
545            "name": format!("Demo Project {}", index),
546            "number": format!("PRJ-{:04}", rng.gen_range(1000..9999)),
547        },
548        "modelInfo": {
549            "version": format!("2024.{}", index),
550            "units": "millimeters",
551        },
552        "statistics": {
553            "totalElements": settings.elements,
554            "totalArea": (total_area * 100.0).round() / 100.0,
555            "totalVolume": (total_volume * 100.0).round() / 100.0,
556        },
557        "elements": elements,
558    });
559
560    let mut file = File::create(&path)?;
561    file.write_all(serde_json::to_string_pretty(&metadata)?.as_bytes())?;
562
563    Ok(path.metadata()?.len())
564}
565
566fn generate_xyz(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
567    let mut rng = rand::thread_rng();
568    let path = output.join(format!("scan-{}.xyz", index));
569
570    let mut content = format!(
571        "# XYZ Point Cloud - Scan {}\n# Points: {}\n# Format: X Y Z R G B Intensity\n",
572        index, settings.points
573    );
574
575    for _ in 0..settings.points {
576        // Generate points on building surfaces
577        let surface = rng.gen_range(0..6);
578        let (x, y, z) = match surface {
579            0 => (rng.gen_range(-20.0..20.0), -10.0, rng.gen_range(0.0..10.0)),
580            1 => (rng.gen_range(-20.0..20.0), 10.0, rng.gen_range(0.0..10.0)),
581            2 => (-20.0, rng.gen_range(-10.0..10.0), rng.gen_range(0.0..10.0)),
582            3 => (20.0, rng.gen_range(-10.0..10.0), rng.gen_range(0.0..10.0)),
583            4 => (rng.gen_range(-20.0..20.0), rng.gen_range(-10.0..10.0), 0.0),
584            _ => (rng.gen_range(-20.0..20.0), rng.gen_range(-10.0..10.0), 10.0),
585        };
586
587        // Add noise
588        let x = x + rng.gen_range(-0.5..0.5);
589        let y = y + rng.gen_range(-0.5..0.5);
590        let z = z + rng.gen_range(-0.5..0.5);
591
592        let r: u8 = rng.gen_range(100..200);
593        let g: u8 = rng.gen_range(100..200);
594        let b: u8 = rng.gen_range(100..200);
595        let intensity: f64 = rng.gen_range(0.5..1.0);
596
597        content.push_str(&format!(
598            "{:.3} {:.3} {:.3} {} {} {} {:.2}\n",
599            x, y, z, r, g, b, intensity
600        ));
601    }
602
603    let mut file = File::create(&path)?;
604    file.write_all(content.as_bytes())?;
605
606    Ok(path.metadata()?.len())
607}
608
609fn generate_ifc_guid() -> String {
610    let uuid = uuid::Uuid::new_v4();
611    let bytes = uuid.as_bytes();
612    let mut result = String::with_capacity(22);
613
614    const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
615
616    for i in 0..22 {
617        let idx = (bytes[i % 16] as usize + i) % 64;
618        result.push(CHARS[idx] as char);
619    }
620
621    result
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627
628    #[test]
629    fn test_generate_ifc_guid_format() {
630        let guid = generate_ifc_guid();
631
632        // IFC GUIDs should be exactly 22 characters
633        assert_eq!(guid.len(), 22);
634
635        // Should only contain valid IFC GUID characters
636        const VALID_CHARS: &str =
637            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
638        for ch in guid.chars() {
639            assert!(
640                VALID_CHARS.contains(ch),
641                "Invalid character in IFC GUID: {}",
642                ch
643            );
644        }
645    }
646
647    #[test]
648    fn test_generate_ifc_guid_uniqueness() {
649        // Generate multiple GUIDs and verify they're different
650        let guid1 = generate_ifc_guid();
651        let guid2 = generate_ifc_guid();
652        let guid3 = generate_ifc_guid();
653
654        assert_ne!(guid1, guid2);
655        assert_ne!(guid2, guid3);
656        assert_ne!(guid1, guid3);
657    }
658
659    #[test]
660    fn test_generate_ifc_guid_multiple_calls() {
661        // Generate 100 GUIDs to ensure they're all valid
662        for _ in 0..100 {
663            let guid = generate_ifc_guid();
664            assert_eq!(guid.len(), 22);
665        }
666    }
667}