## three-d.pkg
# Compiled by:
#
src/lib/x-kit/widget/xkit-widget.sublib### "The competent programmer is fully aware
### of the limited size of his own skull.
### He therefore approaches his task with full humility,
### and avoids clever tricks like the plague."
###
### -- E.J. Dijkstra
stipulate
package f8b = eight_byte_float; # eight_byte_float is from
src/lib/std/eight-byte-float.pkg package wb = widget_base; # widget_base is from
src/lib/x-kit/widget/old/basic/widget-base.pkg package xc = xclient; # xclient is from
src/lib/x-kit/xclient/xclient.pkg package g2d = geometry2d; # geometry2d is from
src/lib/std/2d/geometry2d.pkgherein
package three_d
: (weak) Three_D # Three_D is from
src/lib/x-kit/widget/old/lib/three-d.api {
Relief = FLAT
| RAISED | SUNKEN | GROOVE | RIDGE;
#
fun draw3drect drawable ({ col, row, wide, high }, width) # Used by draw_box for FLAT, RAISED and SUNKEN.
=
{ point_list
=
[ { col, row => row+high },
{ col, row },
{ col => col+wide, row },
{ col => col+wide-width, row => row+width },
{ col => col+width, row => row+width },
{ col => col+width, row => row+high-width },
{ col, row => row+high }
];
r1 = { col, row=>row+high-width, wide, high=>width };
r2 = { col=>col+wide-width, row, wide=>width, high };
dblw = width + width;
if (wide < dblw or high < dblw)
#
\\ _ = ();
else
\\ { top, bottom }
=
{
xc::fill_box drawable bottom r1;
xc::fill_box drawable bottom r2;
xc::fill_polygon drawable top
{ verts=>point_list,
shape => xc::NONCONVEX_SHAPE
};
};
fi;
};
fun draw3drect2 drawable (box as ({ col, row, wide, high } ), width) # Used by draw_box for GROOVE and RIDGE.
=
{ half_width = width / 2;
half_width' = width - half_width;
outer = draw3drect drawable (box, half_width');
r' = { col => col+half_width',
row => row+half_width',
#
wide => wide - 2*half_width',
high => high - 2*half_width'
};
inner = draw3drect drawable (r', half_width);
\\ pens = { outer pens; inner { top=> pens.bottom, bottom=> pens.top }; };
};
fun draw_box drawable { box, width, relief }
=
case relief
#
FLAT =>
{ f = draw3drect drawable (box, width);
#
\\ ( { base, ... }: wb::Shades)
=
f { top=>base, bottom=>base };
};
RAISED
=>
{ f = draw3drect drawable (box, width);
#
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
SUNKEN
=>
{ f = draw3drect drawable (box, width);
#
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
RIDGE
=>
{ f = draw3drect2 drawable (box, width);
#
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
GROOVE
=>
{ f = draw3drect2 drawable (box, width);
#
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
esac;
fun draw_filled_box dr { box, relief=>FLAT, width } shades # Same as draw_box except we fill the interior of the box in shades.base.
=>
xc::fill_box dr shades.base box;
draw_filled_box dr { box, width=>0, relief => _ } shades
=>
xc::fill_box dr shades.base box;
draw_filled_box dr (a as { box=>{ col, row, wide, high }, width, ... } ) shades
=>
{ delta = width + width;
box' = { col=>col+width, row=>row+width, wide=>wide - delta, high=>high - delta }; # box' is nested 'width' inside box 'a'.
xc::fill_box dr shades.base box';
draw_box dr a shades;
};
end;
fun draw3dround_box drawable { box, width, c_wid, c_ht } # Used by draw_round_box for FLAT, RAISED and SUNKEN.
=
{ box -> { col, row, wide, high };
#
halfwidth = width / 2;
col = col + halfwidth;
row = row + halfwidth;
w = wide - 2*halfwidth;
h = high - 2*halfwidth;
w2 = c_wid+c_wid;
h2 = c_ht+c_ht;
my (ew, ew2) = if (w2 > w) (0, 0); else (c_wid, w2); fi;
my (eh, eh2) = if (h2 > h) (0, 0); else (c_ht, h2); fi;
\\ { top, bottom }
=
{ top = xc::clone_pen (top, [xc::p::LINE_WIDTH width]);
bottom = xc::clone_pen (bottom,[xc::p::LINE_WIDTH width]);
xc::draw_arcs drawable top
[
{ col=> col, row=> row, wide=> ew2, high=> eh2, angle1=> 180*64, angle2=> -90*64 },
{ col=> col+ew, row=> row, wide=> w - ew2, high=> 0, angle1=> 180*64, angle2=> -180*64 },
{ col=> col, row=> row+eh, wide=> 0, high=> h - eh2, angle1=> 270*64, angle2=> -180*64 },
{ col=> col+w - ew2, row=> row, wide=> ew2, high=> eh2, angle1=> 45*64, angle2=> 45*64 },
{ col=> col, row=> row+h - eh2, wide=> ew2, high=> eh2, angle1=> 225*64, angle2=> -45*64 }
];
xc::draw_arcs drawable bottom
[
{ col=> col+w - ew2, row=> row, wide=> ew2, high=> eh2, angle1=> 45*64, angle2=> -45*64 },
{ col=> col+w, row=> row+eh, wide=> 0, high=> h - eh2, angle1=> 90*64, angle2=> -180*64 },
{ col=> col+w - ew2, row=> row+h - eh2, wide=> ew2, high=> eh2, angle1=> 0, angle2=> -90*64 },
{ col=> col+ew, row=> row+h, wide=> w - ew2, high=> 0, angle1=> 0, angle2=> -180*64 },
{ col=> col, row=> row+h - eh2, wide=> ew2, high=> eh2, angle1=> 270*64, angle2=> -45*64 }
];
};
};
fun draw3dround_box2 drawable { box as { col, row, wide, high }, width, c_wid, c_ht } # Used by draw_round_box for GROOVE and RIDGE.
=
{ half_width = width / 2;
half_width' = width - half_width;
outer = draw3dround_box drawable
{ box, width=>half_width', c_wid, c_ht };
r' = { col => col+half_width',
row => row+half_width',
wide => wide - 2*half_width',
high => high - 2*half_width'
};
inner = draw3dround_box drawable
{ box=>r', width=>half_width, c_wid, c_ht };
\\ pens = { outer pens; inner { top=> pens.bottom, bottom=> pens.top }; };
};
fun draw_round_box drawable { box, width, c_wid, c_ht, relief }
=
case relief
#
FLAT => { f = draw3dround_box drawable { box, width, c_wid, c_ht };
\\ ( { base, ... }: wb::Shades)
=
f { top=>base, bottom=>base };
};
RAISED => { f = draw3dround_box drawable { box, width, c_wid, c_ht };
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
SUNKEN => { f = draw3dround_box drawable { box, width, c_wid, c_ht };
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
RIDGE => { f = draw3dround_box2 drawable { box, width, c_wid, c_ht };
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
GROOVE => { f = draw3dround_box2 drawable { box, width, c_wid, c_ht };
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
esac;
# The table below is used for a quick approximation in
# computing a new point parallel to a given line
# An index into the table is 128 times the slope of the
# original line (the slope must always be between 0.0
# and 1.0). The value of the table entry is 128 times
# the amount to displace the new line in row for each unit
# of perpendicular distance. In other words, the table
# maps from the tangent of an angle to the inverse of
# its cosine. If the slope of the original line is greater
# than 1, then the displacement is done in col rather than in row.
#
shift_table
=
{ fun compute i
=
{ tangent = (float i) / 128.0;
#
f8b::truncate ((128.0 / math::cos (math::atan tangent)) + 0.5);
};
v = vector::from_fn (129, compute);
\\ i = vector::get (v, i);
};
# Given two points on a line, compute a point on a
# new line that is parallel to the given line and
# a given distance away from it.
#
fun shift_line (p1 as { col, row }, p2, distance)
=
{ fun (<<) (w, i) = unt::to_int (unt::(<<) (unt::from_int w, i));
fun (>>) (w, i) = unt::to_int (unt::(>>) (unt::from_int w, i));
infix my << >>;
(g2d::point::subtract (p2, p1))
->
{ col=>dx, row=>dy };
my (dy, dy_neg) = if (dy < 0) (-dy, TRUE); else (dy, FALSE); fi;
my (dx, dx_neg) = if (dx < 0) (-dx, TRUE); else (dx, FALSE); fi;
fun adjust (dy, dx)
=
((distance * shift_table((dy << 0u7) / dx)) + 64) >> 0u7;
if (dy <= dx )
#
dy = adjust (dy, dx);
{ col, row=> row + (if dx_neg dy; else -dy;fi) };
else
dx = adjust (dx, dy);
{ col=> col + (if dy_neg -dx; else dx;fi), row };
fi;
};
# Find the intersection of two lines
# with the given endpoints.
# Return NULL if lines are parallel
#
fun intersect
( a1 as ({ col=>a1x, row=>a1y } ), a2,
b1 as ({ col=>b1x, row=>b1y } ), b2
)
=
{ my { col=>ax, row=>ay } = g2d::point::subtract (a2, a1);
my { col=>bx, row=>by } = g2d::point::subtract (b2, b1);
axby = ax * by;
bxay = bx * ay;
axbx = ax * bx;
ayby = ay * by;
fun solve (p, q)
=
{ my (p, q)
=
if (q < 0 ) (-p,-q);
else ( p, q); fi;
if (p < 0)
-(((-p) + q / 2) / q);
else
(p + (q / 2)) / q;
fi;
};
if (axby == bxay)
NULL;
else
col = solve (a1x*bxay - b1x*axby + (b1y - a1y)*axbx, bxay - axby);
row = solve (a1y*axby - b1y*bxay + (b1x - a1x)*ayby, axby - bxay);
(THE ({ col, row } ));
fi;
};
fun make_perp
( { col, row },
{ col=>col', row=>row' }
)
=
{ col=>col+(row'-row), row=>row-(col'-col) };
fun last2pts [] => raise exception lib_base::IMPOSSIBLE "three_d::last2Pts";
last2pts [v1, v2] => (v1, v2);
last2pts (v ! vs) => last2pts vs;
end;
/*
* draw3DPoly draws a polygon of given width. The widening occurs
* on the left of the polygon as it is traversed. If the width
* is negative, the widening occurs on the right. Duplicate points
* are ignored. If there are less than two distinct points, nothing
* is drawn.
*
* The main loop below (loop2) is executed once for each vertex in
* the polgon. At the beginning of each iteration things get like this:
*
* poly1 /
* * /
*
| /
* b1 * poly0
*
| |
*
| |
*
| |
*
| |
*
| |
*
| | p1 p2
* b2 *--------------------*
*
|
*
|
* *----*--------------------*
* poly2 newb1 newb2
*
* For each interation, we:
* (a) Compute poly2 (the border corner corresponding to p1)
* As part of this process, compute a new b1 and b2 value
* for the next side (p1-p2) of the polygon.
* (b) Draw the polygon (poly0, poly1, poly2, p1)
*
* The above situation doesn't exist until two points have
* been processed. We start with the last two points in the list
* (in loop0) to get an initial b1 and b2. Then, in loop1, we
* use the first point to get a new b1 and b2, with which we
* can calculate an initial poly1 (poly0 is the last point in
* the list). At this point, we can start the main loop.
*
* If two consecutive segments of the polygon are parallel,
* then things get more complex. (See findIntersect).
* Consider the following diagram:
*
* poly1
* *----b1-----------b2------a
* \
* \
* *---------*----------* b
* poly0 p2 p1 /
* /
* --*--------*----c
* newB1 newB2
*
* Instead of using the intersection and p1 as the last two points
* in the polygon and as poly1 and poly0 in the next iteration, we
* use a and b, and b and c, respectively.
*
* Do the computation in three stages:
* 1. Compute a point "perp" such that the line p1-perp
* is perpendicular to p1-p2.
* 2. Compute the points a and c by intersecting the lines
* b1-b2 and newb1-newb2 with p1-perp.
* 3. Compute b by shifting p1-perp to the right and
* intersecting it with p1-p2.
*/
fun draw3dpoly _ ([ ], _) _ => (); # Used by draw_poly for FLAT, RAISED and SUNKEN.
draw3dpoly _ ([_], _) _ => ();
draw3dpoly drawable (ps as (i_p ! _), width) { top, bottom }
=>
loop0 (p1, p2 ! ps)
where
(last2pts ps) -> (p1, p2);
fun calc_off_points (v1, v2)
=
{ b1 = shift_line (v1, v2, width);
#
(b1, g2d::point::add (b1, g2d::point::subtract (v2, v1)));
};
fun find_intersect (p1, p2, newb1, newb2, b1, b2)
=
case (intersect (newb1, newb2, b1, b2))
#
THE col => (col, p1, col);
#
NULL =>
(poly2, poly3, c)
where
perp = make_perp (p1, p2);
#
poly2 = the (intersect (p1, perp, b1, b2));
c = the (intersect (p1, perp, newb1, newb2));
shift1 = shift_line (p1, perp, width);
shift2 = g2d::point::add (shift1, g2d::point::subtract (perp, p1));
poly3 = the (intersect (p1, p2, shift1, shift2));
end;
esac;
fun draw (p0, p1, p2, p3)
=
{ (g2d::point::subtract (p3, p0))
->
{ col => dx, # We color lines (polygons) "bottom" if pointing into the lower-right halfplane from the origin, else "top".
row => dy #
}; # "top" /
# /
pen = if (dx > 0) if (dy <= dx) bottom; else top; fi; # O
elif (dy < dx) bottom; else top; fi; # / "bottom"
# /
xc::fill_polygon drawable pen
{ verts => [p0, p1, p2, p3],
shape => xc::CONVEX_SHAPE
};
};
fun loop2 (p1,[], b1, b2, poly0, poly1)
=>
if (p1 != i_p)
#
(calc_off_points (p1, i_p)) -> (newb1, newb2);
(find_intersect (p1, i_p, newb1, newb2, b1, b2)) -> (poly2, poly3, _);
draw (poly0, poly1, poly2, poly3);
fi;
loop2 (p1, p2 ! ps, b1, b2, poly0, poly1)
=>
if (p1 == p2)
#
loop2 (p1, ps, b1, b2, poly0, poly1);
else
(calc_off_points (p1, p2)) -> (newb1, newb2);
(find_intersect (p1, p2, newb1, newb2, b1, b2)) -> (poly2, poly3, c);
draw (poly0, poly1, poly2, poly3);
loop2 (p2, ps, newb1, newb2, poly3, c);
fi;
end;
fun loop1 (p1,[], _, _) => ();
loop1 (p1, p2 ! ps, b1, b2)
=>
if (p1 == p2)
#
loop1 (p1, ps, b1, b2);
else
(calc_off_points (p1, p2)) -> (newb1, newb2);
(find_intersect (p1, p2, newb1, newb2, b1, b2)) -> (poly2, poly3, c);
loop2 (p2, ps, newb1, newb2, poly3, c);
fi;
end;
fun loop0 (_,[]) => ();
loop0 (p1, p2 ! ps)
=>
if (p1 == p2)
#
loop0 (p2, ps);
else
(calc_off_points (p1, p2)) -> (b1, b2);
loop1 (p2, ps, b1, b2);
fi;
end;
end;
end;
fun draw3dpoly2 drawable (pts, width) # Used by draw_poly for GROOVE and RIDGE.
=
{ half_width = width / 2;
#
outer = draw3dpoly drawable (pts, half_width);
inner = draw3dpoly drawable (pts,-half_width);
\\ pens
=
{ outer pens;
inner { top=> pens.bottom, bottom=> pens.top };
};
};
fun draw_poly drawable { pts, width, relief }
=
case relief
#
FLAT => { f = draw3dpoly drawable (pts, width);
\\ ( { base, ... }: wb::Shades)
=
f { top=>base, bottom=>base };
};
RAISED => { f = draw3dpoly drawable (pts, width);
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
SUNKEN => { f = draw3dpoly drawable (pts, width);
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
RIDGE => { f = draw3dpoly2 drawable (pts, width);
\\ { light, dark, ... }
=
f { top=>light, bottom=>dark };
};
GROOVE => { f = draw3dpoly2 drawable (pts, width);
\\ { light, dark, ... }
=
f { top=>dark, bottom=>light };
};
esac;
};
end;