Pixel-Composer/scripts/_3D/_3D.gml

583 lines
16 KiB
Text
Raw Normal View History

2023-01-09 03:14:20 +01:00
enum CAMERA_PROJ {
ortho,
perspective
}
2022-01-13 05:24:03 +01:00
#region setup
2023-02-14 11:40:24 +01:00
globalvar PRIMITIVES, FORMAT_P, FORMAT_PT, FORMAT_PNT;
2022-01-13 05:24:03 +01:00
PRIMITIVES = ds_map_create();
2023-02-14 11:40:24 +01:00
vertex_format_begin();
vertex_format_add_position_3d();
FORMAT_P = vertex_format_end();
2022-01-13 05:24:03 +01:00
vertex_format_begin();
vertex_format_add_position_3d();
vertex_format_add_texcoord();
FORMAT_PT = vertex_format_end();
2022-12-12 09:08:03 +01:00
vertex_format_begin();
vertex_format_add_position_3d();
vertex_format_add_normal();
vertex_format_add_texcoord();
FORMAT_PNT = vertex_format_end();
2022-01-13 05:24:03 +01:00
#endregion
2023-05-03 21:42:17 +02:00
#region 3d obj
function VertexObject() constructor {
positions = [];
textures = [];
normals = [];
faces = [];
buffer = noone;
renderSurface = noone;
renderTexture = noone;
static addPosition = function(_pos, _merge = false) {
if(!_merge) {
array_push(positions, _pos);
return array_length(positions) - 1;
}
var ind = array_find(positions, _pos);
if(ind == -1) {
array_push(positions, _pos);
return array_length(positions) - 1;
}
return ind;
}
static addNormal = function(_nor, _merge = false) {
if(!_merge) {
array_push(normals, _nor);
return array_length(normals) - 1;
}
var ind = array_find(normals, _nor);
if(ind == -1) {
array_push(normals, _nor);
return array_length(normals) - 1;
}
return ind;
}
static addTexture = function(_tex, _merge = false) {
if(!_merge) {
array_push(textures, _tex);
return array_length(textures) - 1;
}
var ind = array_find(textures, _tex);
if(ind == -1) {
array_push(textures, _tex);
return array_length(textures) - 1;
}
return ind;
}
2023-05-08 10:50:42 +02:00
static addFace = function(v1 = [0, 0, 0], n1 = [0, 0, 0], t1 = [0, 0],
v2 = [0, 0, 0], n2 = [0, 0, 0], t2 = [0, 0],
v3 = [0, 0, 0], n3 = [0, 0, 0], t3 = [0, 0], _merge = false) {
2023-05-03 21:42:17 +02:00
var pi0 = addPosition(v1, _merge);
var pi1 = addPosition(v2, _merge);
var pi2 = addPosition(v3, _merge);
var ni0 = addNormal(n1, _merge);
var ni1 = addNormal(n2, _merge);
var ni2 = addNormal(n3, _merge);
var ti0 = addTexture(t1, _merge);
var ti1 = addTexture(t2, _merge);
var ti2 = addTexture(t3, _merge);
array_append(faces, [
[pi0, ni0, ti0],
[pi1, ni1, ti1],
[pi2, ni2, ti2],
]);
}
static createBuffer = function() {
if(buffer != noone) vertex_delete_buffer(buffer);
var VB = vertex_create_buffer();
vertex_begin(VB, FORMAT_PNT);
for( var i = 0; i < array_length(faces); i++ ) {
var face = faces[i];
var _pos = positions[face[0]];
var _nor = normals [face[1]];
var _tex = textures [face[2]];
vertex_add_pnt(VB, _pos, _nor, _tex);
}
vertex_end(VB);
vertex_freeze(VB);
buffer = VB;
return VB;
}
static submit = function(surface = noone) {
if(!is_surface(surface)) {
__submit();
return;
}
renderSurface = surface;
submitTexture(surface_get_texture(surface));
}
static submitTexture = function(texture) {
renderTexture = texture;
__submit();
}
static __submit = function() {
2023-05-16 21:28:16 +02:00
if(renderTexture == noone) return;
if(buffer == noone) return;
2023-05-03 21:42:17 +02:00
vertex_submit(buffer, pr_trianglelist, renderTexture);
}
static clone = function(_submit = true) {
var v = new VertexObject();
v.positions = array_clone(positions);
v.textures = array_clone(textures);
v.normals = array_clone(normals);
v.faces = array_clone(faces);
v.renderTexture = renderTexture;
if(_submit) v.createBuffer();
return v;
}
static destroy = function() {
vertex_delete_buffer(buffer);
}
}
#endregion
#region primitives
2022-01-13 05:24:03 +01:00
var _0 = -.5;
var _1 = .5;
2023-05-03 21:42:17 +02:00
var v = new VertexObject();
v.addFace( [_1, _0, 0], [0, 0, 1], [1, 0],
[_0, _0, 0], [0, 0, 1], [0, 0],
[_1, _1, 0], [0, 0, 1], [1, 1], );
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
v.addFace( [_1, _1, 0], [0, 0, 1], [1, 1],
[_0, _0, 0], [0, 0, 1], [0, 0],
[_0, _1, 0], [0, 0, 1], [0, 1], );
2022-12-12 09:08:03 +01:00
2023-05-03 21:42:17 +02:00
PRIMITIVES[? "plane"] = v;
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
var v = [];
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[0] = new VertexObject();
v[0].addFace( [_1, _0, _0], [0, 0, -1], [1, 0],
[_0, _0, _0], [0, 0, -1], [0, 0],
[_1, _1, _0], [0, 0, -1], [1, 1], );
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[0].addFace( [_1, _1, _0], [0, 0, -1], [1, 1],
[_0, _0, _0], [0, 0, -1], [0, 0],
[_0, _1, _0], [0, 0, -1], [0, 1], );
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[1] = new VertexObject();
v[1].addFace( [_1, _0, _1], [0, 0, 1], [0, 0],
[_0, _0, _1], [0, 0, 1], [1, 0],
[_1, _1, _1], [0, 0, 1], [0, 1], );
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[1].addFace( [_1, _1, _1], [0, 0, 1], [0, 1],
[_0, _0, _1], [0, 0, 1], [1, 0],
[_0, _1, _1], [0, 0, 1], [1, 1], );
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[2] = new VertexObject();
v[2].addFace( [_1, _0, _0], [0, -1, 0], [1, 0],
[_0, _0, _0], [0, -1, 0], [0, 0],
[_1, _0, _1], [0, -1, 0], [1, 1], );
2022-01-13 05:24:03 +01:00
2023-05-03 21:42:17 +02:00
v[2].addFace( [_1, _0, _1], [0, -1, 0], [1, 1],
[_0, _0, _0], [0, -1, 0], [0, 0],
[_0, _0, _1], [0, -1, 0], [0, 1], );
v[3] = new VertexObject();
v[3].addFace( [_1, _1, _0], [0, 1, 0], [1, 0],
[_0, _1, _0], [0, 1, 0], [0, 0],
[_1, _1, _1], [0, 1, 0], [1, 1], );
2023-02-14 11:40:24 +01:00
2023-05-03 21:42:17 +02:00
v[3].addFace( [_1, _1, _1], [0, 1, 0], [1, 1],
[_0, _1, _0], [0, 1, 0], [0, 0],
[_0, _1, _1], [0, 1, 0], [0, 1], );
2023-02-14 11:40:24 +01:00
2023-05-03 21:42:17 +02:00
v[4] = new VertexObject();
v[4].addFace( [_0, _1, _0], [-1, 0, 0], [1, 1],
[_0, _0, _0], [-1, 0, 0], [1, 0],
[_0, _1, _1], [-1, 0, 0], [0, 1], );
v[4].addFace( [_0, _1, _1], [-1, 0, 0], [0, 1],
[_0, _0, _0], [-1, 0, 0], [1, 0],
[_0, _0, _1], [-1, 0, 0], [0, 0], );
v[5] = new VertexObject();
v[5].addFace( [_1, _1, _0], [1, 0, 0], [0, 1],
[_1, _0, _0], [1, 0, 0], [0, 0],
[_1, _1, _1], [1, 0, 0], [1, 1], );
v[5].addFace( [_1, _1, _1], [1, 0, 0], [1, 1],
[_1, _0, _0], [1, 0, 0], [0, 0],
[_1, _0, _1], [1, 0, 0], [1, 0], );
PRIMITIVES[? "cube"] = v;
2023-02-14 11:40:24 +01:00
#endregion
2023-01-01 02:06:02 +01:00
#region helper
2023-05-03 21:42:17 +02:00
enum GIZMO_3D_TYPE {
move,
rotate,
scale
}
function _3d_node_init(iDim, gPos, gSca, iPos, iRot, iSca) {
2023-01-01 02:06:02 +01:00
VB = [];
use_normal = true;
TM = matrix_build(0, 0, 0, 0, 0, 0, 1, 1, 1);
cam = camera_create();
2023-01-09 03:14:20 +01:00
2023-01-01 02:06:02 +01:00
cam_view = matrix_build_lookat(0, 0, 1, 0, 0, 0, 0, 1, 0);
cam_proj = matrix_build_projection_ortho(1, 1, 1, 100);
2023-01-09 03:14:20 +01:00
camera_set_view_mat(cam, cam_view);
camera_set_proj_mat(cam, cam_proj);
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
drag_index = noone;
drag_sv = 0;
drag_delta = 0;
drag_prev = 0;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
drag_mx = 0;
drag_my = 0;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
input_dim = iDim;
global_pos = gPos;
global_sca = gSca;
input_pos = iPos;
input_rot = iRot;
input_sca = iSca;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
gizmo_hover = noone;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
tools = [
new NodeTool( "Transform", THEME.tools_3d_transform ),
new NodeTool( "Rotate", THEME.tools_3d_rotate ),
new NodeTool( "Scale", THEME.tools_3d_scale ),
];
}
function _3d_gizmo(active, _x, _y, _s, _mx, _my, _snx, _sny) {
var _gpos = inputs[| global_pos].getValue();
var _gsca = inputs[| global_sca].getValue();
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
var _pos = inputs[| input_pos].getValue();
var _rot = inputs[| input_rot].getValue();
var _sca = inputs[| input_sca].getValue();
var cx = _x + _gpos[0] * _s;
var cy = _y + _gpos[1] * _s;
var _qrot = new BBMOD_Quaternion().FromEuler(_rot[0], -_rot[1], -_rot[2]);
var _hover = noone;
var _hoverDist = 10;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
if(isUsingTool(0)) {
var ga = [];
var dir = 0;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
ga[0] = new BBMOD_Vec3(64, 0, 0);
ga[1] = new BBMOD_Vec3( 0, -64, 0);
ga[2] = new BBMOD_Vec3( 0, 0, 64);
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
for( var i = 0; i < 3; i++ ) {
ga[i] = _qrot.Rotate(ga[i]);
var th = 2 + (gizmo_hover == i);
if(drag_index == i) {
th = 3;
dir = point_direction(0, 0, ga[i].X, ga[i].Y);
} else if(drag_index != noone)
th = 1;
draw_set_color(COLORS.axis[i]);
draw_line_round_arrow(cx, cy, cx + ga[i].X, cy + ga[i].Y, th);
var _d = distance_to_line(_mx, _my, cx, cy, cx + ga[i].X, cy + ga[i].Y);
if(_d < _hoverDist) {
_hover = i;
_hoverDist = _d;
}
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
gizmo_hover = _hover;
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
if(drag_index != noone) {
var mAng = point_direction(cx, cy, _mx, _my);
var _n = BBMOD_VEC3_FORWARD;
var _mmx = _mx - cx;
var _mmy = _my - cy;
var _max = ga[drag_index].X;
var _may = ga[drag_index].Y;
var mAdj = dot_product(_mmx, _mmy, _max, _may);
if(drag_prev != undefined) {
_pos[drag_index] += (mAdj - drag_prev) / 8000;
if(inputs[| input_pos].setValue(_pos))
UNDO_HOLDING = true;
}
drag_prev = mAdj;
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
} else if(isUsingTool(1)) {
var _pa = [], pa = [];
var drx, dry, drz;
var _sub = 64;
for( var i = 0; i <= _sub; i++ ) {
var ang = i * 360 / _sub;
pa[0] = new BBMOD_Vec3(0, lengthdir_x(64, ang), lengthdir_y(64, ang));
pa[0] = _qrot.Rotate(pa[0]);
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
pa[1] = new BBMOD_Vec3(lengthdir_x(64, ang), 0, lengthdir_y(64, ang));
pa[1] = _qrot.Rotate(pa[1]);
2023-01-01 02:06:02 +01:00
2023-05-03 21:42:17 +02:00
pa[2] = new BBMOD_Vec3(lengthdir_x(64, ang), lengthdir_y(64, ang), 0);
pa[2] = _qrot.Rotate(pa[2]);
if(i) {
for( var j = 0; j < 3; j++ ) {
draw_set_color(COLORS.axis[j]);
var th = (_pa[j].Z >= 0) + 1 + (gizmo_hover == j);
if(drag_index == j)
th = 3;
else if(drag_index != noone)
th = 1;
if(_pa[j].Z >= 0 || i % 2 || drag_index == j) {
draw_line_round(cx + _pa[j].X, cy + _pa[j].Y, cx + pa[j].X, cy + pa[j].Y, th);
drx = point_direction(cx + _pa[j].X, cy + _pa[j].Y, cx + pa[j].X, cy + pa[j].Y);
}
var _d = distance_to_line(_mx, _my, cx + _pa[j].X, cy + _pa[j].Y, cx + pa[j].X, cy + pa[j].Y);
if(_d < _hoverDist) {
_hover = j;
_hoverDist = _d;
}
}
}
for( var j = 0; j < 3; j++ )
_pa[j] = pa[j];
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
gizmo_hover = _hover;
if(drag_index != noone) {
var mAng = point_direction(cx, cy, _mx, _my);
var _n = BBMOD_VEC3_FORWARD;
switch(drag_index) {
case 0 : _n = new BBMOD_Vec3(1.0, 0.0, 0.0); break;
case 1 : _n = new BBMOD_Vec3(0.0, 1.0, 0.0); break;
case 2 : _n = new BBMOD_Vec3(0.0, 0.0, 1.0); break;
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
var camVector = new BBMOD_Vec3(0.0, 0.0, 1.0);
if(drag_prev != undefined) {
var _currQ = new BBMOD_Quaternion().FromEuler(_rot[0], _rot[1], _rot[2]);
var _currR = new BBMOD_Quaternion().FromAxisAngle(_n, (mAng - drag_prev) * (_currQ.Rotate(_n).Z > 0? -1 : 1));
var _mulp = _currQ.Mul(_currR);
var _Nrot = new BBMOD_Matrix(_mulp.ToMatrix()).ToEuler();
if(inputs[| input_rot].setValue(_Nrot))
UNDO_HOLDING = true;
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
draw_set_color(COLORS._main_accent);
draw_line_dashed(cx, cy, _mx, _my, 2, 8);
drag_prev = mAng;
}
} else if(isUsingTool(2)) {
var ga = [];
ga[0] = new BBMOD_Vec3(64, 0, 0);
ga[1] = new BBMOD_Vec3( 0, -64, 0);
ga[2] = new BBMOD_Vec3( 0, 0, 64);
for( var i = 0; i < 3; i++ ) {
ga[i] = _qrot.Rotate(ga[i]);
var th = 2 + (gizmo_hover == i);
if(drag_index == i)
th = 3;
else if(drag_index != noone)
th = 1;
draw_set_color(COLORS.axis[i]);
draw_line_round_arrow_scale(cx, cy, cx + ga[i].X, cy + ga[i].Y, th);
var _d = distance_to_line(_mx, _my, cx, cy, cx + ga[i].X, cy + ga[i].Y);
if(_d < _hoverDist) {
_hover = i;
_hoverDist = _d;
}
}
gizmo_hover = _hover;
if(drag_index != noone) {
var mAng = point_direction(cx, cy, _mx, _my);
var _n = BBMOD_VEC3_FORWARD;
var _mmx = _mx - cx;
var _mmy = _my - cy;
var _max = ga[drag_index].X;
var _may = ga[drag_index].Y;
var mAdj = dot_product(_mmx, _mmy, _max, _may);
if(drag_prev != undefined) {
_sca[drag_index] += (mAdj - drag_prev) / 8000;
if(inputs[| input_sca].setValue(_sca))
UNDO_HOLDING = true;
2023-01-01 02:06:02 +01:00
}
2023-05-03 21:42:17 +02:00
drag_prev = mAdj;
2023-01-01 02:06:02 +01:00
}
}
2023-05-03 21:42:17 +02:00
if(drag_index != noone && mouse_release(mb_left)) {
drag_index = noone;
UNDO_HOLDING = false;
}
if(_hover != noone && mouse_press(mb_left, active)) {
drag_index = _hover;
drag_prev = undefined;
drag_mx = _mx;
drag_my = _my;
}
inputs[| global_pos].drawOverlay(active, _x, _y, _s, _mx, _my, _snx, _sny);
2023-01-01 02:06:02 +01:00
}
function _3d_local_transform(_lpos, _lrot, _lsca) {
matrix_stack_push(matrix_build(0, 0, 0, _lrot[0], _lrot[1], _lrot[2], 1, 1, 1));
2023-05-03 21:42:17 +02:00
matrix_stack_push(matrix_build(-_lpos[0], -_lpos[1], _lpos[2], 0, 0, 0, 1, 1, 1));
2023-01-01 02:06:02 +01:00
matrix_stack_push(matrix_build(0, 0, 0, 0, 0, 0, _lsca[0], _lsca[1], _lsca[2]));
matrix_set(matrix_world, matrix_stack_top());
}
function _3d_clear_local_transform() {
matrix_stack_pop();
matrix_stack_pop();
matrix_stack_pop();
}
2023-02-23 07:02:19 +01:00
function _3d_pre_setup(_outSurf, _dim, _pos, _sca, _ldir, _lhgt, _lint, _lclr, _aclr, _lpos, _lrot, _lsca, _cam, _pass = "diff", _scale = noone) {
2023-01-01 02:06:02 +01:00
_outSurf = surface_verify(_outSurf, _dim[0], _dim[1]);
2023-02-23 07:02:19 +01:00
var _proj = _cam.projection;
var _fov = _cam.fov;
var _applyLocal = _scale == noone? true : _scale.local;
var scaleDimension = _scale == noone? true : _scale.dimension;
2023-01-01 02:06:02 +01:00
var lightFor = [ -cos(degtorad(_ldir)), -_lhgt, -sin(degtorad(_ldir)) ];
gpu_set_ztestenable(true);
surface_set_target(_outSurf);
2023-03-19 09:17:39 +01:00
DRAW_CLEAR
2023-01-01 02:06:02 +01:00
2023-01-09 03:14:20 +01:00
var shader = sh_vertex_pnt_light;
if(_pass == "diff") shader = sh_vertex_pnt_light;
else if(_pass == "norm") shader = sh_vertex_normal_pass;
else if(_pass == "dept") shader = sh_vertex_depth_pass;
uniVertex_lightFor = shader_get_uniform(shader, "u_LightForward");
uniLightAmb = shader_get_uniform(shader, "u_AmbientLight");
uniLightClr = shader_get_uniform(shader, "u_LightColor");
uniLightInt = shader_get_uniform(shader, "u_LightIntensity");
uniLightNrm = shader_get_uniform(shader, "useNormal");
shader_set(shader);
2023-02-14 11:40:24 +01:00
shader_set_uniform_f_array_safe(uniVertex_lightFor, lightFor);
shader_set_uniform_f_array_safe(uniLightAmb, colorArrayFromReal(_aclr));
shader_set_uniform_f_array_safe(uniLightClr, colorArrayFromReal(_lclr));
2023-01-01 02:06:02 +01:00
shader_set_uniform_f(uniLightInt, _lint);
shader_set_uniform_i(uniLightNrm, use_normal);
2023-01-04 02:30:04 +01:00
2023-01-09 03:14:20 +01:00
var cam_view, cam_proj;
2023-02-14 11:40:24 +01:00
var dw = array_safe_get(_dim, 0);
var dh = array_safe_get(_dim, 1);
2023-01-09 03:14:20 +01:00
if(_proj == CAMERA_PROJ.ortho) {
cam_view = matrix_build_lookat(0, 0, 128, 0, 0, 0, 0, 1, 0);
2023-02-14 11:40:24 +01:00
cam_proj = matrix_build_projection_ortho(dw, dh, 0.1, 256);
2023-01-09 03:14:20 +01:00
} else {
var _adjFov = power(_fov / 90, 1 / 4) * 90;
var dist = _dim[0] / 2 * dtan(90 - _adjFov);
cam_view = matrix_build_lookat(0, 0, 1 + dist, 0, 0, 0, 0, 1, 0);
2023-02-14 11:40:24 +01:00
cam_proj = matrix_build_projection_perspective(dw, dh, dist, dist + 256);
2023-01-09 03:14:20 +01:00
}
var cam = camera_get_active();
2023-02-14 11:40:24 +01:00
camera_set_view_size(cam, dw, dh);
2023-01-09 03:14:20 +01:00
camera_set_view_mat(cam, cam_view);
camera_set_proj_mat(cam, cam_proj);
2023-01-01 02:06:02 +01:00
camera_apply(cam);
2023-01-09 03:14:20 +01:00
if(_proj == CAMERA_PROJ.ortho)
2023-02-23 07:02:19 +01:00
matrix_stack_push(matrix_build(dw / 2 - _pos[0], _pos[1] - dh / 2, 0, 0, 0, 0, (scaleDimension? dw : 1) * _sca[0], (scaleDimension? dh : 1) * _sca[1], 1));
2023-01-09 03:14:20 +01:00
else
2023-02-23 07:02:19 +01:00
matrix_stack_push(matrix_build(dw / 2 - _pos[0], _pos[1] - dh / 2, 0, 0, 0, 0, (scaleDimension? dw : 1) * _sca[0], (scaleDimension? dh : 1) * _sca[1], 1));
2023-01-09 03:14:20 +01:00
//matrix_stack_push(matrix_build(0, 0, 0, 0, 0, 0, 1, 1, 1));
2023-01-01 02:06:02 +01:00
if(_applyLocal) _3d_local_transform(_lpos, _lrot, _lsca);
matrix_set(matrix_world, matrix_stack_top());
2023-05-03 21:42:17 +02:00
return _outSurf;
2023-01-01 02:06:02 +01:00
}
function _3d_post_setup() {
shader_reset();
matrix_stack_clear();
matrix_set(matrix_world, MATRIX_IDENTITY);
gpu_set_ztestenable(false);
2023-01-09 03:14:20 +01:00
var cam = camera_get_active();
camera_set_view_mat(cam, matrix_build_lookat(0, 0, 1, 0, 0, 0, 0, 1, 0));
camera_set_proj_mat(cam, matrix_build_projection_ortho(1, 1, 0.1, 256));
camera_apply(cam);
2023-01-01 02:06:02 +01:00
surface_reset_target();
}
2022-01-13 05:24:03 +01:00
#endregion