1+ using System . Globalization ;
2+ using System . Text ;
3+ using NEXUS . Parsers . Ovito . Models . XYZFile ;
4+
5+ namespace NEXUS . Parsers . BCR . Helpers ;
6+
7+ public static class XyzToBcrConverter
8+ {
9+ public static BcrFile Convert ( this XyzFile xyzFile , double pixelSize = 0.5 , double atomRadius = 1.34 ,
10+ double baseZ = - 10.0 , string unit = "nm" , BcrParser . BcrDataType dataType = BcrParser . BcrDataType . Float32 )
11+ {
12+ if ( xyzFile == null || xyzFile . Particles . Count == 0 )
13+ throw new ArgumentException ( "XYZ файл пустой или невалидный." ) ;
14+
15+ // Определяем bounding box
16+ double minX = xyzFile . Particles . Min ( p => p . X ) ;
17+ double maxX = xyzFile . Particles . Max ( p => p . X ) ;
18+ double minY = xyzFile . Particles . Min ( p => p . Y ) ;
19+ double maxY = xyzFile . Particles . Max ( p => p . Y ) ;
20+
21+ int xPixels = ( int ) Math . Ceiling ( ( maxX - minX ) / pixelSize ) + 1 ;
22+ int yPixels = ( int ) Math . Ceiling ( ( maxY - minY ) / pixelSize ) + 1 ;
23+
24+ double [ , ] data = new double [ yPixels , xPixels ] ;
25+ bool [ , ] voidMask = new bool [ yPixels , xPixels ] ;
26+
27+ // ИЗМЕНЕНИЕ: Подложка всегда имеет высоту baseZ, voidMask = false везде!
28+ for ( int j = 0 ; j < yPixels ; j ++ )
29+ {
30+ for ( int i = 0 ; i < xPixels ; i ++ )
31+ {
32+ data [ j , i ] = baseZ ; // Подложка по умолчанию
33+ voidMask [ j , i ] = false ; // ВАЖНО: НЕ void!
34+ }
35+ }
36+
37+ // Для каждого пикселя вычисляем высоту от атомов (перекрывает подложку)
38+ Parallel . For ( 0 , yPixels , j => // Параллелизация для скорости
39+ {
40+ double gridY = minY + j * pixelSize ;
41+ for ( int i = 0 ; i < xPixels ; i ++ )
42+ {
43+ double gridX = minX + i * pixelSize ;
44+ double maxHeight = baseZ ;
45+
46+ foreach ( var atom in xyzFile . Particles )
47+ {
48+ double distXY = Math . Sqrt ( Math . Pow ( gridX - atom . X , 2 ) + Math . Pow ( gridY - atom . Y , 2 ) ) ;
49+ if ( distXY <= atomRadius ) // <= вместо < для полного покрытия
50+ {
51+ double heightContribution =
52+ atom . Z + Math . Sqrt ( Math . Pow ( atomRadius , 2 ) - Math . Pow ( distXY , 2 ) ) ;
53+ if ( heightContribution > maxHeight )
54+ {
55+ maxHeight = heightContribution ;
56+ }
57+ }
58+ }
59+
60+ data [ j , i ] = maxHeight ; // Атомы перекрывают подложку
61+ }
62+ } ) ;
63+
64+ // Нормализация (сдвигаем min Z к 0)
65+ double globalMinZ = data . Cast < double > ( ) . Min ( ) ;
66+ for ( int j = 0 ; j < yPixels ; j ++ )
67+ {
68+ for ( int i = 0 ; i < xPixels ; i ++ )
69+ {
70+ data [ j , i ] -= globalMinZ ; // Подложка теперь на Z=0
71+ }
72+ }
73+
74+ var metadata = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase )
75+ {
76+ { "fileformat" , dataType == BcrParser . BcrDataType . Int16 ? "bcrstm" : "bcrf" } ,
77+ { "xpixels" , xPixels . ToString ( ) } ,
78+ { "ypixels" , yPixels . ToString ( ) } ,
79+ { "zunit" , unit } ,
80+ { "xlength" , ( maxX - minX + pixelSize ) . ToString ( "F3" , CultureInfo . InvariantCulture ) } ,
81+ { "ylength" , ( maxY - minY + pixelSize ) . ToString ( "F3" , CultureInfo . InvariantCulture ) } ,
82+ { "zmin" , "0" }
83+ } ;
84+
85+ if ( dataType == BcrParser . BcrDataType . Int16 )
86+ {
87+ metadata [ "bit2nm" ] = "1.0" ;
88+ }
89+
90+ // Добавляем комментарий о подложке
91+ metadata [ "comment" ] = $ "Generated from XYZ with substrate at Z={ baseZ } and atom radius={ atomRadius } ";
92+
93+ return new BcrFile
94+ {
95+ XPixels = xPixels ,
96+ YPixels = yPixels ,
97+ Data = data ,
98+ VoidMask = voidMask , // Все false - вся поверхность валидна!
99+ Metadata = metadata
100+ } ;
101+ }
102+
103+ /// <summary>
104+ /// Сохраняет BcrFile в файл (реализация Save для BCR).
105+ /// </summary>
106+ /// <param name="bcrFile">Объект BcrFile.</param>
107+ /// <param name="filePath">Путь к файлу.</param>
108+ public static void SaveBcrFile ( BcrFile bcrFile , string filePath )
109+ {
110+ if ( bcrFile == null )
111+ throw new ArgumentNullException ( nameof ( bcrFile ) ) ;
112+
113+ bool isInt16 = bcrFile . Metadata [ "fileformat" ] . Contains ( "stm" ) ;
114+ double scale = ParseUnitToScale ( bcrFile . Metadata . GetValueOrDefault ( "zunit" , "nm" ) ) ;
115+ double bit2nm = 1.0 ;
116+ if ( isInt16 && bcrFile . Metadata . TryGetValue ( "bit2nm" , out var bitStr ) )
117+ double . TryParse ( bitStr , out bit2nm ) ;
118+
119+ // Подготавливаем заголовок
120+ var header = new StringBuilder ( ) ;
121+ foreach ( var kvp in bcrFile . Metadata )
122+ {
123+ header . AppendLine ( $ "{ kvp . Key } = { kvp . Value } ") ;
124+ }
125+
126+ // Добавляем zmin если нужно
127+ double minZ = bcrFile . Data . Cast < double > ( ) . Min ( ) ;
128+ header . AppendLine ( $ "zmin = { minZ . ToString ( CultureInfo . InvariantCulture ) } ") ;
129+
130+ byte [ ] headerBytes = Encoding . ASCII . GetBytes ( header . ToString ( ) ) ;
131+ Array . Resize ( ref headerBytes , 2048 ) ; // Фиксированный размер 2048 байт, заполняем нулями если меньше
132+
133+ // Подготавливаем данные
134+ using var ms = new MemoryStream ( ) ;
135+ using var bw = new BinaryWriter ( ms ) ;
136+
137+ for ( int y = 0 ; y < bcrFile . YPixels ; y ++ )
138+ {
139+ for ( int x = 0 ; x < bcrFile . XPixels ; x ++ )
140+ {
141+ double val = bcrFile . Data [ y , x ] / scale ;
142+ if ( bcrFile . VoidMask [ y , x ] )
143+ {
144+ if ( isInt16 ) bw . Write ( ( short ) 32767 ) ;
145+ else bw . Write ( 1.7e38f ) ;
146+ }
147+ else
148+ {
149+ if ( isInt16 )
150+ {
151+ short scaledVal = ( short ) ( val / bit2nm ) ;
152+ bw . Write ( scaledVal ) ;
153+ }
154+ else
155+ {
156+ bw . Write ( ( float ) val ) ;
157+ }
158+ }
159+ }
160+ }
161+
162+ // Сохраняем файл
163+ using var fs = new FileStream ( filePath , FileMode . Create ) ;
164+ fs . Write ( headerBytes , 0 , headerBytes . Length ) ;
165+ fs . Write ( ms . ToArray ( ) , 0 , ( int ) ms . Length ) ;
166+ }
167+
168+ private static double ParseUnitToScale ( string unit )
169+ {
170+ unit = unit . Trim ( ) . ToLowerInvariant ( ) ;
171+ return unit switch
172+ {
173+ "nm" => 1.0 ,
174+ "um" => 1e3 ,
175+ "mm" => 1e6 ,
176+ "m" => 1e9 ,
177+ _ => 1.0
178+ } ;
179+ }
180+ }
0 commit comments