From 824056d71f0e2775f59152c8d8c07501752210e2 Mon Sep 17 00:00:00 2001 From: march3 Date: Fri, 14 Apr 2023 14:03:43 +0800 Subject: [PATCH] =?UTF-8?q?Python=E8=B6=85=E4=BA=BA-=E5=AE=87=E5=AE=99?= =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- objs/__init__.py | 0 objs/obj.py | 427 ++++++++++++++++++ sim_scenes/func.py | 8 +- sim_scenes/funny/dancing_with_jupiter.py | 4 +- sim_scenes/solar_system/speed_of_light.py | 6 +- .../solar_system/speed_of_light_init.py | 7 +- .../solar_system/transit_of_venus_mercury.py | 35 +- simulators/ursina_simulator.py | 1 + textures/transparent.png | Bin 0 -> 2929 bytes 9 files changed, 470 insertions(+), 18 deletions(-) create mode 100644 objs/__init__.py create mode 100644 objs/obj.py create mode 100644 textures/transparent.png diff --git a/objs/__init__.py b/objs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/objs/obj.py b/objs/obj.py new file mode 100644 index 0000000..d1cdb53 --- /dev/null +++ b/objs/obj.py @@ -0,0 +1,427 @@ +# -*- coding:utf-8 -*- +# title :对象基类 +# description :对象基类(所有星体都继承了该类) +# author :Python超人 +# date :2023-02-11 +# link :https://gitcode.net/pythoncr/ +# python_version :3.8 +# ============================================================================== +from abc import ABCMeta, abstractmethod +import json +import numpy as np +import math +from common.consts import AU +import copy + + +class Obj(metaclass=ABCMeta): + """ + 对象基类 + """ + + def __init__(self, name, mass, init_position, init_velocity, + density=5e3, color=(125 / 255, 125 / 255, 125 / 255), + texture=None, size_scale=1.0, distance_scale=1.0, + parent=None, ignore_mass=False, + trail_color=None, show_name=False): + """ + 对象类 + @param name: 对象名称 + @param mass: 对象质量 (kg) + @param init_position: 初始位置 (km) + @param init_velocity: 初始速度 (km/s) + @param density: 平均密度 (kg/m³) + @param color: 对象颜色(纹理图片优先) + @param texture: 纹理图片 + @param size_scale: 尺寸缩放 + @param distance_scale: 距离缩放 + @param parent: 对象的父对象 + @param ignore_mass: 是否忽略质量(如果为True,则不计算引力) + TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在对象物理学中是不严谨) + @param trail_color: 对象拖尾颜色(默认对象颜色) + @param show_name: 是否显示对象名称 + """ + self.__his_pos = [] + self.__his_vel = [] + self.__his_acc = [] + self.__his_reserved_num = 200 + + if name is None: + name = getattr(self.__class__, '__name__') + + self.name = name + self.__mass = mass + + # 是否忽略质量(如果为True,则不计算引力) + # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在对象物理学中是不严谨) + if self.__mass <= 0: # 质量小于等于0就忽略 + self.ignore_mass = True + else: + self.ignore_mass = ignore_mass + + self.__init_position = None + self.__init_velocity = None + + self.init_position = np.array(init_position, dtype='float32') + self.init_velocity = np.array(init_velocity, dtype='float32') + + self.__density = density + + self.color = color + self.trail_color = color if trail_color is None else trail_color + self.texture = texture + + self.size_scale = size_scale + self.distance_scale = distance_scale + + # 初始化后,加速度为0,只有多个对象的引力才会影响到加速度 + # km/s² + self.__acceleration = np.array([0, 0, 0], dtype='float32') + self.__record_history() + + # 是否显示 + self.appeared = True + self.parent = parent + + self.show_name = show_name + + self.resolution = None + self.light_disable = False + + self.__has_rings = False + + def set_ignore_gravity(self, value=True): + """ + 设置忽略质量,True为引力失效 + @param value: + @return: + """ + # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在对象物理学中是不严谨) + if self.__mass <= 0: # 质量小于等于0就忽略 + self.ignore_mass = True + else: + self.ignore_mass = value + return self + + def set_light_disable(self, value=True): + """ + 设置灯光为无效 + @param value: + @return: + """ + self.light_disable = value + return self + + def set_resolution(self, value): + """ + 设置对象的分辨率 + @param value: + @return: + """ + self.resolution = value + return self + + @property + def init_position(self): + """ + 获取对象的初始位置(单位:km) + @return: + """ + return self.__init_position + + @init_position.setter + def init_position(self, value): + """ + 设置对象的初始位置(单位:km) + @param value: + @return: + """ + self.__init_position = np.array(value, dtype='float32') + self.__position = copy.deepcopy(self.__init_position) + + @property + def init_velocity(self): + """ + 获取对象的初始速度 (km/s) + @return: + """ + return self.__init_velocity + + @init_velocity.setter + def init_velocity(self, value): + """ + 设置对象的初始速度 (km/s) + @param value: + @return: + """ + self.__init_velocity = np.array(value, dtype='float32') + self.__velocity = copy.deepcopy(self.__init_velocity) + + @property + def position(self): + """ + 获取对象的位置(单位:km) + @return: + """ + return self.__position + + @position.setter + def position(self, value): + """ + 设置对象的位置(单位:km) + @param value: + @return: + """ + self.__position = value + self.__record_history() + + @property + def acceleration(self): + """ + 获取对象的加速度(单位:km/s²) + @return: + """ + return self.__acceleration + + @acceleration.setter + def acceleration(self, value): + """ + 设置对象的加速度(单位:km/s²) + @param value: + @return: + """ + self.__acceleration = np.array(value, dtype=float) + self.__record_history() + + def stop(self): + """ + 停止运动,将加速度和速度置零 + @return: + """ + self.init_velocity = [0.0, 0.0, 0.0] + self.acceleration = [0.0, 0.0, 0.0] + + def stop_and_ignore_gravity(self): + """ + 停止运动,并忽略质量(不受引力影响) + TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在对象物理学中是不严谨) + @return: + """ + self.set_ignore_gravity() + self.stop() + + @property + def velocity(self): + """ + 获取对象的速度(单位:km/s) + @return: + """ + return self.__velocity + + @velocity.setter + def velocity(self, value): + """ + 设置对象的速度(单位:km/s) + @param value: + @return: + """ + self.__velocity = value + self.__record_history() + + def __append_history(self, his_list, data): + """ + 追加每个位置时刻的历史数据 + @param his_list: + @param data: + @return: + """ + # 如果历史记录为0 或者 新增数据和最后的历史数据不相同,则添加 + if len(his_list) == 0 or \ + np.sum(data == his_list[-1]) < len(data): + his_list.append(data.copy()) + + def __record_history(self): + """ + 记录每个位置时刻的历史数据 + @return: + """ + # 如果历史记录数超过了保留数量,则截断,只保留 __his_reserved_num 数量的历史 + if len(self.__his_pos) > self.__his_reserved_num: + self.__his_pos = self.__his_pos[len(self.__his_pos) - self.__his_reserved_num:] + self.__his_vel = self.__his_vel[len(self.__his_vel) - self.__his_reserved_num:] + self.__his_acc = self.__his_acc[len(self.__his_acc) - self.__his_reserved_num:] + + # 追加历史记录(位置、速度、加速度) + self.__append_history(self.__his_pos, self.position) + self.__append_history(self.__his_vel, self.velocity) + self.__append_history(self.__his_acc, self.acceleration) + # print(self.name, "his pos->", self.__his_pos) + + def his_position(self): + """ + 历史位置 + @return: + """ + return self.__his_pos + + def his_velocity(self): + """ + 历史瞬时速度 + @return: + """ + return self.__his_vel + + def his_acceleration(self): + """ + 历史瞬时加速度 + @return: + """ + return self.__his_acc + + @property + def mass(self): + """ + 对象质量 (单位:kg) + @return: + """ + return self.__mass + + @property + def density(self): + """ + 平均密度 (单位:kg/m³) + @return: + """ + return self.__density + + def __repr__(self): + return '<%s(%s)> m=%.3e(kg), d=%.3e(kg/m³), p=[%.3e,%.3e,%.3e](km), v=%s(km/s)' % \ + (self.name, self.__class__.__name__, self.mass, self.density, + self.position[0], self.position[1], self.position[2], self.velocity) + + def ignore_gravity_with(self, body): + """ + 是否忽略指定对象的引力 + @param body: + @return: + """ + # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在对象物理学中是不严谨) + if self.ignore_mass: + return True + + return False + + def position_au(self): + """ + 获取对象的位置(单位:天文单位 A.U.) + @return: + """ + pos = self.position + pos_au = pos / AU + return pos_au + + # def change_velocity(self, dv): + # self.velocity += dv + # + # def move(self, dt): + # self.position += self.velocity * dt + + def reset(self): + """ + 重新设置初始速度和初始位置 + @return: + """ + self.position = copy.deepcopy(self.init_position) + self.velocity = copy.deepcopy(self.init_velocity) + + # def kinetic_energy(self): + # """ + # 计算动能(千焦耳) + # 表示动能,单位为焦耳j,m为质量,单位为千克,v为速度,单位为米/秒。 + # ek=(1/2).m.v^2 + # m(kg) v(m/s) -> j + # m(kg) v(km/s) -> kj + # """ + # v = self.velocity + # return 0.5 * self.mass * (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) + + @staticmethod + def build_objs_from_json(json_file): + """ + JSON文件转为对象对象 + @param json_file: + @return: + """ + bodies = [] + params = {} + from bodies import FixedStar, Body + with open(json_file, "r", encoding='utf-8') as read_content: + json_data = json.load(read_content) + for body_data in json_data["bodies"]: + try: + body_data = Obj.exp(body_data) # print(body_data) + except Exception as e: + err_msg = f"{json_file} 格式错误:" + str(e) + raise Exception(err_msg) + is_fixed_star = False + if "is_fixed_star" in body_data: + if body_data["is_fixed_star"]: + is_fixed_star = True + if is_fixed_star: + body_data.pop("is_fixed_star") + body = FixedStar(**body_data) + else: + has_rings = False + if "has_rings" in body_data: + if body_data["has_rings"]: + has_rings = True + body_data.pop("has_rings") + + if "rotation_speed" in body_data: + body = Body(**body_data) + if has_rings: + body.has_rings = True + else: + body_data.pop("rotation_speed") + body_data.pop("is_fixed_star") + body = Obj(**body_data) + + # [x, y, z]->[-y, z, x] + # body.init_velocity = [-body.init_velocity[1],body.init_velocity[2],body.init_velocity[0]] + # body.init_position = [-body.init_position[1],body.init_position[2],body.init_position[0]] + bodies.append(body) + if "params" in json_data: + params = json_data["params"] + # print(body.position_au()) + return bodies, params + + @staticmethod + def exp(body_data): + """ + 进行表达式分析,将表达式改为eval执行后的结果 + @param body_data: + @return: + """ + # + for k in body_data.keys(): + v = body_data[k] + if isinstance(v, str): + if v.startswith("$exp:"): + exp = v[5:] + body_data[k] = eval(exp) + elif isinstance(v, list): + for idx, item in enumerate(v): + if isinstance(item, str): + if item.startswith("$exp:"): + exp = item[5:] + v[idx] = eval(exp) + + return body_data + + +if __name__ == '__main__': + # build_bodies_from_json('../data/sun.json') + objs, params = Obj.build_objs_from_json('../data/sun_earth.json') + + for obj in objs: + print(obj) diff --git a/sim_scenes/func.py b/sim_scenes/func.py index b0dbd23..56a5d28 100644 --- a/sim_scenes/func.py +++ b/sim_scenes/func.py @@ -190,10 +190,10 @@ def create_solar_system_bodies(ignore_mass=False, init_velocity=None): sun = Sun(name="太阳", size_scale=0.5e2) # 太阳放大 50 倍,距离保持不变 bodies = [ sun, - Mercury(name="水星", size_scale=1e3), # 水星放大 1000 倍,距离保持不变 - Venus(name="金星", size_scale=1e3), # 金星放大 1000 倍,距离保持不变 - Earth(name="地球", size_scale=1e3), # 地球放大 1000 倍,距离保持不变 - Mars(name="火星", size_scale=1e3), # 火星放大 1000 倍,距离保持不变 + Mercury(name="水星", size_scale=0.3e3), # 水星放大 300 倍,距离保持不变 + Venus(name="金星", size_scale=0.3e3), # 金星放大 300 倍,距离保持不变 + Earth(name="地球", size_scale=0.3e3), # 地球放大 300 倍,距离保持不变 + Mars(name="火星", size_scale=0.3e3), # 火星放大 300 倍,距离保持不变 # Asteroids(name="小行星群", size_scale=3.2e2, # parent=sun), # 小行星群模拟(仅 ursina 模拟器支持) Jupiter(name="木星", size_scale=0.3e3), # 木星放大 300 倍,距离保持不变 diff --git a/sim_scenes/funny/dancing_with_jupiter.py b/sim_scenes/funny/dancing_with_jupiter.py index c115d39..03b77d3 100644 --- a/sim_scenes/funny/dancing_with_jupiter.py +++ b/sim_scenes/funny/dancing_with_jupiter.py @@ -21,8 +21,8 @@ if __name__ == '__main__': """ # 选择舞者 Dancer = Earth # 舞者为地球 - Dancer = Venus # 舞者为金星 - Dancer = Mars # 舞者为火星 + # Dancer = Venus # 舞者为金星 + # Dancer = Mars # 舞者为火星 bodies = [ Sun(size_scale=0.8e2), # 太阳放大 80 倍 diff --git a/sim_scenes/solar_system/speed_of_light.py b/sim_scenes/solar_system/speed_of_light.py index 32c4afe..b3f2864 100644 --- a/sim_scenes/solar_system/speed_of_light.py +++ b/sim_scenes/solar_system/speed_of_light.py @@ -12,9 +12,9 @@ from sim_scenes.solar_system.speed_of_light_init import SpeedOfLightInit # TODO: 三种不同的摄像机视角 camera_follow_light = None # 摄像机固定,不会跟随光 -# camera_follow_light = 'ForwardView' # 摄像机跟随光,方向是向前看 -# camera_follow_light = 'SideView' # 摄像机跟随光,方向是从侧面看 -# camera_follow_light = 'SideViewActualSize' # 摄像机跟随光,方向是从侧面看,天体是实际大小 +camera_follow_light = 'ForwardView' # 摄像机跟随光,方向是向前看 +camera_follow_light = 'SideView' # 摄像机跟随光,方向是从侧面看 +camera_follow_light = 'SideViewActualSize' # 摄像机跟随光,方向是从侧面看,天体是实际大小 # 实例化一个初始化对象(订阅事件,记录到达每个行星所需要的时间) init = SpeedOfLightInit(camera_follow_light) diff --git a/sim_scenes/solar_system/speed_of_light_init.py b/sim_scenes/solar_system/speed_of_light_init.py index 02d13f0..dbbf5de 100644 --- a/sim_scenes/solar_system/speed_of_light_init.py +++ b/sim_scenes/solar_system/speed_of_light_init.py @@ -85,7 +85,7 @@ class SpeedOfLightInit: @return: """ self.arrived_bodies.clear() # 重置存放记录光体已到达天体列表 - self.arrived_info = "距离[太阳]:${distance}\n\n" + self.arrived_info = "距离[太阳中心]:${distance}\n\n" if self.text_panel is not None: self.text_panel.text = self.arrived_info.replace("${distance}", "0 AU") @@ -141,6 +141,11 @@ class SpeedOfLightInit: camera.parent = self.light_body.planet self.light_body.planet.input = self.light_body_input camera.rotation_y = -15 + if hasattr(camera, "sky"): + # 摄像机跟随地球后,需要对深空背景进行调整,否则看到的是黑色背景 + camera.sky.scale = 800 + camera.clip_plane_near = 0.1 + camera.clip_plane_far = 1000000 # 取消订阅(防止 光体 的大小进行变化影响摄像机的视角) UrsinaEvent.on_body_size_changed_unsubscription(self.light_body.planet.change_body_scale) diff --git a/sim_scenes/solar_system/transit_of_venus_mercury.py b/sim_scenes/solar_system/transit_of_venus_mercury.py index 68ab862..56521e0 100644 --- a/sim_scenes/solar_system/transit_of_venus_mercury.py +++ b/sim_scenes/solar_system/transit_of_venus_mercury.py @@ -14,30 +14,49 @@ from simulators.ursina.ursina_event import UrsinaEvent if __name__ == '__main__': # 水星、金星凌日 - earth = Earth(name="地球") - sun = Sun(name="太阳", size_scale=5e1) # 太阳放大 20 倍 + earth = Earth(name="地球", rotation_speed=0, texture="transparent.png") # 地球纹理透明,不会挡住摄像机视线 + sun = Sun(name="太阳", size_scale=5e1) # 太阳放大 50 倍 bodies = [ sun, + earth, Mercury(name="水星", init_position=[0.384 * AU, 0, 0], init_velocity=[0, 0, 47.87], - size_scale=5e1), # 水星放大 10 倍,距离保持不变 + size_scale=5e1), # 水星放大 50 倍,距离保持不变 Venus(name="金星", init_position=[0.721 * AU, 0, 0], init_velocity=[0, 0, 35], - size_scale=5e1) # 金星放大 10 倍,距离保持不变 + size_scale=5e1) # 金星放大 50 倍,距离保持不变 ] def on_ready(): - camera_look_at(sun, rotation_x=None, rotation_y=None, rotation_z=0) - pass + from ursina import camera + # 摄像机跟随地球(模拟在地球上看到的效果) + camera.parent = earth.planet + + if hasattr(camera, "sky"): + # 摄像机跟随地球后,需要对深空背景进行调整,否则看到的是黑色背景 + camera.sky.scale = 800 + camera.clip_plane_near = 0.1 + camera.clip_plane_far = 1000000 + + # 让太阳的旋转速度放慢10倍 + sun.rotation_speed /= 10 + + + def on_timer_changed(time_data: TimeData): + # 时时刻刻的让地球看向太阳(摄像机跟随地球看向太阳) + earth.planet.look_at(sun.planet) + earth.planet.rotation_z = 0 UrsinaEvent.on_ready_subscription(on_ready) + UrsinaEvent.on_timer_changed_subscription(on_timer_changed) # 使用 ursina 查看的运行效果 # 常用快捷键: P:运行和暂停 O:重新开始 I:显示天体轨迹 # position = 左-右+、上+下-、前+后- - ursina_run(bodies, SECONDS_PER_DAY * 3, - position=earth.init_position) + ursina_run(bodies, SECONDS_PER_WEEK, + position=[0, 0, 0], # 以地球为中心的位置 + show_timer=True) diff --git a/simulators/ursina_simulator.py b/simulators/ursina_simulator.py index 8ce7fc7..1d8cc4f 100644 --- a/simulators/ursina_simulator.py +++ b/simulators/ursina_simulator.py @@ -169,6 +169,7 @@ class UrsinaSimulator(Simulator): # sky = SphereSky(texture=texture, scale=sky_scale) sky.scale = sky_scale + camera.sky = sky # sky.set_shader_input('texture_scale', Vec2(20, 20)) # 一定要够大,如果小于 Sky(texture=texture).scale = 50000,宇宙背景就会出现黑色方洞 if camera.clip_plane_far < sky_scale * 2: diff --git a/textures/transparent.png b/textures/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..3b64fe1804f79e00cdc691412b427e78d6f1c10b GIT binary patch literal 2929 zcmbuB_dgZ>7suaL-AhK+HIfyd3fX<6Y;wuCu1&JC??v{tM@9)(R`x8TOGvrN%H|qT zBqQ5R#d152oXZMw;k;S| z+dnu5)Dv#*4FK%&e?`$JyrTpFt*(oj8Uo?$;p5@$?BU6$rKZN`>E+?*f^q;LU?SJh z-zU^G!3->k_%MF0Uo9HfWIyol)~uzkwJtlSwLM{B~$b~y)b+z~lS3(t+4 zFSw3(Qh6R{5pVoZZi9EK^g~D>X?^|Z$5A6Ohj=`Q8Db2-%q*O|T;vMwAfxZekMkz! z!#jGL!YSF{v($V{%Y-&3noa;VsW1VdKGjOc6z@C$CGZ@*b&a`u?kuVSA#4ixlEJJ1 zGm(x0iv&HljCxo>4+p3=eE*mhqym7#1gi>w3VN`%tEET-#>-QtU|_uHN;w21P=L(q zn%5|mKLZ=%2&5=wYa1xQDGS`9ZmpsSH~%Q5Ng3Wm1N;)ru#Cz!AUso5v>))vQ3AEA zECW=0I#dM`BQ3M#)}GOLI0a}Wqzz;|7?Ebhr+ww}-`<>^=bvvw)u`ISZZJ7Q7X@-M zvISY-L@J%ZO8{^k41YP5Q^FFL7w488i73);XVIzKriXwc|JKZD;|oe^z@#@sny_A4 z8l&>;rl|I9aVxh36DGiU=D*`HUi!j{ zYh}p!!qeCaqqM!Cu6*fQ>pdpb3ya}bXbU;gRXB}W<;ST%R8w&kT+W(fs{0}-M^Bt( zei*Up3A4C7=^2^6vwo4or7=*ww6R8Dk;W977OZZmeSPcX^d%Q|0 z%T-;It>A*&VqUCw5*{iaVkdj}q@Eu?Z`71bji@!C`cCPq{HctW>v>m!_ppx)qg8l% zk=rm=-BpF~k42@!4}SalfRN{j_Tj^l42~~;+CF>{t6}rZj7zwi-G0eT)+9;Rp2zH8 zKQ`{xJX;dv8fAPq%Npd-Q>HdlBo?b_remg7CbTA4$J_uhg+@Jp+XHAxl!3wPhWYmZ)FnOQ~iSR2U4D z=Y5A%#L4GdXrT-I5aNch}rG(vEtFMSuRd4WM}aep9bi3)K!g)uNfn5 zzb~#3SE6?PFVV-)M|H#Pu5-F^262{h)N_hw>b#-(mXXD+WMALKnZVE9CFQVXraIdD? zu;w0mw|EcshcreS=KWVf267wAiISAF-|WZQ|8{%jf*;;|I#+g?;M|bh;aohdRs0WL zcq7(nK{f!U=!o;UVuzFY^Uub~15n16Gan@$)3p%oYvUGRnb-S zm?;XuZJK>4TRxlcv8*z?vcoSMJ;OTBnsj62#-5@3gyh=s8tq!v+J*Hy`?mW+`{?~4 zp8hM$JY#o#?go4L`1bpuJR4U_=49H-JIdQvOr5x$3cYi8WMOX}Ud@smE*Id%2uz zlD025eK$ny&^bo`zHgT_R39mVN#=a{NpMHkM1AZRZs_NepJI1Xb5e7Pk@POaW-9JBo;;&@W<^(S6)0v3=x4!2|D|Nj6lp&Tw%ZvI@b#t|MM15@5+;8Gwg3!7aW^ z&p4j`IS{0aWDjn_RwMA$@5k{0=3BD@EgGNM1>+52{RL}{aQ^V(o`m`JhhkLUbSoS$)3$N?XfY*HDlD$BncK8oY-=PfYyMuJ2o4g|C*Zy5q>^E$_6OTvbND#%$78a&ILpaa*IcfB6&! zy6z%PM#!G~$VTLJ5|=ylM=eGSs8tB(h2>Rn4qEfkHs zop2Q{@41v6?lFhwGYf>hSl#uR^b?t5&0Hh zoLDisK%uaS^!r6|F3%$#&F1>EP?F?=y>GW{l4)K+?sbK&gT=^{h0@2RdAWK@3g0uQ zFl3$MOgAe7t0pJ4p*$s+&xTIDHMw`^7ct3dv(a-`fNVD@RR5q-p-MqB)bnHx{?5{4 z)qzY{H@#Qov#Yy$cbN1o?9jD!)ceQZAGfUXF-@edHST(M;pr=Qp@zM`e*5b%f7%V5 zDXYMwfOp5u6DNNz9y0z3t2?c6XhqNdwQ02(YBy&jF+gMUPE*gm#m$Od2X@@1yoO=LJ06qYyK{xEv^4b8t5j_w{{#DlU5o$# literal 0 HcmV?d00001 -- GitLab