You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

765 lines
24 KiB
Plaintext

#import "Window_Creation";
Debug :: #import "Debug";
Simp :: #import "Simp";
Input :: #import "Input";
#import "Math";
#import "Basic";
TIMESTEP: float = 1.0 / 60.0;
DEBUGGING :: true;
xy :: (x: int, y: int) -> Vector2 {
return xy(cast(float)x, cast(float)y);
}
normalize :: (using v: *Vector2) -> float {
sq := sqrt(x*x + y*y);
factor := 1.0 / sq;
x *= factor;
y *= factor;
return sq;
}
local_to_world :: (using b: Body, local_point: Vector2) -> Vector2 {
return rotate(local_point, angle) + pos;
}
world_to_local :: (using b: Body, world_point: Vector2) -> Vector2 {
// world = rotate(local, angle) + pos
// world - pos = rotate(local, angle)
// rotate(world - pos, -angle) = local
return rotate(world_point - pos, -angle);
}
normalize :: (v: Vector2) -> Vector2 #must {
normalize(*v);
return v;
}
normalize_or_zero :: inline (using _v: Vector2) -> Vector2 #must {
v := _v;
normalize_or_zero(*v);
return v;
}
length_squared :: inline (a: Vector2) -> float {
return a.x*a.x + a.y*a.y;
}
negative :: (v: Vector2) -> Vector2 #must {
return xy(-v.x, -v.y);
}
cross :: (a: Vector2, b: Vector2) -> float {
return a.x * b.y - a.y * b.x;
}
hit_impulse :: (body_pos: Vector2, mouse_pos: Vector2) -> Vector2 {
return .{0, -15.0};
//return normalize(body_pos - mouse_pos) * 15.0;
}
ShapeType :: enum {
Circle;
Rectangle;
};
degrees :: (d: float) -> float {
return d * PI/180.0;
}
/*
A transform is an offset, a rotation, and a scaling
transforms define new basis vectors which in turn define a new space, the space of that transform
when a transform transforms a point in one space, it moves it into its space. It takes whatever point you give it, and acts as if its offset was from the transform's point of view, not where the point came from.
An entity's transform, when applied to a point, converts from local to world space. The input is local, the output is world
The inverse does the opposite, world to local space.
*/
Transform2D :: struct {
pos: Vector2;
angle: float;
scale: Vector2 = .{1.0, 1.0};
};
approx_eq :: (v1: Vector2, v2: Vector2) -> bool {
return abs(v1.x - v2.x) < 0.01 && abs(v1.y - v2.y) < 0.01;
}
#run {
point := xy(5, 6);
t := Transform2D.{pos = .{1, 2}, angle = 0.0, scale = .{2.0, 1.0}};
print("% | % | %\n", inv(t), xform(inv(t), xform(t, point)), point);
assert(approx_eq(point, xform(inv(t), xform(t, point))));
}
inv :: (t: Transform2D) -> Transform2D {
// v is a vector, v || s means xy(v.x*s.x, v.y*s.y), elementwise product. For
// scaling vector
// inv(t) * t * pos = pos
// inv(t) * ( rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos ) = pos
// p' = rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos
// inv(t) * p' = pos
// in = inv(t)
// rotate(xy(p'.x*in.scale.x, p'.y*in.scale.y), in.angle) + in.pos = pos
// rotate(xy(p'.x*in.scale.x, p'.y*in.scale.y), in.angle) = pos - in.pos
// p' || in.scale = rotate(pos - in.pos, -in.angle)
// p' = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(pos || t.scale, t.angle) + t.pos = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(pos - in.pos, -in.angle) || 1.0/in.scale = rotate(pos || t.scale, t.angle) + t.pos
// ^^ trying to find all the in., so want to make the left side look like the right side
return .{
pos = -t.pos,
scale = .{1.0/t.scale.x, 1.0/t.scale.y},
angle = -t.angle,
};
// THIS IS WRONG: I think that: rotate(v, theta) || scale == rotate(v || scale, theta)
V :: Vector2.{0.5, 0.6};
THETA :: #run degrees(39.0);
SCALE :: Vector2.{2.0, 3.0};
apply_scale :: (to: Vector2, scale: Vector2) -> Vector2 {
return .{to.x*scale.x, to.y*scale.y};
}
#run {
/*
rotation:
x' = x * cos(angle) + y * -sin(angle)
y' = x * sin(angle) + y * cos(angle)
want to solve for inner
rotate(v, theta) || scale_in = rotate(v || inner, theta)
where x = v.x, y = v.y
(x * cos(angle) + y * -sin(angle))*scale_in.x = x * inner.x * cos(angle) + y * inner.y -sin(angle)
(x * sin(angle) + y * cos(angle))*scale_in.y = x * inner.x * sin(angle) + y * inner.y * cos(angle)
system of equations, all variables known except for inner. Solving for inner.x:
((x * cos(angle) + y * -sin(angle))*scale_in.x - y * inner.y -sin(angle)) / (x * cos(angle)) = inner.x
now reinserting into the other equation...
(x * sin(angle) + y * cos(angle))*scale_in.y = x * ((x * cos(angle) + y * -sin(angle))*scale_in.x - y * inner.y -sin(angle)) / (x * cos(angle)) * sin(angle) + y * inner.y * cos(angle)
(x * sin(angle) + y * cos(angle))*scale_in.y = (x * (x * cos(angle) + y * -sin(angle))*scale_in.x - x * y * inner.y -sin(angle)) / (x * cos(angle)) * sin(angle) + y * inner.y * cos(angle)
*/
/*
left := apply_scale(rotate(V, THETA), SCALE);
right := rotate(apply_scale(V, SCALE), THETA);
print("% %\n", left, right);
assert(approx_eq(left, right));*/
}
//#assert approx_eq(apply_scale(rotate(V, THETA), SCALE), rotate(apply_scale(V, SCALE), THETA));
return .{};
/*return .{
pos = -
}*/
}
xform :: (using t: Transform2D, p: Vector2) -> Vector2 {
result: Vector2 = p;
result.x *= scale.x;
result.y *= scale.y;
result = rotate(result, angle);
result += pos;
return result;
}
xform :: (from: Transform2D, other: Transform2D) -> Transform2D {
result: Transform2D;
result.pos = xform(from, other.pos);
result.angle = from.angle + other.angle;
result.scale.x = from.scale.x * other.scale.x;
result.scale.y = from.scale.y * other.scale.y;
return result;
}
xform_inv :: (using t: Transform2D, p: Vector2) -> Vector2 {
result: Vector2 = p;
result -= pos;
result = rotate(result, -angle);
result.x /= scale.x;
result.y /= scale.y;
return result;
}
Shape :: struct {
type: ShapeType;
offset: Vector2; // in local space of body
radius: float; // only valid on circle
halfsize: Vector2; // only valid on rectangle
invmass: float = 1.0;
inv_moment_of_inertia: float;
}
Body :: struct {
pos , vel , force : Vector2; // pos is in world space
angle, angle_vel, torque: float;
elasticity: float; // 1 is bouncy, 0 is absorbant
shape: Shape;
}
point_query :: (bodies: [] Body, world_point: Vector2) -> *Body {
for *bodies {
if length_squared(world_to_local(it, world_point)) < it.shape.radius*it.shape.radius return it;
}
return null;
}
Contact :: struct {
point_a: Vector2;
point_b: Vector2;
normal: Vector2;
separation: float; // positive when non-penetrating, negative when penetrating
time_of_impact: float;
body_a: *Body;
body_b: *Body;
};
// returns world point
center_of_mass :: (using b: Body) -> Vector2 {
return b.pos;
}
apply_impulse_linear :: (using b: *Body, impulse: Vector2) {
if shape.invmass != 0.0 {
vel += impulse * shape.invmass;
}
}
apply_impulse_angular :: (using b: *Body, angle_impulse: float) {
if shape.inv_moment_of_inertia != 0.0 {
angle_vel += angle_impulse * shape.inv_moment_of_inertia;
}
}
apply_impulse :: (using b: *Body, world_point: Vector2, impulse: Vector2) {
apply_impulse_linear(b, impulse);
r: Vector2 = center_of_mass(b) - world_point;
dL: float = cross(r, impulse);
apply_impulse_angular(b, dL);
}
intersect :: (a: *Body, b: *Body, c: *Contact) -> bool {
c.body_a = a;
c.body_b = b;
ab := b.pos - a.pos;
c.normal = normalize_or_zero(ab);
assert(a.shape.type == .Circle);
assert(b.shape.type == .Circle);
c.point_a = a.pos + c.normal * a.shape.radius;
c.point_b = b.pos - c.normal * b.shape.radius;
radius_ab: float = a.shape.radius + b.shape.radius;
lengthsqr: float = length_squared(ab);
if lengthsqr <= (radius_ab * radius_ab) {
return true;
} else {
return false;
}
}
resolve :: (using c: Contact) {
// collision impulse
elasticity: float = body_a.elasticity * body_b.elasticity;
vab: Vector2 = body_a.vel - body_b.vel;
dot_product: float = dot(vab, normal);
impulse_j_magnitude: float = -(1.0 + elasticity) * dot_product / (body_a.shape.invmass + body_b.shape.invmass);
impulse_j: Vector2 = normal * impulse_j_magnitude;
apply_impulse_linear(body_a, impulse_j);
apply_impulse_linear(body_b, -impulse_j);
// move outside eachother, center of mass of both bodies stays the same, but separate them.
ta: float = body_a.shape.invmass / (body_a.shape.invmass + body_b.shape.invmass);
tb: float = body_b.shape.invmass / (body_a.shape.invmass + body_b.shape.invmass);
ds: Vector2 = point_b - point_a;
body_a.pos += ds * ta;
body_b.pos -= ds * tb;
}
#scope_file;
// ------------------------------------------------------------------------
// demo stuff visualization/debugging stuff
window: Window_Type;
quit: bool;
dt: float;
panning: bool = false;
window_width: s32 = 1280;
window_height: s32 = 720;
// the camera is a transform which goes from its parent space (screen) to its space (world space)
camera: Transform2D = .{scale = .{0.01, 0.01}, pos = #run -xy(window_width, window_height)/2.0 * 0.01};
mouse_delta_screen: Vector2;
mouse_frozen: bool;
mouse_frozen_at: Vector2;
last_time: float64;
last_mouse_pos: Vector2;
dbgprint :: print; // so can be detected and removed
#scope_file;
normalize :: (using v: *Vector2) -> float {
sq := sqrt(x*x + y*y);
factor := 1.0 / sq;
x *= factor;
y *= factor;
return sq;
}
normalize :: (v: Vector2) -> Vector2 #must {
normalize(*v);
return v;
}
xy :: (x: int, y: int) -> Vector2 {
return xy(cast(float)x, cast(float)y);
}
#scope_export;
WHITE :: Vector4.{1.0, 1.0, 1.0, 1.0};
RED :: Vector4.{1.0, 0.0, 0.0, 1.0};
GREEN :: Vector4.{0.0, 1.0, 0.0, 1.0};
BLUE :: Vector4.{0.0, 0.0, 1.0, 1.0};
ORANGE :: Vector4.{1.0, cast(float)0xA5/ cast(float)255, 0.0, 1.0};
DrawingSettings :: struct {
world_space: bool = true;
color: Vector4 = WHITE;
};
line :: (using d: DrawingSettings, _from: Vector2, _to: Vector2, thickness: float = 1.0) {
width := thickness;
from := _from;
to := _to;
if world_space {
from = world_to_screen(from);
to = world_to_screen(to);
}
Simp.set_shader_for_color(true);
normal := rotate(unit_vector(to - from), PI/2.0);
Simp.immediate_quad(from + normal*width, from - normal*width, to + normal*width, to - normal*width, color = color);
}
vector :: (d: DrawingSettings, from: Vector2, vector: Vector2) {
line(d, from, from+vector);
arrow_head_length := length(vector) * 0.5;
// arrow head
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, PI/2.0 + PI/4.0));
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, -PI/2.0 + -PI/4.0));
}
LastingPip :: struct {
alive_for: float;
pos: Vector2;
d: DrawingSettings;
};
pips : [100] LastingPip;
push_pip :: (d: DrawingSettings, at: Vector2) {
for * pips
{
if it.alive_for <= 0.0
{
it.alive_for = 1.0;
it.pos = at;
it.d = d;
break;
}
}
}
draw_pips :: (dt: float) {
for * pips if it.alive_for > 0.0
{
d := it.d;
d.color.w *= it.alive_for;
pip(d, it.pos);
it.alive_for -= dt;
}
}
pip :: (using d: DrawingSettings, _at: Vector2, size: float = 0.05) {
at := _at;
Simp.set_shader_for_color(true);
if world_space {
at = world_to_screen(at);
size /= camera.scale.x;
}
Simp.immediate_quad(at + xy(-size, size), at + xy(size, size), at + xy(size, -size), at + xy(-size, -size), color = color);
}
font: *Simp.Dynamic_Font;
text :: (using d: DrawingSettings, t: string, p: Vector2) {
drawing_at := p;
if world_space {
drawing_at = world_to_screen(drawing_at);
}
Simp.draw_text(font, xx drawing_at.x, xx drawing_at.y, t, color = color);
}
init :: () {
window = create_window(window_width, window_height, "Test Kit");
// Actual render size in pixels can be different from the window dimensions we specified above (for example on high-resolution displays on macOS/iOS).
window_width, window_height = Simp.get_render_dimensions(window);
Simp.set_render_target(window);
font = Simp.get_font_at_size(".", "Roboto-Regular.ttf", 18);
assert(font != null);
}
// screen coordinates
mouse_screen :: () -> Vector2 {
if mouse_frozen return mouse_frozen_at;
x, y := get_mouse_pointer_position();
pos: Vector2 = xy(cast(float)x, cast(float)y);
// simp is lower left is (0, 0) and y+ is up
pos.y = window_height - pos.y;
return pos;
}
mouse_world :: () -> Vector2 {
return screen_to_world(mouse_screen());
}
// query quit to see if should quit. Call this in a loop
frame_start :: () {
dt = cast(float)(get_time() - last_time);
last_time = get_time();
Input.update_window_events();
mouse_delta_screen = mouse_screen() - last_mouse_pos; // using Input.mouse_delta_x seems to incorrectly accumulate
last_mouse_pos = mouse_screen();
for Input.get_window_resizes() {
Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window.
if it.window == window {
should_reinit := (it.width != window_width) || (it.height != window_height);
window_width = it.width;
window_height = it.height;
//if should_reinit my_init_fonts(); // Resize the font for the new window size.
}
}
camera.scale.x *= 1.0 - 0.1*Input.mouse_delta_z/120.0;
camera.scale.y = camera.scale.x;
if panning {
camera.pos -= mouse_delta_screen*camera.scale.x;
}
Simp.immediate_begin();
Simp.clear_render_target(0.0, 0.0, 0.0, 1.0);
// draw grid
grid_d: DrawingSettings = .{true,.{0.2, 0.2, 0.2, 0.5}};
text_d := grid_d;
text_d.color = .{0.6, 0.6, 0.6, 1.0};
help_text_max :: 8;
help_zoom_max :: 0.04;
for x: -30..30 {
line(grid_d, xy(x, 30), xy(x, -30));
if camera.scale.x <= help_zoom_max && abs(x) < help_text_max then text(text_d, tprint("%m", x), .{xx x, 0.0});
}
for y: -30..30 {
line(grid_d, xy(30, y), xy(-30, y));
if camera.scale.x <= help_zoom_max && abs(y) < help_text_max then text(text_d, tprint("%m", y), .{0.0, xx y});
}
draw_pips(dt);
if mouse_frozen pip(.{false, ORANGE}, mouse_frozen_at, size = 20.0);
// handle inputs
for Input.events_this_frame {
if it.type == .QUIT then quit = true;
if it.type == {
case .KEYBOARD;
if it.key_pressed {
if it.key_code == {
case .ESCAPE;
quit = true;
case #char "T";
if !mouse_frozen mouse_frozen_at = mouse_screen();
mouse_frozen = !mouse_frozen;
}
}
if it.key_code == .MOUSE_BUTTON_LEFT {
panning = cast(bool)it.key_pressed;
}
}
}
}
frame_end :: () {
Simp.immediate_flush();
Simp.swap_buffers(window);
}
screen_to_world :: (screen: Vector2) -> Vector2 {
return xform(camera, screen);
}
world_to_screen :: (world: Vector2) -> Vector2 {
return xform_inv(camera, world);
}
draw_body :: (d: DrawingSettings, b: Body) {
draw_shape(d, b.shape);
}
draw_shape :: (d: DrawingSettings, shape: Shape) {
#if false {
if shape.type == {
case .Circle;
points: int = 32;
if shape.radius > 100.0 points = 128;
drawing_circle_at: Vector2 = local_to_world(b, shape.offset);
for 0..points {
theta: float = (it/cast(float)points) * PI*2.0;
theta_next: float = (it+1)/cast(float)points * PI*2.0;
from := drawing_circle_at + .{cos(theta),sin(theta)} * shape.radius;
to := drawing_circle_at + .{cos(theta_next),sin(theta_next)} * shape.radius;
line(d, from, to);
}
pip(d, pos);
case .Rectangle;
using b.shape;
facing_to_right := rotate(xy(halfsize.x,0.0), angle);
facing_to_up := rotate(xy(0.0,halfsize.y), angle);
upper_right := pos + facing_to_right + facing_to_up;
upper_left := pos - facing_to_right + facing_to_up;
lower_left := pos - facing_to_right - facing_to_up;
lower_right := pos + facing_to_right - facing_to_up;
line(d, upper_left, upper_right);
line(d, upper_right, lower_right);
line(d, lower_right, lower_left);
line(d, lower_left, upper_left);
}
}
}
demo_circle :: () {
init();
bodies: [..]Body;
array_add(*bodies, .{pos = .{0.0, 0.0}, shape = .{type = .Circle, radius = 1.0}, elasticity = 0.5});
array_add(*bodies, .{pos = .{0.0, -1003.0}, shape = .{type = .Circle, radius = 1000.0, invmass = 0.0}, elasticity = 1.0}); // ground sphere
array_add(*bodies, .{pos = .{3.0, 0.0}, shape = .{type = .Circle, radius = 1.0}, elasticity = 0.3});
unprocessed_time: float = 0.0;
while !quit {
frame_start();
defer frame_end();
// process physics
unprocessed_time += dt;
{
dt: string = "do not use, physics processes with TIMESTEP not dt";
while unprocessed_time > TIMESTEP
{
defer unprocessed_time -= TIMESTEP;
if get_time() < 0.5
{
//apply_force_at_point(*rects[0], xy(3, 0), rects[0].pos + xy(-0.1, 0.03));
}
// iterating by index so can do a sort of iteration that avoids double checking
// collisions between bodies. This is important because if intersections are double
// tested then they may have operated on shapes which were just separated but not
// moved by velocity
for 0..bodies.count-1 {
a := *bodies[it];
// calculate moment of inertia
if a.shape.type == {
case .Circle;
a.shape.inv_moment_of_inertia = 1.0/(PI * pow(a.shape.radius, 4.0) / 4.0);
case;
assert(false);
}
// gravity must be impulse for some reason
apply_impulse_linear(a, .{0.0, -9.81 * (1.0 / a.shape.invmass) * TIMESTEP});
// collisions
for it+1..bodies.count-1 {
b := *bodies[it];
contact: Contact;
if intersect(a, b, *contact) {
resolve(contact);
}
}
}
for * bodies {
defer { it.force = .{}; it.torque = 0.0; }
it.vel += (it.force * it.shape.invmass) * TIMESTEP;
it.pos += it.vel * TIMESTEP;
it.angle_vel += (it.torque * it.shape.inv_moment_of_inertia) * TIMESTEP;
it.angle += it.angle_vel * TIMESTEP;
}
}
}
b := bodies[0];
pip(.{}, local_to_world(b, world_to_local(b, mouse_screen())));
hovering: *Body = point_query(bodies, screen_to_world(mouse_screen()));
for * bodies {
d: DrawingSettings = .{true, GREEN};
if hovering == it {
d.color = RED;
vector(d, screen_to_world(mouse_screen()), hit_impulse(it.pos, screen_to_world(mouse_screen())) * TIMESTEP);
}
draw_body(d, it);
}
for Input.events_this_frame {
if it.type == {
case .KEYBOARD;
if it.key_code == .MOUSE_BUTTON_LEFT {
panning = cast(bool)it.key_pressed;
to_tap := point_query(bodies, screen_to_world(mouse_screen()));
if to_tap != null apply_impulse(to_tap, mouse_world(), hit_impulse(to_tap.pos, mouse_screen()));
}
}
}
}
}
demo_transforms_print :: () {
point := xy(1, 1);
t: Transform2D;
t.pos = xy(5, 0);
t.angle = degrees(45.0);
print("%\n", xform(t, point));
print("%\n", xform_inv(t, xform(t, point)));
}
demo_transforms :: () {
init();
t: Transform2D;
point := xy(2, 1);
while !quit {
frame_start();
pip(.{color = GREEN}, t.pos);
pip(.{}, xform(t, point));
POINTS :: 64;
cur_point := point;
cur_point_inv := point;
START_COLOR :: WHITE;
END_COLOR :: BLUE;
END_COLOR_INV :: ORANGE;
for 0..POINTS-1 {
progress: float = cast(float)it / cast(float)POINTS;
cur_point = xform(t, cur_point);
cur_point_inv = xform_inv(t, cur_point_inv);
pip(.{color = lerp(START_COLOR, END_COLOR, progress)}, cur_point);
pip(.{color = lerp(START_COLOR, END_COLOR_INV, progress)}, cur_point_inv);
}
print("\n");
horizontal: float = 0.0;
vertical: float = 0.0;
for Input.input_button_states if it & .DOWN {
keycode: Input.Key_Code = xx it_index;
if keycode == #char "D" horizontal += 1.0;
if keycode == #char "A" horizontal -= 1.0;
if keycode == #char "W" vertical += 1.0;
if keycode == #char "S" vertical -= 1.0;
if keycode == .MOUSE_BUTTON_RIGHT t.pos += mouse_delta_screen*camera.scale.x;
}
print("should be equal: % %\n", point, xform_inv(t, xform(t, point)));
t.angle += horizontal*dt*degrees(45.0);
t.scale.x += vertical*dt;
print("%\n", t);
frame_end();
}
}
demo_transforms2 :: () {
init();
draw_transform :: (d: DrawingSettings, t: Transform2D) {
horizontal := xform(t, xy(1.0, 0.0));
vertical := xform(t, xy(0.0, 1.0));
line(d, t.pos, vertical);
line(d, t.pos, horizontal);
pip(d, t.pos);
}
t1: Transform2D;
t2: Transform2D;
point := xy(2, 1);
while !quit {
print("%\n", camera);
frame_start();
defer frame_end();
combined := xform(t1, t2);
draw_transform(.{color = GREEN}, t1);
draw_transform(.{color = BLUE}, t2);
draw_transform(.{color = ORANGE}, combined);
pip(.{}, xform(t1, point));
pip(.{color = ORANGE}, xform(combined, point));
horizontal: float;
vertical: float;
right_panning: Vector2;
for Input.input_button_states if it & .DOWN {
keycode: Input.Key_Code = xx it_index;
if keycode == #char "D" horizontal += 1.0;
if keycode == #char "A" horizontal -= 1.0;
if keycode == #char "W" vertical += 1.0;
if keycode == #char "S" vertical -= 1.0;
if keycode == .MOUSE_BUTTON_RIGHT right_panning = mouse_delta_screen*camera.scale.x;
}
t1.angle += horizontal*dt*degrees(45.0);
t2.angle += vertical*dt*degrees(45.0);
t1.pos += right_panning;
//t1.scale.x += horizontal*dt;
t2.pos += 2.0*right_panning;
}
}
demo_contact :: () {
init();
size := Vector2.{1.0, 1.0};
from := Shape.{type = .Rectangle, halfsize = size/2.0};
to := Shape.{type = .Rectangle, offset = xy(0, 1.0), halfsize = size/2.0};
while !quit {
frame_start();
frame_end();
}
}
//main :: () { demo_circle(); }
//main :: () { demo_transforms_print(); }
//main :: () { demo_transforms(); }
main :: () { demo_transforms2(); }
//main :: () { demo_contact(); }