maps4fs 2.8.2__py3-none-any.whl → 2.8.4__py3-none-any.whl

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.

Potentially problematic release.


This version of maps4fs might be problematic. Click here for more details.

@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
  import os
7
7
  import shutil
8
8
  from copy import deepcopy
9
- from typing import Any
9
+ from typing import Any, Literal
10
10
 
11
11
  import cv2
12
12
  import numpy as np
@@ -57,6 +57,14 @@ class Background(MeshComponent, ImageComponent):
57
57
  os.makedirs(self.background_directory, exist_ok=True)
58
58
  os.makedirs(self.water_directory, exist_ok=True)
59
59
 
60
+ self.textured_mesh_directory = os.path.join(self.background_directory, "textured_mesh")
61
+ os.makedirs(self.textured_mesh_directory, exist_ok=True)
62
+
63
+ self.assets_background_directory = os.path.join(self.map.assets_directory, "background")
64
+ self.assets_water_directory = os.path.join(self.map.assets_directory, "water")
65
+ os.makedirs(self.assets_background_directory, exist_ok=True)
66
+ os.makedirs(self.assets_water_directory, exist_ok=True)
67
+
60
68
  self.water_resources_path = os.path.join(self.water_directory, "water_resources.png")
61
69
 
62
70
  self.output_path = os.path.join(self.background_directory, f"{Parameters.FULL}.png")
@@ -110,8 +118,28 @@ class Background(MeshComponent, ImageComponent):
110
118
 
111
119
  if self.map.background_settings.generate_background:
112
120
  self.generate_obj_files()
121
+ if self.game.mesh_processing:
122
+ self.logger.debug("Mesh processing is enabled, will decimate, texture and convert.")
123
+ self.decimate_background_mesh()
124
+ self.texture_background_mesh()
125
+ background_conversion_result = self.convert_background_mesh_to_i3d()
126
+ if background_conversion_result:
127
+ self.add_note_file(asset="background")
128
+ else:
129
+ self.logger.warning(
130
+ "Mesh processing is disabled for the game, skipping background mesh processing."
131
+ )
113
132
  if self.map.background_settings.generate_water:
114
133
  self.generate_water_resources_obj()
134
+ if self.game.mesh_processing:
135
+ self.logger.debug("Mesh processing is enabled, will convert water mesh to i3d.")
136
+ water_conversion_result = self.convert_water_mesh_to_i3d()
137
+ if water_conversion_result:
138
+ self.add_note_file(asset="water")
139
+ else:
140
+ self.logger.warning(
141
+ "Mesh processing is disabled for the game, skipping water mesh processing."
142
+ )
115
143
 
116
144
  def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
117
145
  """Creates foundations for buildings based on the DEM data.
@@ -253,6 +281,257 @@ class Background(MeshComponent, ImageComponent):
253
281
  remove_center=self.map.background_settings.remove_center,
254
282
  )
255
283
 
284
+ @staticmethod
285
+ def get_decimate_factor(map_size: int) -> float:
286
+ """Returns the decimation factor based on the map size.
287
+
288
+ Arguments:
289
+ map_size (int): The size of the map in pixels.
290
+
291
+ Raises:
292
+ ValueError: If the map size is too large for decimation.
293
+
294
+ Returns:
295
+ float -- The decimation factor.
296
+ """
297
+ thresholds = {
298
+ 2048: 0.1,
299
+ 4096: 0.05,
300
+ 8192: 0.025,
301
+ 16384: 0.0125,
302
+ }
303
+ for threshold, factor in thresholds.items():
304
+ if map_size <= threshold:
305
+ return factor
306
+ raise ValueError(
307
+ "Map size is too large for decimation, perform manual decimation in Blender."
308
+ )
309
+
310
+ @staticmethod
311
+ def get_background_texture_resolution(map_size: int) -> int:
312
+ """Returns the background texture resolution based on the map size.
313
+
314
+ Arguments:
315
+ map_size (int): The size of the map in pixels.
316
+
317
+ Returns:
318
+ int -- The background texture resolution.
319
+ """
320
+ resolutions = {
321
+ 2048: 2048,
322
+ 4096: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
323
+ 8192: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
324
+ 16384: Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE,
325
+ }
326
+ for threshold, resolution in resolutions.items():
327
+ if map_size <= threshold:
328
+ return resolution
329
+ return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
330
+
331
+ def decimate_background_mesh(self) -> None:
332
+ """ ""Decimates the background mesh based on the map size."""
333
+ if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
334
+ self.logger.warning("Background mesh not found, cannot generate i3d background.")
335
+ return
336
+
337
+ try:
338
+ mesh = trimesh.load_mesh(self.assets.background_mesh, force="mesh")
339
+ except Exception as e:
340
+ self.logger.error("Could not load background mesh: %s", e)
341
+ return
342
+
343
+ try:
344
+ decimate_factor = self.get_decimate_factor(self.map_size)
345
+ except ValueError as e:
346
+ self.logger.error("Could not determine decimation factor: %s", e)
347
+ return
348
+
349
+ decimated_save_path = os.path.join(
350
+ self.background_directory, f"{Parameters.DECIMATED_BACKGROUND}.obj"
351
+ )
352
+ try:
353
+ self.logger.debug("Decimating background mesh with factor %s.", decimate_factor)
354
+ decimated_mesh = self.decimate_mesh(mesh, decimate_factor)
355
+ self.logger.debug("Decimation completed.")
356
+ except Exception as e:
357
+ self.logger.error("Could not decimate background mesh: %s", e)
358
+ return
359
+
360
+ decimated_mesh.export(decimated_save_path)
361
+ self.logger.debug("Decimated background mesh saved: %s", decimated_save_path)
362
+
363
+ self.assets.decimated_background_mesh = decimated_save_path
364
+
365
+ def texture_background_mesh(self) -> None:
366
+ """Textures the background mesh using satellite imagery."""
367
+ satellite_component = self.map.get_satellite_component()
368
+ if not satellite_component:
369
+ self.logger.warning("Satellite component not found, cannot texture background mesh.")
370
+ return
371
+
372
+ background_texture_path = satellite_component.assets.background
373
+
374
+ if not background_texture_path or not os.path.isfile(background_texture_path):
375
+ self.logger.warning("Background texture not found, cannot texture background mesh.")
376
+ return
377
+
378
+ decimated_background_mesh_path = self.assets.decimated_background_mesh
379
+ if not decimated_background_mesh_path or not os.path.isfile(decimated_background_mesh_path):
380
+ self.logger.warning(
381
+ "Decimated background mesh not found, cannot texture background mesh."
382
+ )
383
+ return
384
+
385
+ background_texture_resolution = self.get_background_texture_resolution(self.map_size)
386
+ non_resized_texture_image = cv2.imread(background_texture_path, cv2.IMREAD_UNCHANGED)
387
+
388
+ if non_resized_texture_image is None:
389
+ self.logger.error(
390
+ "Failed to read background texture image: %s", background_texture_path
391
+ )
392
+ return
393
+
394
+ resized_texture_image = cv2.resize(
395
+ non_resized_texture_image, # type: ignore
396
+ (background_texture_resolution, background_texture_resolution),
397
+ interpolation=cv2.INTER_AREA,
398
+ )
399
+
400
+ resized_texture_save_path = os.path.join(
401
+ self.textured_mesh_directory,
402
+ "background_texture.jpg",
403
+ )
404
+
405
+ cv2.imwrite(resized_texture_save_path, resized_texture_image)
406
+ self.logger.debug("Resized background texture saved: %s", resized_texture_save_path)
407
+
408
+ decimated_mesh = trimesh.load_mesh(decimated_background_mesh_path, force="mesh")
409
+
410
+ if decimated_mesh is None:
411
+ self.logger.error("Failed to load decimated mesh after all retry attempts")
412
+ return
413
+
414
+ try:
415
+ obj_save_path, mtl_save_path = self.texture_mesh(
416
+ decimated_mesh,
417
+ resized_texture_save_path,
418
+ output_directory=self.textured_mesh_directory,
419
+ output_name="background_textured_mesh",
420
+ )
421
+
422
+ self.assets.textured_background_mesh = obj_save_path
423
+ self.assets.textured_background_mtl = mtl_save_path
424
+ self.assets.resized_background_texture = resized_texture_save_path
425
+ self.logger.debug("Textured background mesh saved: %s", obj_save_path)
426
+ except Exception as e:
427
+ self.logger.error("Could not texture background mesh: %s", e)
428
+ return
429
+
430
+ def convert_background_mesh_to_i3d(self) -> bool:
431
+ """Converts the textured background mesh to i3d format.
432
+
433
+ Returns:
434
+ bool -- True if the conversion was successful, False otherwise.
435
+ """
436
+ if not self.assets.textured_background_mesh or not os.path.isfile(
437
+ self.assets.textured_background_mesh
438
+ ):
439
+ self.logger.warning("Textured background mesh not found, cannot convert to i3d.")
440
+ return False
441
+
442
+ if not self.assets.resized_background_texture or not os.path.isfile(
443
+ self.assets.resized_background_texture
444
+ ):
445
+ self.logger.warning("Resized background texture not found, cannot convert to i3d.")
446
+ return False
447
+
448
+ try:
449
+ mesh = trimesh.load_mesh(self.assets.textured_background_mesh, force="mesh")
450
+ except Exception as e:
451
+ self.logger.error("Could not load textured background mesh: %s", e)
452
+ return False
453
+
454
+ try:
455
+ i3d_background_terrain = self.mesh_to_i3d(
456
+ mesh,
457
+ output_dir=self.assets_background_directory,
458
+ name=Parameters.BACKGROUND_TERRAIN,
459
+ texture_path=self.assets.resized_background_texture,
460
+ water_mesh=False,
461
+ )
462
+ self.logger.debug(
463
+ "Background mesh converted to i3d successfully: %s", i3d_background_terrain
464
+ )
465
+ self.assets.background_terrain_i3d = i3d_background_terrain
466
+ return True
467
+ except Exception as e:
468
+ self.logger.error("Could not convert background mesh to i3d: %s", e)
469
+ return False
470
+
471
+ def add_note_file(self, asset: Literal["background", "water"]) -> None:
472
+ """Adds a note file to the background or water directory.
473
+
474
+ Arguments:
475
+ asset (Literal["background", "water"]): The asset type to add the note file to.
476
+ """
477
+ filename = "DO_NOT_USE_THESE_FILES.txt"
478
+ note_template = (
479
+ "Please find the ready-to-use {asset} i3d files in the {asset_directory} directory."
480
+ )
481
+ directory = {
482
+ "background": self.background_directory,
483
+ "water": self.water_directory,
484
+ }
485
+
486
+ content = (
487
+ "The files in this directory can be used to create the mesh files manually in Blender. "
488
+ "However, it's recommended to use the ready-to-use i3d files located in the assets "
489
+ "directory. There you'll find the i3d files, that can be imported directly into the "
490
+ "Giants Editor without any additional processing."
491
+ )
492
+ note = note_template.format(
493
+ asset=asset,
494
+ asset_directory=f"assets/{asset}",
495
+ )
496
+
497
+ file_path = os.path.join(directory[asset], filename)
498
+ with open(file_path, "w", encoding="utf-8") as f:
499
+ f.write(content + "\n\n" + note)
500
+
501
+ def convert_water_mesh_to_i3d(self) -> bool:
502
+ """Converts the line-based water mesh to i3d format.
503
+
504
+ Returns:
505
+ bool -- True if the conversion was successful, False otherwise.
506
+ """
507
+ if not self.assets.line_based_water_mesh or not os.path.isfile(
508
+ self.assets.line_based_water_mesh
509
+ ):
510
+ self.logger.warning("Line-based water mesh not found, cannot convert to i3d.")
511
+ return False
512
+
513
+ try:
514
+ mesh = trimesh.load_mesh(self.assets.line_based_water_mesh, force="mesh")
515
+ except Exception as e:
516
+ self.logger.error("Could not load line-based water mesh: %s", e)
517
+ return False
518
+
519
+ try:
520
+ i3d_water_resources = self.mesh_to_i3d(
521
+ mesh,
522
+ output_dir=self.assets_water_directory,
523
+ name=Parameters.WATER_RESOURCES,
524
+ water_mesh=True,
525
+ )
526
+ self.logger.debug(
527
+ "Water resources mesh converted to i3d successfully: %s", i3d_water_resources
528
+ )
529
+ self.assets.water_resources_i3d = i3d_water_resources
530
+ return True
531
+ except Exception as e:
532
+ self.logger.error("Could not convert water mesh to i3d: %s", e)
533
+ return False
534
+
256
535
  def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
257
536
  """Cuts out the center of the DEM (the actual map) and saves it as a separate file.
258
537
 
@@ -623,6 +902,8 @@ class Background(MeshComponent, ImageComponent):
623
902
  mesh.export(line_based_save_path)
624
903
  self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
625
904
 
905
+ self.assets.line_based_water_mesh = line_based_save_path
906
+
626
907
  def mesh_from_3d_polygons(
627
908
  self, polygons: list[shapely.Polygon], single_z_value: int | None = None
628
909
  ) -> Trimesh | None:
@@ -22,12 +22,31 @@ if TYPE_CHECKING:
22
22
 
23
23
 
24
24
  class AttrDict(dict):
25
- """A dictionary that allows attribute-style access to its keys."""
25
+ """A dictionary that allows attribute-style access to its keys.
26
+ Allows safe access to non-existing keys, returning None instead of raising KeyError.
27
+ """
28
+
29
+ def __getattr__(self, name: str) -> Any | None:
30
+ """Returns the value of the given key or None if the key does not exist.
26
31
 
27
- def __getattr__(self, name):
28
- return self[name]
32
+ Arguments:
33
+ name (str): The key to retrieve.
29
34
 
30
- def __setattr__(self, name, value):
35
+ Returns:
36
+ Any | None: The value of the key or None if the key does not exist.
37
+ """
38
+ try:
39
+ return self[name]
40
+ except KeyError:
41
+ return None
42
+
43
+ def __setattr__(self, name: str, value: Any) -> None:
44
+ """Sets the value of the given key.
45
+
46
+ Arguments:
47
+ name (str): The key to set.
48
+ value (Any): The value to set.
49
+ """
31
50
  self[name] = value
32
51
 
33
52
 
@@ -4,6 +4,7 @@ import os
4
4
 
5
5
  import cv2
6
6
  import numpy as np
7
+ from PIL import Image, ImageFile
7
8
 
8
9
  from maps4fs.generator.component.base.component import Component
9
10
  from maps4fs.generator.settings import Parameters
@@ -237,3 +238,35 @@ class ImageComponent(Component):
237
238
  edge_mask = cv2.subtract(bigger_mask, smaller_mask)
238
239
 
239
240
  return self.blur_by_mask(data, edge_mask)
241
+
242
+ @staticmethod
243
+ def convert_png_to_dds(input_png_path: str, output_dds_path: str):
244
+ """Convert a PNG file to DDS format using PIL
245
+
246
+ Arguments:
247
+ input_png_path (str): Path to input PNG file
248
+ output_dds_path (str): Path for output DDS file
249
+
250
+ Raises:
251
+ FileNotFoundError: If the input PNG file does not exist.
252
+ RuntimeError: If the DDS conversion fails.
253
+ """
254
+ if not os.path.exists(input_png_path):
255
+ raise FileNotFoundError(f"Input PNG file not found: {input_png_path}")
256
+
257
+ try:
258
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
259
+
260
+ with Image.open(input_png_path) as img:
261
+ # Convert to RGB if needed (DDS works better with RGB)
262
+ if img.mode == "RGBA":
263
+ # Create RGB version on white background
264
+ rgb_img = Image.new("RGB", img.size, (255, 255, 255))
265
+ rgb_img.paste(img, mask=img.split()[-1]) # Use alpha as mask
266
+ img = rgb_img
267
+ elif img.mode != "RGB":
268
+ img = img.convert("RGB")
269
+
270
+ img.save(output_dds_path, format="DDS")
271
+ except Exception as e:
272
+ raise RuntimeError(f"DDS conversion failed: {e}")
@@ -1,10 +1,15 @@
1
1
  """Base class for all components that primarily used to work with meshes."""
2
2
 
3
3
  import os
4
+ import shutil
5
+ import xml.etree.ElementTree as ET
6
+ from datetime import datetime
4
7
 
5
8
  import cv2
6
9
  import numpy as np
10
+ import open3d as o3d
7
11
  import trimesh
12
+ from PIL import Image
8
13
  from tqdm import tqdm
9
14
 
10
15
  from maps4fs.generator.component.base.component import Component
@@ -236,7 +241,9 @@ class MeshComponent(Component):
236
241
  cube_mesh = trimesh.creation.box([remove_size, remove_size, z_size * 4])
237
242
 
238
243
  return trimesh.boolean.difference(
239
- [mesh_copy, cube_mesh], check_volume=False, engine="blender"
244
+ [mesh_copy, cube_mesh],
245
+ check_volume=False,
246
+ engine="blender",
240
247
  )
241
248
 
242
249
  @staticmethod
@@ -269,3 +276,405 @@ class MeshComponent(Component):
269
276
  mesh_copy = mesh.copy()
270
277
  mesh_copy.faces = mesh_copy.faces[:, ::-1] # type: ignore
271
278
  return mesh_copy
279
+
280
+ @staticmethod
281
+ def decimate_mesh(mesh: trimesh.Trimesh, reduction_factor: float) -> trimesh.Trimesh:
282
+ """Decimate mesh using Open3D's quadric decimation (similar to Blender's approach)
283
+
284
+ Arguments:
285
+ mesh (trimesh.Trimesh): Input trimesh mesh
286
+ reduction_factor (float): Reduce to this fraction of original triangles (0.5 = 50%)
287
+
288
+ Returns:
289
+ trimesh.Trimesh: Decimated trimesh mesh
290
+ """
291
+ # 1. Convert trimesh to Open3D format.
292
+ vertices = mesh.vertices
293
+ faces = mesh.faces
294
+
295
+ o3d_mesh = o3d.geometry.TriangleMesh()
296
+ o3d_mesh.vertices = o3d.utility.Vector3dVector(vertices)
297
+ o3d_mesh.triangles = o3d.utility.Vector3iVector(faces)
298
+
299
+ # 2. Calculate target number of triangles.
300
+ target_triangles = int(len(faces) * reduction_factor)
301
+
302
+ # 3. Apply quadric decimation.
303
+ decimated_o3d = o3d_mesh.simplify_quadric_decimation(target_triangles)
304
+
305
+ # 4. Convert back to trimesh.
306
+ decimated_vertices = np.asarray(decimated_o3d.vertices)
307
+ decimated_faces = np.asarray(decimated_o3d.triangles)
308
+ decimated_mesh = trimesh.Trimesh(vertices=decimated_vertices, faces=decimated_faces)
309
+
310
+ return decimated_mesh
311
+
312
+ @staticmethod
313
+ def texture_mesh(
314
+ mesh: trimesh.Trimesh,
315
+ resized_texture_path: str,
316
+ output_directory: str,
317
+ output_name: str,
318
+ ) -> tuple[str, str]:
319
+ """Apply texture to mesh with UV mapping based on X and Z coordinates (ground plane).
320
+
321
+ Arguments:
322
+ mesh (trimesh.Trimesh): The mesh to texture
323
+ resized_texture_path (str): Path to resized texture image
324
+ output_directory (str): Directory to save textured OBJ and MTL files and texture.
325
+ output_name (str): Base name for output files (without extension)
326
+
327
+ Returns:
328
+ tuple[str, str]: Paths to the saved OBJ and MTL files
329
+ """
330
+ # 1. Copy texture to output directory (only if not already there).
331
+ texture_filename = os.path.basename(resized_texture_path)
332
+ texture_output_path = os.path.join(output_directory, texture_filename)
333
+
334
+ # Check if source and destination are the same file to avoid copy conflicts
335
+ if os.path.abspath(resized_texture_path) != os.path.abspath(texture_output_path):
336
+ shutil.copy2(resized_texture_path, texture_output_path)
337
+
338
+ # 2. Apply rotation to fix 90-degree X-axis rotation.
339
+ rotation_matrix = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
340
+ mesh.apply_transform(rotation_matrix)
341
+
342
+ # 3. Get mesh bounds: using X and Z for ground plane.
343
+ vertices = mesh.vertices
344
+
345
+ # 4. Ground plane coordinates (X, Z)
346
+ min_x = np.min(vertices[:, 0]) # X coordinate
347
+ max_x = np.max(vertices[:, 0])
348
+ min_z = np.min(vertices[:, 2]) # Z coordinate
349
+ max_z = np.max(vertices[:, 2])
350
+
351
+ width = max_x - min_x # X dimension
352
+ depth = max_z - min_z # Z dimension
353
+
354
+ # 5. Load texture.
355
+ texture_image = Image.open(texture_output_path)
356
+
357
+ # 6. Calculate UV coordinates based on X and Z positions.
358
+ uv_coords = np.zeros((len(vertices), 2), dtype=np.float32)
359
+ for i in tqdm(range(len(vertices)), desc="Calculating UVs", unit="vertex"):
360
+ vertex = vertices[i]
361
+
362
+ # Map X coordinate to U
363
+ u = (vertex[0] - min_x) / width if width > 0 else 0.5
364
+
365
+ # Map Z coordinate to V (NOT Y!)
366
+ v = (vertex[2] - min_z) / depth if depth > 0 else 0.5
367
+
368
+ # Flip V coordinate for correct orientation
369
+ v = 1.0 - v
370
+
371
+ # Clamp to valid range
372
+ u = np.clip(u, 0.0, 1.0)
373
+ v = np.clip(v, 0.0, 1.0)
374
+
375
+ uv_coords[i] = [u, v]
376
+
377
+ # 7. Create material.
378
+ material = trimesh.visual.material.PBRMaterial(
379
+ baseColorTexture=texture_image,
380
+ metallicFactor=0.0,
381
+ roughnessFactor=1.0,
382
+ emissiveFactor=[0.0, 0.0, 0.0],
383
+ )
384
+
385
+ # 8. Apply UV and material to mesh.
386
+ visual = trimesh.visual.TextureVisuals(uv=uv_coords, material=material)
387
+ mesh.visual = visual
388
+
389
+ mtl_filename = f"{output_name}.mtl"
390
+ obj_filename = f"{output_name}.obj"
391
+ mtl_filepath = os.path.join(output_directory, mtl_filename)
392
+ obj_filepath = os.path.join(output_directory, obj_filename)
393
+
394
+ faces = mesh.faces
395
+
396
+ # 9. Write OBJ file with correct UV mapping.
397
+ with open(obj_filepath, "w") as f: # pylint: disable=unspecified-encoding
398
+ f.write("# Corrected UV mapping using X,Z coordinates (ground plane)\n")
399
+ f.write("# Y coordinate represents elevation\n")
400
+ f.write(f"mtllib {os.path.basename(mtl_filename)}\n")
401
+
402
+ # Write vertices
403
+ for vertex in vertices:
404
+ f.write(f"v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n")
405
+
406
+ # Write UV coordinates
407
+ for uv in uv_coords:
408
+ f.write(f"vt {uv[0]:.6f} {uv[1]:.6f}\n")
409
+
410
+ # Write faces
411
+ f.write("usemtl TerrainMaterial_XZ\n")
412
+ for face in faces:
413
+ v1, v2, v3 = face[0] + 1, face[1] + 1, face[2] + 1
414
+ f.write(f"f {v1}/{v1} {v2}/{v2} {v3}/{v3}\n")
415
+
416
+ # 10. Write MTL file.
417
+ with open(mtl_filepath, "w") as f: # pylint: disable=unspecified-encoding
418
+ f.write("# Material with X,Z UV mapping\n")
419
+ f.write("newmtl TerrainMaterial_XZ\n")
420
+ f.write("Ka 1.0 1.0 1.0\n")
421
+ f.write("Kd 1.0 1.0 1.0\n")
422
+ f.write("Ks 0.0 0.0 0.0\n")
423
+ f.write("illum 1\n")
424
+ f.write(f"map_Kd {texture_filename}\n")
425
+
426
+ return obj_filepath, mtl_filepath
427
+
428
+ @staticmethod
429
+ def mesh_to_i3d(
430
+ mesh: trimesh.Trimesh,
431
+ output_dir: str,
432
+ name: str,
433
+ texture_path: str | None = None,
434
+ water_mesh: bool = False,
435
+ ) -> str:
436
+ """Convert a trimesh to i3d format with optional water shader support.
437
+
438
+ Arguments:
439
+ mesh (trimesh.Trimesh): trimesh.Trimesh object to convert
440
+ output_dir (str): Directory to save i3d and copy textures to
441
+ name (str): Base name for output files (e.g., "terrain_mesh")
442
+ texture_path (str | None): Optional path to texture file (will be copied to output_dir)
443
+ water_mesh (bool): If True, adds ocean shader material for water rendering
444
+
445
+ Returns:
446
+ str: Full path to the generated i3d file
447
+ """
448
+
449
+ # Ensure output directory exists
450
+ if not os.path.exists(output_dir):
451
+ os.makedirs(output_dir)
452
+
453
+ # Apply transformations (only for water meshes)
454
+ if water_mesh:
455
+ # 1. Apply rotation fix (90-degree X-axis correction) - water only
456
+ rotation_matrix = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
457
+ mesh.apply_transform(rotation_matrix)
458
+
459
+ # 2. Center mesh at origin - water only
460
+ vertices = mesh.vertices
461
+ center = vertices.mean(axis=0)
462
+ mesh.vertices = vertices - center
463
+
464
+ # 3. Handle texture copying if provided
465
+ texture_file = None
466
+ if texture_path and os.path.exists(texture_path):
467
+ texture_filename = os.path.basename(texture_path)
468
+ texture_dest = os.path.join(output_dir, texture_filename)
469
+
470
+ # Copy texture if it's not already in output_dir
471
+ if os.path.abspath(texture_path) != os.path.abspath(texture_dest):
472
+ shutil.copy2(texture_path, texture_dest)
473
+
474
+ texture_file = texture_filename
475
+
476
+ # 4. Generate i3d file
477
+ output_path = os.path.join(output_dir, f"{name}.i3d")
478
+ MeshComponent._write_i3d_file(mesh, output_path, name, texture_file, water_mesh)
479
+
480
+ return output_path
481
+
482
+ @staticmethod
483
+ def _write_i3d_file(
484
+ mesh: trimesh.Trimesh,
485
+ output_path: str,
486
+ name: str,
487
+ texture_file: str | None,
488
+ is_water: bool,
489
+ ) -> None:
490
+ """Write the actual i3d XML file.
491
+
492
+ Arguments:
493
+ mesh (trimesh.Trimesh): object containing the geometry
494
+ output_path (str): Full path where to save the i3d file
495
+ name (str): Name for the mesh in i3d file
496
+ texture_file (str | None): Optional texture filename (if copied to output dir)
497
+ is_water (bool): If True, generates water mesh with ocean shader
498
+ """
499
+
500
+ # Root element
501
+ i3d = ET.Element(
502
+ "i3D",
503
+ attrib={
504
+ "name": name,
505
+ "version": "1.6",
506
+ "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
507
+ "xsi:noNamespaceSchemaLocation": "http://i3d.giants.ch/schema/i3d-1.6.xsd",
508
+ },
509
+ )
510
+
511
+ # Asset section
512
+ asset = ET.SubElement(i3d, "Asset")
513
+ exp = ET.SubElement(asset, "Export")
514
+ exp.set("program", "maps4fs")
515
+ exp.set("version", "1.0")
516
+ exp.set("date", datetime.now().strftime("%Y-%m-%d"))
517
+
518
+ vertices = mesh.vertices
519
+ faces = mesh.faces
520
+
521
+ has_normals = mesh.vertex_normals is not None and len(mesh.vertex_normals) == len(vertices)
522
+ has_uv = (
523
+ hasattr(mesh.visual, "uv")
524
+ and mesh.visual.uv is not None
525
+ and len(mesh.visual.uv) == len(vertices)
526
+ )
527
+
528
+ # Files section
529
+ files_section = None
530
+ if is_water:
531
+ # Water mesh: add ocean shader
532
+ files_section = ET.SubElement(i3d, "Files")
533
+ shader_file = ET.SubElement(files_section, "File")
534
+ shader_file.set("fileId", "4")
535
+ shader_file.set("filename", "$data/shaders/oceanShader.xml")
536
+ elif texture_file:
537
+ # Terrain mesh: add texture file
538
+ files_section = ET.SubElement(i3d, "Files")
539
+ file_entry = ET.SubElement(files_section, "File")
540
+ file_entry.set("fileId", "1")
541
+ file_entry.set("filename", texture_file)
542
+ file_entry.set("relativePath", "true")
543
+
544
+ # Materials section
545
+ materials_section = ET.SubElement(i3d, "Materials")
546
+ material = ET.SubElement(materials_section, "Material")
547
+
548
+ if is_water:
549
+ # Water material with ocean shader
550
+ material.set("name", "OceanShader")
551
+ material.set("materialId", "1")
552
+ material.set("diffuseColor", "0.8 0.8 0.8 1")
553
+ material.set("specularColor", "0.501961 1 0")
554
+ material.set("customShaderId", "4")
555
+ material.set("customShaderVariation", "simple")
556
+
557
+ # Required for ocean shader
558
+ normalmap = ET.SubElement(material, "Normalmap")
559
+ normalmap.set("fileId", "2")
560
+
561
+ refractionmap = ET.SubElement(material, "Refractionmap")
562
+ refractionmap.set("coeff", "1")
563
+ refractionmap.set("bumpScale", "0.01")
564
+ refractionmap.set("withSSRData", "true")
565
+ else:
566
+ # Standard terrain material
567
+ material.set("name", f"{name}_material")
568
+ material.set("materialId", "1")
569
+ material.set("diffuseColor", "1 1 1 1")
570
+ material.set("specularColor", "0.5 0.5 0.5")
571
+
572
+ if texture_file:
573
+ texture = ET.SubElement(material, "Texture")
574
+ texture.set("fileId", "1")
575
+
576
+ # Shapes section
577
+ shapes = ET.SubElement(i3d, "Shapes")
578
+ shape = ET.SubElement(shapes, "IndexedTriangleSet")
579
+ shape.set("name", name)
580
+ shape.set("shapeId", "1")
581
+
582
+ # Calculate bounding sphere
583
+ if len(vertices) > 0:
584
+ center = vertices.mean(axis=0)
585
+ max_dist = ((vertices - center) ** 2).sum(axis=1).max() ** 0.5
586
+ shape.set("bvCenter", f"{center[0]:.6f} {center[1]:.6f} {center[2]:.6f}")
587
+ shape.set("bvRadius", f"{max_dist:.6f}")
588
+
589
+ # Vertices block
590
+ xml_vertices = ET.SubElement(shape, "Vertices")
591
+ xml_vertices.set("count", str(len(vertices)))
592
+
593
+ if has_normals:
594
+ xml_vertices.set("normal", "true")
595
+ if has_uv:
596
+ xml_vertices.set("uv0", "true")
597
+
598
+ # Write vertex data
599
+ for idx in tqdm(range(len(vertices)), desc="Writing vertices", unit="vertex"):
600
+ v = vertices[idx]
601
+ v_el = ET.SubElement(xml_vertices, "v")
602
+ v_el.set("p", f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
603
+
604
+ if has_normals:
605
+ n = mesh.vertex_normals[idx]
606
+ v_el.set("n", f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}")
607
+
608
+ if has_uv:
609
+ uv = mesh.visual.uv[idx]
610
+ v_el.set("t0", f"{uv[0]:.6f} {uv[1]:.6f}")
611
+
612
+ # Triangles block
613
+ xml_tris = ET.SubElement(shape, "Triangles")
614
+ xml_tris.set("count", str(len(faces)))
615
+ for f in tqdm(faces, desc="Writing triangles", unit="triangle"):
616
+ t = ET.SubElement(xml_tris, "t")
617
+ t.set("vi", f"{f[0]} {f[1]} {f[2]}")
618
+
619
+ # Subsets block
620
+ xml_subs = ET.SubElement(shape, "Subsets")
621
+ xml_subs.set("count", "1")
622
+ subset = ET.SubElement(xml_subs, "Subset")
623
+ subset.set("firstVertex", "0")
624
+ subset.set("numVertices", str(len(vertices)))
625
+ subset.set("firstIndex", "0")
626
+ subset.set("numIndices", str(len(faces) * 3))
627
+
628
+ # Scene section
629
+ scene = ET.SubElement(i3d, "Scene")
630
+
631
+ if is_water:
632
+ # Water: direct shape node
633
+ shape_node = ET.SubElement(scene, "Shape")
634
+ shape_node.set("name", name)
635
+ shape_node.set("shapeId", "1")
636
+ shape_node.set("nodeId", "4")
637
+ shape_node.set("castsShadows", "true")
638
+ shape_node.set("receiveShadows", "true")
639
+ shape_node.set("materialIds", "1")
640
+ else:
641
+ # Terrain: transform group with shape
642
+ transform_group = ET.SubElement(scene, "TransformGroup")
643
+ transform_group.set("name", name)
644
+ transform_group.set("nodeId", "1")
645
+
646
+ shape_node = ET.SubElement(transform_group, "Shape")
647
+ shape_node.set("name", f"{name}_shape")
648
+ shape_node.set("nodeId", "2")
649
+ shape_node.set("shapeId", "1")
650
+ shape_node.set("static", "true")
651
+ shape_node.set("compound", "false")
652
+ shape_node.set("collision", "true")
653
+ shape_node.set("materialIds", "1")
654
+
655
+ # Pretty print and write
656
+ MeshComponent._indent(i3d)
657
+ tree = ET.ElementTree(i3d)
658
+ tree.write(output_path, encoding="iso-8859-1", xml_declaration=True)
659
+
660
+ @staticmethod
661
+ def _indent(elem: ET.Element, level: int = 0) -> None:
662
+ """Pretty print XML formatting. Modifies the element in place.
663
+
664
+ Arguments:
665
+ elem (ET.Element): The XML element to indent
666
+ level (int): Current indentation level
667
+ """
668
+ i = "\n" + level * " "
669
+ if len(elem):
670
+ if not elem.text or not elem.text.strip():
671
+ elem.text = i + " "
672
+ if not elem.tail or not elem.tail.strip():
673
+ elem.tail = i
674
+ for e in elem:
675
+ MeshComponent._indent(e, level + 1)
676
+ if not elem.tail or not elem.tail.strip():
677
+ elem.tail = i
678
+ else:
679
+ if level and (not elem.tail or not elem.tail.strip()):
680
+ elem.tail = i
@@ -6,11 +6,13 @@ import os
6
6
 
7
7
  import cv2
8
8
 
9
+ from maps4fs.generator.component.base.component_image import ImageComponent
9
10
  from maps4fs.generator.component.base.component_xml import XMLComponent
11
+ from maps4fs.generator.settings import Parameters
10
12
 
11
13
 
12
14
  # pylint: disable=R0903
13
- class Config(XMLComponent):
15
+ class Config(XMLComponent, ImageComponent):
14
16
  """Component for map settings and configuration.
15
17
 
16
18
  Arguments:
@@ -36,6 +38,8 @@ class Config(XMLComponent):
36
38
  if self.game.fog_processing:
37
39
  self._adjust_fog()
38
40
 
41
+ self._set_overview()
42
+
39
43
  def _set_map_size(self) -> None:
40
44
  """Edits map.xml file to set correct map size."""
41
45
  tree = self.get_tree()
@@ -213,3 +217,62 @@ class Config(XMLComponent):
213
217
  )
214
218
 
215
219
  return dem_maximum_meter, dem_minimum_meter
220
+
221
+ def _set_overview(self) -> None:
222
+ """Generates and sets the overview image for the map."""
223
+ try:
224
+ overview_image_path = self.game.overview_file_path(self.map_directory)
225
+ except NotImplementedError:
226
+ self.logger.warning(
227
+ "Game does not support overview image file, overview generation will be skipped."
228
+ )
229
+ return
230
+
231
+ satellite_component = self.map.get_satellite_component()
232
+ if not satellite_component:
233
+ self.logger.warning(
234
+ "Satellite component not found, overview generation will be skipped."
235
+ )
236
+ return
237
+
238
+ if not satellite_component.assets.overview or not os.path.isfile(
239
+ satellite_component.assets.overview
240
+ ):
241
+ self.logger.warning(
242
+ "Satellite overview image not found, overview generation will be skipped."
243
+ )
244
+ return
245
+
246
+ satellite_images_directory = os.path.dirname(satellite_component.assets.overview)
247
+ overview_image = cv2.imread(satellite_component.assets.overview, cv2.IMREAD_UNCHANGED)
248
+
249
+ if overview_image is None:
250
+ self.logger.warning(
251
+ "Failed to read satellite overview image, overview generation will be skipped."
252
+ )
253
+ return
254
+
255
+ resized_overview_image = cv2.resize(
256
+ overview_image,
257
+ (Parameters.OVERVIEW_IMAGE_SIZE, Parameters.OVERVIEW_IMAGE_SIZE),
258
+ interpolation=cv2.INTER_LINEAR,
259
+ )
260
+
261
+ resized_overview_path = os.path.join(
262
+ satellite_images_directory,
263
+ f"{Parameters.OVERVIEW_IMAGE_FILENAME}.png",
264
+ )
265
+
266
+ cv2.imwrite(resized_overview_path, resized_overview_image)
267
+ self.logger.info("Overview image saved to: %s", resized_overview_path)
268
+
269
+ if os.path.isfile(overview_image_path):
270
+ try:
271
+ os.remove(overview_image_path)
272
+ self.logger.debug("Old overview image removed: %s", overview_image_path)
273
+ except Exception as e:
274
+ self.logger.warning("Failed to remove old overview image: %s", e)
275
+ return
276
+
277
+ self.convert_png_to_dds(resized_overview_path, overview_image_path)
278
+ self.logger.info("Overview image converted and saved to: %s", overview_image_path)
maps4fs/generator/game.py CHANGED
@@ -42,9 +42,10 @@ class Game:
42
42
  _environment_processing: bool = True
43
43
  _fog_processing: bool = True
44
44
  _dissolve: bool = True
45
+ _mesh_processing: bool = True
45
46
 
46
47
  # Order matters! Some components depend on others.
47
- components = [Texture, Background, GRLE, I3d, Config, Satellite]
48
+ components = [Satellite, Texture, Background, GRLE, I3d, Config]
48
49
 
49
50
  def __init__(self, map_template_path: str | None = None):
50
51
  if map_template_path:
@@ -253,6 +254,16 @@ class Game:
253
254
  str: The path to the i3d file."""
254
255
  raise NotImplementedError
255
256
 
257
+ def overview_file_path(self, map_directory: str) -> str:
258
+ """Returns the path to the overview image file.
259
+
260
+ Arguments:
261
+ map_directory (str): The path to the map directory.
262
+
263
+ Returns:
264
+ str: The path to the overview image file."""
265
+ raise NotImplementedError
266
+
256
267
  @property
257
268
  def i3d_processing(self) -> bool:
258
269
  """Returns whether the i3d file should be processed.
@@ -312,6 +323,14 @@ class Game:
312
323
  bool: True if the dissolve should be applied, False otherwise."""
313
324
  return self._dissolve
314
325
 
326
+ @property
327
+ def mesh_processing(self) -> bool:
328
+ """Returns whether the mesh should be processed.
329
+
330
+ Returns:
331
+ bool: True if the mesh should be processed, False otherwise."""
332
+ return self._mesh_processing
333
+
315
334
 
316
335
  class FS22(Game):
317
336
  """Class used to define the game version FS22."""
@@ -324,6 +343,7 @@ class FS22(Game):
324
343
  _fog_processing = False
325
344
  _plants_processing = False
326
345
  _dissolve = False
346
+ _mesh_processing = False
327
347
 
328
348
  def dem_file_path(self, map_directory: str) -> str:
329
349
  """Returns the path to the DEM file.
@@ -427,3 +447,13 @@ class FS25(Game):
427
447
  Returns:
428
448
  str: The path to the environment xml file."""
429
449
  return os.path.join(map_directory, "map", "config", "environment.xml")
450
+
451
+ def overview_file_path(self, map_directory: str) -> str:
452
+ """Returns the path to the overview image file.
453
+
454
+ Arguments:
455
+ map_directory (str): The path to the map directory.
456
+
457
+ Returns:
458
+ str: The path to the overview image file."""
459
+ return os.path.join(map_directory, "map", "overview.dds")
maps4fs/generator/map.py CHANGED
@@ -12,7 +12,7 @@ from pydtmdl.base.dtm import DTMProviderSettings
12
12
 
13
13
  import maps4fs.generator.config as mfscfg
14
14
  import maps4fs.generator.utils as mfsutils
15
- from maps4fs.generator.component import Background, Component, Layer, Texture
15
+ from maps4fs.generator.component import Background, Component, Layer, Satellite, Texture
16
16
  from maps4fs.generator.game import Game
17
17
  from maps4fs.generator.settings import GenerationSettings, MainSettings, SharedSettings
18
18
  from maps4fs.generator.statistics import send_advanced_settings, send_main_settings
@@ -150,6 +150,9 @@ class Map:
150
150
  raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
151
151
  # endregion
152
152
 
153
+ self.assets_directory = os.path.join(self.map_directory, "assets")
154
+ os.makedirs(self.assets_directory, exist_ok=True)
155
+
153
156
  self.shared_settings = SharedSettings()
154
157
  self.components: list[Component] = []
155
158
 
@@ -301,6 +304,17 @@ class Map:
301
304
  return None
302
305
  return component
303
306
 
307
+ def get_satellite_component(self) -> Satellite | None:
308
+ """Get satellite component.
309
+
310
+ Returns:
311
+ Satellite | None: Satellite instance or None if not found.
312
+ """
313
+ component = self.get_component("Satellite")
314
+ if not isinstance(component, Satellite):
315
+ return None
316
+ return component
317
+
304
318
  def get_texture_layer(self, by_usage: str | None = None) -> Layer | None:
305
319
  """Get texture layer by usage.
306
320
 
@@ -28,12 +28,19 @@ class Parameters:
28
28
  WATER = "water"
29
29
  FARMYARDS = "farmyards"
30
30
 
31
+ MAXIMUM_BACKGROUND_TEXTURE_SIZE = 4096
32
+
31
33
  PREVIEW_MAXIMUM_SIZE = 2048
32
34
 
33
35
  BACKGROUND_DISTANCE = 2048
34
36
  FULL = "FULL"
35
37
  PREVIEW = "PREVIEW"
36
38
 
39
+ DECIMATED_BACKGROUND = "decimated_background"
40
+ BACKGROUND_TERRAIN = "background_terrain"
41
+
42
+ WATER_RESOURCES = "water_resources"
43
+
37
44
  RESIZE_FACTOR = 8
38
45
 
39
46
  FARMLAND_ID_LIMIT = 254
@@ -48,6 +55,9 @@ class Parameters:
48
55
 
49
56
  HEIGHT_SCALE = "heightScale"
50
57
 
58
+ OVERVIEW_IMAGE_SIZE = 4096
59
+ OVERVIEW_IMAGE_FILENAME = "overview"
60
+
51
61
 
52
62
  class SharedSettings(BaseModel):
53
63
  """Represents the shared settings for all components."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.2
3
+ Version: 2.8.4
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -26,18 +26,22 @@ Requires-Dist: scipy
26
26
  Requires-Dist: pydtmdl
27
27
  Requires-Dist: manifold3d
28
28
  Requires-Dist: fast-simplification
29
+ Requires-Dist: open3d
29
30
  Dynamic: license-file
30
31
 
31
- <p align="center">
32
- <a href="https://github.com/iwatkot/maps4fs">maps4fs</a> •
33
- <a href="https://github.com/iwatkot/maps4fsui">maps4fs UI</a> •
34
- <a href="https://github.com/iwatkot/maps4fsdata">maps4fs Data</a> •
35
- <a href="https://github.com/iwatkot/maps4fsapi">maps4fs API</a> •
36
- <a href="https://github.com/iwatkot/maps4fsstats">maps4fs Stats</a> •
37
- <a href="https://github.com/iwatkot/maps4fsbot">maps4fs Bot</a><br>
38
- <a href="https://github.com/iwatkot/pygmdl">pygmdl</a> •
39
- <a href="https://github.com/iwatkot/pydtmdl">pydtmdl</a>
40
- </p>
32
+ <div align="center" markdown>
33
+
34
+ [![Maps4FS](https://img.shields.io/badge/maps4fs-gray?style=for-the-badge)](https://github.com/iwatkot/maps4fs)
35
+ [![PYDTMDL](https://img.shields.io/badge/pydtmdl-blue?style=for-the-badge)](https://github.com/iwatkot/pydtmdl)
36
+ [![PYGDMDL](https://img.shields.io/badge/pygmdl-teal?style=for-the-badge)](https://github.com/iwatkot/pygmdl)
37
+ [![Maps4FS API](https://img.shields.io/badge/maps4fs-api-green?style=for-the-badge)](https://github.com/iwatkot/maps4fsapi)
38
+ [![Maps4FS UI](https://img.shields.io/badge/maps4fs-ui-blue?style=for-the-badge)](https://github.com/iwatkot/maps4fsui)
39
+ [![Maps4FS Data](https://img.shields.io/badge/maps4fs-data-orange?style=for-the-badge)](https://github.com/iwatkot/maps4fsdata)
40
+ [![Maps4FS Upgrader](https://img.shields.io/badge/maps4fs-upgrader-yellow?style=for-the-badge)](https://github.com/iwatkot/maps4fsupgrader)
41
+ [![Maps4FS Stats](https://img.shields.io/badge/maps4fs-stats-red?style=for-the-badge)](https://github.com/iwatkot/maps4fsstats)
42
+ [![Maps4FS Bot](https://img.shields.io/badge/maps4fs-bot-teal?style=for-the-badge)](https://github.com/iwatkot/maps4fsbot)
43
+
44
+ </div>
41
45
 
42
46
  <div align="center" markdown>
43
47
  <a href="https://discord.gg/Sj5QKKyE42">
@@ -2,15 +2,15 @@ maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
2
2
  maps4fs/logger.py,sha256=6sem0aFKQqtVjQ_yNu9iGcc-hqzLQUhfxco05K6nqow,763
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/config.py,sha256=KwYYWM5wrMB_tKn3Fls6WpEurZJxmrBH5AgAXPGHTJE,7122
5
- maps4fs/generator/game.py,sha256=nf6iuYNA5NJc-ir_WOgkw-MdJVgetVHeEtxbWJYt3Vo,14462
6
- maps4fs/generator/map.py,sha256=IC72Mr_04wm5kby4J0m-Bw32UrcE6qn2KwaX9vLEzn4,14036
5
+ maps4fs/generator/game.py,sha256=axi-h2wmLZHMu5xdzFq4BEYjofJHmMNiGjmw5AA36JA,15421
6
+ maps4fs/generator/map.py,sha256=ZZRU8x0feGbgeJgxc0D3N-mfiasyFXxj6gbGyl-WRzE,14528
7
7
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
8
- maps4fs/generator/settings.py,sha256=oDs_bgvqHMJfKGK7uVVkm6MykAL3dVkvm5L3-hvZx2c,13044
8
+ maps4fs/generator/settings.py,sha256=OSwnkvaF9O2nh0khQ9u9RS4pomXsywTUGyqZOUcUk2Y,13299
9
9
  maps4fs/generator/statistics.py,sha256=Dp1-NS-DWv0l0UdmhOoXeQs_N-Hs7svYUnmziSW5Z9I,2098
10
10
  maps4fs/generator/utils.py,sha256=ugdQ8C22NeiZLIlldLoEKCc7ioOefz4W-8qF2eOy9qU,4834
11
11
  maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
12
- maps4fs/generator/component/background.py,sha256=rUDTPduNCD7KKt_eoVWN4XVSVgjwGSoLNPbZOzpGx7E,34532
13
- maps4fs/generator/component/config.py,sha256=uL76h9UwyhZKZmbxz0mBmWtEPN6qYay4epTEqqtej60,8601
12
+ maps4fs/generator/component/background.py,sha256=tFWdYASUUZsJ8QiQrxxba-6aMECD3hvpd0pMm_UdrH8,46151
13
+ maps4fs/generator/component/config.py,sha256=yujW_FF-0mbJ_5PuSeFC45hHb5MOoPAJYdISPQvmtRQ,11118
14
14
  maps4fs/generator/component/dem.py,sha256=FPqcXmFQg5MPaGuy4g5kxzvY1wbhozeCf-aNMCj5eaU,11687
15
15
  maps4fs/generator/component/grle.py,sha256=0PC1K829wjD4y4d9qfIbnU29ebjflIPBbwIZx8FXwc8,27242
16
16
  maps4fs/generator/component/i3d.py,sha256=RvpiW9skkZ6McyahC-AeIdPuSQjpXiFs1l0xOioJAu4,26638
@@ -18,12 +18,12 @@ maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDec
18
18
  maps4fs/generator/component/satellite.py,sha256=9nKwL8zQ-BB6WFMx2m8zduFn6RaxSNv6Vtpge1-QMYE,5052
19
19
  maps4fs/generator/component/texture.py,sha256=gXZgr73ehT3_qjuIku0j7N7exloywtmmEQKBJ-MDcco,36988
20
20
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
21
- maps4fs/generator/component/base/component.py,sha256=lf0V9CLUXMg88Nm2yI3rP5taVYYlNivud0x6kbhBYqA,23312
22
- maps4fs/generator/component/base/component_image.py,sha256=WTGC6v1KuS5sLNCC95Z48nCspvATKKNOuhTNYzTWXr4,8315
23
- maps4fs/generator/component/base/component_mesh.py,sha256=3hC-qDT8Vde6SmRMqs9USAkrF-gL2dDTYW71ATpxUS4,9130
21
+ maps4fs/generator/component/base/component.py,sha256=-7H3donrH19f0_rivNyI3fgLsiZkntXfGywEx4tOnM4,23924
22
+ maps4fs/generator/component/base/component_image.py,sha256=GXFkEFARNRkWkDiGSjvU4WX6f_8s6R1t2ZYqZflv1jk,9626
23
+ maps4fs/generator/component/base/component_mesh.py,sha256=S5M_SU-FZz17-LgzTIM935ms1Vc4O06UQNTEN4e0INU,24729
24
24
  maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
25
- maps4fs-2.8.2.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
26
- maps4fs-2.8.2.dist-info/METADATA,sha256=MeLuVswRau8eG5hivACyRZqqN71C8nbxREvmuoAOYKQ,9425
27
- maps4fs-2.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- maps4fs-2.8.2.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
29
- maps4fs-2.8.2.dist-info/RECORD,,
25
+ maps4fs-2.8.4.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
26
+ maps4fs-2.8.4.dist-info/METADATA,sha256=YQl9Cryl9G4MFtHX-Lt_FQSLzx5eGnbggY7QvISISdg,10042
27
+ maps4fs-2.8.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ maps4fs-2.8.4.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
29
+ maps4fs-2.8.4.dist-info/RECORD,,