%%\input epsf %%\def\newpage{\vfill\eject} %%\advance\vsize1in %%\let\ora\overrightarrow %%\def\title#1{\hrule\vskip1mm#1\par\vskip1mm\hrule\vskip5mm} %%\def\figure#1{\par\centerline{\epsfbox{#1}}} %%\title{{\bf 3D.MP: 3-DIMENSIONAL REPRESENTATIONS IN METAPOST}} %% version 1.34, 17 August 2003 %% {\bf Denis Roegel} ({\tt roegel@loria.fr}) %% This package provides definitions enabling the manipulation %% and animation of 3-dimensional objects. %% Such objects can be included in a \TeX{} file or used on web pages %% for instance. See the documentation enclosed in the distribution for %% more details. %% Thanks to John Hobby and Ulrik Vieth for helpful hints. %% PROJECTS FOR THE FUTURE: %% $-$ take light sources into account and show shadows and darker faces %% $-$ handle overlapping of objects ({\it obj\_name\/} can be used when %% going through all faces) if known three_d_version: expandafter endinput % avoids loading this package twice fi; message "*** 3d, v1.34 (c) D. Roegel, 17 August 2003 ***"; numeric three_d_version; three_d_version=1.34; % This package needs |3dgeom| in a few places. |3dgeom| also loads |3d| % but that's not a problem. % input 3dgeom; %%\newpage %%\title{Vector operations} % components of vector |i| def xval(expr i)=vec[i]x enddef; def yval(expr i)=vec[i]y enddef; def zval(expr i)=vec[i]z enddef; % vector (or point) equality (absolute version) def vec_eq_(expr i,j)= ((xval(i)=xval(j)) and (yval(i)=yval(j)) and (zval(i)=zval(j))) enddef; % vector (or point) equality (local version) def vec_eq(expr i,j)=vec_eq_(pnt(i),pnt(j)) enddef; % vector inequality (absolute version) def vec_neq_(expr i,j)=(not vec_eq_(i,j)) enddef; % vector inequality (local version) def vec_neq(expr i,j)=(not vec_eq(i,j)) enddef; % definition of vector |i| by its coordinates (absolute version) def vec_def_(expr i,xi,yi,zi)= vec[i]x:=xi;vec[i]y:=yi;vec[i]z:=zi; enddef; % definition of vector |i| by its coordinates (local version) def vec_def(expr i,xi,yi,zi)= vec_def_(pnt(i),xi,yi,zi) enddef; % a point is stored as a vector (absolute version) let set_point_ = vec_def_; % a point is stored as a vector (local version) let set_point = vec_def; def set_point_vec_(expr i,v)= set_point_(i,xval(v),yval(v),zval(v)) enddef; def set_point_vec(expr i,v)=set_point_vec_(pnt(i),v) enddef; let vec_def_vec_=set_point_vec_; let vec_def_vec=set_point_vec; % vector sum: |vec[k]| $\leftarrow$ |vec[i]|$+$|vec[j]| (absolute version) def vec_sum_(expr k,i,j)= vec[k]x:=vec[i]x+vec[j]x; vec[k]y:=vec[i]y+vec[j]y; vec[k]z:=vec[i]z+vec[j]z; enddef; % vector sum: |vec[k]| $\leftarrow$ |vec[i]|$+$|vec[j]| (local version) def vec_sum(expr k,i,j)=vec_sum_(pnt(k),pnt(i),pnt(j)) enddef; % vector translation: |vec[i]| $\leftarrow$ |vec[i]|$+$|vec[v]| def vec_translate_(expr i,v)=vec_sum_(i,i,v) enddef; % Here, the second parameter is absolute, because this is probably % the most common case. def vec_translate(expr i,v)=vec_translate_(pnt(i),v) enddef; % vector difference: |vec[k]| $\leftarrow$ |vec[i]|$-$|vec[j]| def vec_diff_(expr k,i,j)= vec[k]x:=vec[i]x-vec[j]x; vec[k]y:=vec[i]y-vec[j]y; vec[k]z:=vec[i]z-vec[j]z; enddef; def vec_diff(expr k,i,j)=vec_diff_(pnt(k),pnt(i),pnt(j)) enddef; % dot product of |vec[i]| and |vec[j]| vardef vec_dprod_(expr i,j)= (vec[i]x*vec[j]x+vec[i]y*vec[j]y+vec[i]z*vec[j]z) enddef; vardef vec_dprod(expr i,j)=vec_dprod_(pnt(i),pnt(j)) enddef; % modulus of |vec[i]|, absolute version % In the computation, we try to avoid overflows or underflows; % we perform a scaling in order to avoid losing too much % information in certain cases vardef vec_mod_(expr i)= save prod,m_; hide( new_vec(v_a); m_=max(abs(xval(i)),abs(yval(i)),abs(zval(i))); if m_>0:vec_mult_(v_a,i,1/m_);else:vec_def_vec_(v_a,vec_null);fi; prod=m_*sqrt(vec_dprod_(v_a,v_a)); free_vec(v_a); ) prod enddef; % modulus of |vec[i]|, local version % If the return value must be compared to 0, % use |vec_eq| with |vec_null| instead. vardef vec_mod(expr i)= vec_mod_(pnt(i)) enddef; % unit vector |vec[i]| corresponding to vector |vec[j]| % only non-null vectors are changed def vec_unit_(expr i,j)= if vec_mod_(j)>0: vec_mult_(i,j,1/vec_mod_(j)); else:vec_def_vec_(i,j); fi; enddef; def vec_unit(expr i,j)=vec_unit_(pnt(i),pnt(j)) enddef; % vector product: |vec[k]| $\leftarrow$ |vec[i]| $\land$ |vec[j]| def vec_prod_(expr k,i,j)= vec[k]x:=vec[i]y*vec[j]z-vec[i]z*vec[j]y; vec[k]y:=vec[i]z*vec[j]x-vec[i]x*vec[j]z; vec[k]z:=vec[i]x*vec[j]y-vec[i]y*vec[j]x; enddef; def vec_prod(expr k,i,j)=vec_prod_(pnt(k),pnt(i),pnt(j)) enddef; % scalar multiplication: |vec[j]| $\leftarrow$ |vec[i]*v| (absolute version) def vec_mult_(expr j,i,v)= vec[j]x:=v*vec[i]x;vec[j]y:=v*vec[i]y;vec[j]z:=v*vec[i]z; enddef; % scalar multiplication: |vec[j]| $\leftarrow$ |vec[i]*v| (local version) def vec_mult(expr j,i,v)=vec_mult_(pnt(j),pnt(i),v) enddef; % middle of two points (absolute version) def mid_point_(expr k,i,j)= vec_sum_(k,i,j);vec_mult_(k,k,.5); enddef; % middle of two points (local version) def mid_point(expr k,i,j)= mid_point_(pnt(k),pnt(i),pnt(j)); enddef; %%\newpage %%\title{Vector rotation} % Rotation of |vec[v]| around |vec[axis]| by an angle |alpha| %% The vector $\vec{v}$ is first projected on the axis %% giving vectors $\vec{a}$ and $\vec{h}$: %%\figure{vect-fig.9} %% If we set %% $\vec{b}={\ora{axis}\over \left\Vert\vcenter{\ora{axis}}\right\Vert}$, %% the rotated vector $\vec{v'}$ is equal to $\vec{h}+\vec{f}$ %% where $\vec{f}=\cos\alpha \cdot \vec{a} + \sin\alpha\cdot \vec{c}$. %% and $\vec{h}=(\vec{v}\cdot\vec{b})\vec{b}$ %%\figure{vect-fig.10} % The rotation is independent of |vec[axis]|'s module. % |v| = old and new vector % |axis| = rotation axis % |alpha| = rotation angle % vardef vec_rotate_(expr v,axis,alpha)= new_vec(v_a);new_vec(v_b);new_vec(v_c); new_vec(v_d);new_vec(v_e);new_vec(v_f); new_vec(v_g);new_vec(v_h); vec_mult_(v_b,axis,1/vec_mod_(axis)); vec_mult_(v_h,v_b,vec_dprod_(v_b,v)); % projection of |v| on |axis| vec_diff_(v_a,v,v_h); vec_prod_(v_c,v_b,v_a); vec_mult_(v_d,v_a,cosd(alpha)); vec_mult_(v_e,v_c,sind(alpha)); vec_sum_(v_f,v_d,v_e); vec_sum_(v,v_f,v_h); free_vec(v_h);free_vec(v_g); free_vec(v_f);free_vec(v_e);free_vec(v_d); free_vec(v_c);free_vec(v_b);free_vec(v_a); enddef; % The second parameter is left absolute because this is probably the most % common case. vardef vec_rotate(expr v,axis,alpha)=vec_rotate_(pnt(v),axis,alpha) enddef; %%\newpage %%\title{Operations on objects} % |iname| is the handler for an instance of an object of class |name| % |iname| must be a letter string % |vardef| is not used because at some point we give other names % to |assign_obj| with |let| and this cannot be done with |vardef|. % (see MFbook for details) def assign_obj(expr iname,name)= begingroup save tmpdef; string tmpdef; % we need to add double quotes (char 34) tmpdef="def " & iname & "_class=" & ditto & name & ditto & " enddef"; scantokens tmpdef; def_obj(iname); endgroup enddef; % |name| is the the name of an object instance % It must be made only of letters (or underscores), but no digits. def def_obj(expr name)= scantokens begingroup save tmpdef;string tmpdef; tmpdef="def_" & obj_class_(name) & "(" & ditto & name & ditto & ")"; tmpdef endgroup enddef; % This macro puts an object back where it was right at the beginning, % or rather, where the |set| definition puts it (which may be different % than the initial position, in case it depends on parameters). % |iname| is the name of an object instance. vardef reset_obj(expr iname)= save tmpdef; string tmpdef; define_current_point_offset_(iname); tmpdef="set_" & obj_class_(iname) & "_points"; scantokens tmpdef(iname); enddef; % Put an object at position given by |pos| (a vector) and % with orientations given by angles |psi|, |theta|, |phi|. % The object is scaled by |scale|. % |iname| is the name of an object instance. % If the shape of the object has been changed since it was % created, these changes are lost. vardef put_obj(expr iname,pos,scale,psi,theta,phi)= reset_obj(iname);scale_obj(iname,scale); new_vec(v_x);new_vec(v_y);new_vec(v_z); vec_def_vec_(v_x,vec_I); vec_def_vec_(v_y,vec_J); vec_def_vec_(v_z,vec_K); rotate_obj_abs_pv(iname,point_null,v_z,psi); vec_rotate_(v_x,v_z,psi);vec_rotate_(v_y,v_z,psi); rotate_obj_abs_pv(iname,point_null,v_y,theta); vec_rotate_(v_x,v_y,theta);vec_rotate_(v_z,v_y,theta); rotate_obj_abs_pv(iname,point_null,v_x,phi); vec_rotate_(v_y,v_x,phi);vec_rotate_(v_z,v_x,phi); free_vec(v_z);free_vec(v_y);free_vec(v_x); translate_obj(iname,pos); enddef; %%\newpage %%\title{Rotation, translation and scaling of objects} % Rotation of an object instance |name| around an axis % going through a point |p| (local to the object) % and directed by vector |vec[v]|. The angle of rotation is |a|. vardef rotate_obj_pv(expr name,p,v,a)= define_current_point_offset_(name); rotate_obj_abs_pv(name,pnt(p),v,a); enddef; vardef rotate_obj_abs_pv(expr name,p,v,a)= define_current_point_offset_(name); new_vec(v_a); for i:=1 upto obj_points_(name): vec_diff_(v_a,pnt(i),p); vec_rotate_(v_a,v,a); vec_sum_(pnt(i),v_a,p); endfor; free_vec(v_a); enddef; % Rotation of an object instance |name| around an axis % going through a point |p| (local to the object) % and directed by vector $\ora{pq}$. The angle of rotation is |a|. vardef rotate_obj_pp(expr name,p,q,a)= define_current_point_offset_(name); new_vec(v_a);new_vec(axis); vec_diff_(axis,pnt(q),pnt(p)); for i:=1 upto obj_points_(name): vec_diff_(v_a,pnt(i),pnt(p)); vec_rotate_(v_a,axis,a); vec_sum_(pnt(i),v_a,pnt(p)); endfor; free_vec(axis);free_vec(v_a); enddef; % Translation of an object instance |name| by a vector |vec[v]|. vardef translate_obj(expr name,v)= define_current_point_offset_(name); for i:=1 upto obj_points_(name): vec_sum_(pnt(i),pnt(i),v); endfor; enddef; % Scalar multiplication of an object instance |name| by a scalar |v|. vardef scale_obj(expr name,v)= define_current_point_offset_(name); for i:=1 upto obj_points_(name): vec_mult(i,i,v); endfor; enddef; %%\newpage %%\title{Functions to build new points in space} % Rotation in a plane: this is useful to define a regular polygon. % |k| is a new point obtained from point |j| by rotation around |o| % by a angle $\alpha$ equal to the angle from |i| to |j|. %%\figure{vect-fig.11} vardef rotate_in_plane_(expr k,o,i,j)= save cosalpha,sinalpha,alpha; new_vec(v_a);new_vec(v_b);new_vec(v_c); vec_diff_(v_a,i,o);vec_diff_(v_b,j,o);vec_prod_(v_c,v_a,v_b); cosalpha=vec_dprod_(v_a,v_b)/vec_mod_(v_a)/vec_mod_(v_b); sinalpha=sqrt(1-cosalpha**2); alpha=angle((cosalpha,sinalpha)); vec_rotate_(v_b,v_c,alpha); vec_sum_(k,o,v_b); free_vec(v_c);free_vec(v_b);free_vec(v_a); enddef; vardef rotate_in_plane(expr k,o,i,j)= rotate_in_plane_(pnt(k),o,pnt(i),pnt(j)) enddef; % Build a point on a adjacent face. %% The middle $m$ of points $i$ and $j$ is such that %% $\widehat{(\ora{om},\ora{mc})}=\alpha$ %% This is useful to define regular polyhedra %%\figure{vect-fig.7} vardef new_face_point_(expr c,o,i,j,alpha)= new_vec(v_a);new_vec(v_b);new_vec(v_c);new_vec(v_d);new_vec(v_e); vec_diff_(v_a,i,o);vec_diff_(v_b,j,o); vec_sum_(v_c,v_a,v_b); vec_mult_(v_d,v_c,.5); vec_diff_(v_e,i,j); vec_sum_(c,v_d,o); vec_rotate_(v_d,v_e,alpha); vec_sum_(c,v_d,c); free_vec(v_e);free_vec(v_d);free_vec(v_c);free_vec(v_b);free_vec(v_a); enddef; vardef new_face_point(expr c,o,i,j,alpha)= new_face_point_(pnt(c),pnt(o),pnt(i),pnt(j),alpha) enddef; vardef new_abs_face_point(expr c,o,i,j,alpha)= new_face_point_(c,o,pnt(i),pnt(j),alpha) enddef; %%\newpage %%\title{Computation of the projection of a point on the ``screen''} % |p| is the projection of |m| % |m| = point in space (3 coordinates) % |p| = point of the intersection plane %%\figure{vect-fig.8} vardef project_point(expr p,m)= save tmpalpha; new_vec(v_a);new_vec(v_b); if projection_type=2: % oblique if point_in_plane_p_pl_(m)(projection_plane): % |m| is on the projection plane vec_diff_(v_a,m,ObliqueCenter_); y[p]:=drawing_scale*vec_dprod_(v_a,ProjJ_); x[p]:=drawing_scale*vec_dprod_(v_a,ProjK_); else: % |m| is not on the projection plane new_line_(l)(m,ObliqueCenter_); vec_diff_(l2,l2,Obs); vec_sum_(l2,l2,m); % (the direction does not depend on Obs) if def_inter_p_l_pl_(v_a)(l)(projection_plane): vec_diff_(v_a,v_a,ObliqueCenter_); y[p]:=drawing_scale*vec_dprod_(v_a,ProjJ_); x[p]:=drawing_scale*vec_dprod_(v_a,ProjK_); else: message "Point " & decimal m & " cannot be projected"; x[p]:=too_big_;y[p]=too_big_; fi; free_line(l); fi; else: vec_diff_(v_b,m,Obs); % vector |Obs|-|m| % |vec[v_a]| is |vec[v_b]| expressed in (|ObsI_|,|ObsJ_|,|ObsK_|) % coordinates. vec[v_a]x:=vec[IObsI_]x*vec[v_b]x +vec[IObsJ_]x*vec[v_b]y+vec[IObsK_]x*vec[v_b]z; vec[v_a]y:=vec[IObsI_]y*vec[v_b]x +vec[IObsJ_]y*vec[v_b]y+vec[IObsK_]y*vec[v_b]z; vec[v_a]z:=vec[IObsI_]z*vec[v_b]x +vec[IObsJ_]z*vec[v_b]y+vec[IObsK_]z*vec[v_b]z; if vec[v_a]x not drawn"; x[p]:=too_big_;y[p]=too_big_; else: if (angle(vec[v_a]x,vec[v_a]z)>h_field/2) or (angle(vec[v_a]x,vec[v_a]y)>v_field/2): message "Point " & decimal m & " out of screen -> not drawn"; x[p]:=too_big_;y[p]=too_big_; else: if projection_type=0: % central perspective tmpalpha:=Obs_dist/vec[v_a]x; else: tmpalpha:=1; % parallel fi; y[p]:=drawing_scale*tmpalpha*vec[v_a]y; x[p]:=drawing_scale*tmpalpha*vec[v_a]z; fi; fi; fi; free_vec(v_b);free_vec(v_a); enddef; % At some point, we may need to do an oblique projection % of vectors |ObsK_| and |ObsI_| on a plane, and to normalize % and orthogonalize the projections (with the projection of |ObsK_| % keeping the same direction). This is done here, % where we take two vectors, a direction (line) and % a plane, and return two vectors. This function assumes % there is an intersection between line |l| and plane |p|. % We do not test it here. vardef project_vectors(expr va,vb)(expr k,i)(text l)(text p)= save vc;new_vec(vc); if proj_v_v_l_pl_(va,k)(l)(p): % |va| is the projection of vector |k| else: message "THIS SHOULD NOT HAPPEN"; fi; if proj_v_v_l_pl_(vb,i)(l)(p): % |vb| is the projection of vector |i| else: message "THIS SHOULD NOT HAPPEN"; fi; % now, we orthonormalize these vectors: vec_prod_(vc,va,vb); vec_unit_(va,va);vec_unit_(vc,vc);vec_prod_(vb,vc,va); free_vec(vc); enddef; % Object projection % This is a mere iteration on |project_point| def project_obj(expr name)= define_current_point_offset_(name); for i:=1 upto obj_points_(name): project_point(ipnt_(i),pnt(i));endfor; enddef; % Projection screen vardef show_projection_screen= save dx,dy; dx=Obs_dist*sind(h_field/2)/cosd(h_field/2); dy=Obs_dist*sind(v_field/2)/cosd(v_field/2); new_vec(pa);new_vec(pb);new_vec(pc);new_vec(pd);new_vec(op); new_vec(w);new_vec(h); vec_mult_(op,ObsI_,Obs_dist);vec_sum_(op,op,Obs); % center of screen vec_mult_(w,ObsK_,dx);vec_mult_(h,ObsJ_,dy); vec_sum_(pa,op,w);vec_sum_(pa,pa,h); % upper right corner vec_mult_(w,w,-2);vec_mult_(h,h,-2); vec_sum_(pb,pa,w);vec_sum_(pc,pb,h);vec_sum_(pd,pa,h); message "Screen at corners:"; show_point("urcorner: ",pa); show_point("ulcorner: ",pb); show_point("llcorner: ",pc); show_point("lrcorner: ",pd); show_point("Obs:",Obs); free_vec(h);free_vec(w); free_vec(op);free_vec(pd);free_vec(pc);free_vec(pb);free_vec(pa); enddef; %%\newpage %%\title{Draw one face, hiding it if it is hidden} % The order of the vertices determines what is the visible side % of the face. The order must be clockwise when the face is seen. % |drawhidden| is a boolean; if |true| only hidden faces are drawn; if |false|, % only visible faces are drawn. Therefore, |draw_face| is called twice % by |draw_faces|. vardef draw_face(text vertices)(expr col,drawhidden)= save p,num,overflow,i,j,k,nv; path p;boolean overflow; overflow=false; forsuffixes $=vertices: if z[ipnt_($)]=(too_big_,too_big_):overflow:=true; fi; exitif overflow; endfor; if overflow: message "Face can not be drawn, due to overflow"; else: p=forsuffixes $=vertices:z[ipnt_($)]--endfor cycle; % we do now search for three distinct and non-aligned suffixes: % usually, the first three suffixes do new_vec(normal_vec);new_vec(v_a);new_vec(v_b);new_vec(v_c); % first, we copy all the indexes in an array, so that % it is easier to go through them i=1; % num0 is not used forsuffixes $=vertices:num[i]=$;i:=i+1;endfor; nv=i-1; for $:=1 upto nv: for $$:=$+1 upto nv: for $$$:=$$+1 upto nv: vec_diff_(v_a,pnt(num[$$]),pnt(num[$])); vec_diff_(v_b,pnt(num[$$$]),pnt(num[$$])); vec_prod_(normal_vec,v_a,v_b); exitif vec_neq_(normal_vec,vec_null); % |vec_mod_| must not be used for such a test endfor; exitif vec_neq_(normal_vec,vec_null); endfor; exitif vec_neq_(normal_vec,vec_null); endfor; if projection_type=0: % perspective vec_diff_(v_c,pnt(num1),Obs); else: % parallel vec_def_vec_(v_c,ObsI_); fi; if filled_faces: if vec_dprod_(normal_vec,v_c)<0: fill p withcolor col;drawcontour(p,contour_width,contour_color)(); else: % |draw p dashed evenly;| if this is done, you must ensure % that hidden faces are (re)drawn at the end fi; else: if vec_dprod_(normal_vec,v_c)<0:%visible if not drawhidden:drawcontour(p,contour_width,contour_color)();fi; else: % hidden if drawhidden: drawcontour(p,contour_width,contour_color)(dashed evenly); fi; fi; fi; free_vec(v_c);free_vec(v_b);free_vec(v_a);free_vec(normal_vec); fi; enddef; % |p| is the path to draw (a face contour), |thickness| is the pen width % |col| is the color and |type| is a line modifier. def drawcontour(expr p,thickness,col)(text type)= if draw_contours and (thickness>0): pickup pencircle scaled thickness; draw p withcolor background; % avoid strange overlapping dashes draw p type withcolor col; pickup pencircle scaled .4pt; fi; enddef; %%\newpage % Variables for face handling. First, we have an array for lists of vertices % corresponding to faces. string face_points_[];% analogous to |vec| arrays % Then, we have an array of colors. A color needs to be a string % representing an hexadecimal RGB coding of a color. string face_color_[]; % |name| is the name of an object instance vardef draw_faces(expr name)= save tmpdef;string tmpdef; define_current_face_offset_(name); % first the hidden faces (dashes must be drawn first): for i:=1 upto obj_faces_(name): tmpdef:="draw_face(" & face_points_[face(i)] & ")(hexcolor(" & ditto & face_color_[face(i)] & ditto & "),true)";scantokens tmpdef; endfor; % then, the visible faces: for i:=1 upto obj_faces_(name): tmpdef:="draw_face(" & face_points_[face(i)] & ")(hexcolor(" & ditto & face_color_[face(i)] & ditto & "),false)";scantokens tmpdef; endfor; enddef; % Draw point |n| of object instance |name| vardef draw_point(expr name,n)= define_current_point_offset_(name); project_point(ipnt_(n),pnt(n)); if z[ipnt_(n)] <> (too_big_,too_big_): pickup pencircle scaled 5pt; drawdot(z[ipnt_(n)]); pickup pencircle scaled .4pt; fi; enddef; vardef draw_axes(expr r,g,b)= project_point(1,vec_null); project_point(2,vec_I); project_point(3,vec_J); project_point(4,vec_K); if (z1<>(too_big_,too_big_)): if (z2<>(too_big_,too_big_)): drawarrow z1--z2 dashed evenly withcolor r; fi; if (z3<>(too_big_,too_big_)): drawarrow z1--z3 dashed evenly withcolor g; fi; if (z4<>(too_big_,too_big_)): drawarrow z1--z4 dashed evenly withcolor b; fi; fi; enddef; % Draw a polygonal line through the list of points % This implementation does not work if you call % |draw_lines(i,i+4)| because \MP{} adds parentheses around % the value of |i|. def draw_lines(text vertices)= begingroup % so that we can |let| |draw_lines| save j,num,np; % first, we copy all the indexes in an array, so that % it is easier to go through them j=1; for $=vertices:num[j]=$;j:=j+1;endfor; np=j-1; for j:=1 upto np-1: draw z[ipnt_(num[j])]--z[ipnt_(num[j+1])]; endfor; endgroup enddef; let draw_line=draw_lines; % Draw an arrow between points |i| and |j| of current object % This is used from the |draw| definition of an object. def draw_arrow(expr i,j)= drawarrow z[ipnt_(i)]--z[ipnt_(j)]; enddef; % Draw a line between points |i| of object |obja| and |j| of |objb| % This is used when outside an object (i.e., we can't presuppose % any object offset) vardef draw_line_inter(expr obja, i, objb, j)= project_point(1,pnt_obj(obja,i)); project_point(2,pnt_obj(objb,j)); draw z1--z2; enddef; % Draw an arrow between points |i| of object |obja| and |j| of |objb| % This is used when outside an object (i.e., we can't presuppose % any object offset) vardef draw_arrow_inter(expr obja, i, objb, j)= project_point(1,pnt_obj(obja,i)); project_point(2,pnt_obj(objb,j)); draw z1--z2; enddef; %%\newpage % Definition of a macro |obj_name| returning an object name % when given an absolute % face number. This definition is built incrementally through a string, % everytime a new object is defined. % |obj_name| is defined by |redefine_obj_name_|. % Initial definition string index_to_name_; index_to_name_="def obj_name(expr i)=if i<1:"; % |name| is the name of an object instance % |n| is the absolute index of its last face def redefine_obj_name_(expr name,n)= index_to_name_:=index_to_name_ & "elseif i<=" & decimal n & ":" & ditto & name & ditto; scantokens begingroup index_to_name_ & "fi;enddef;" endgroup; enddef; % |i| is an absolute face number % |vertices| is a string representing a list of vertices % |rgbcolor| is a string representing a color in rgb hexadecimal def set_face(expr i,vertices,rgbcolor)= face_points_[i]:=vertices;face_color_[i]:=rgbcolor; enddef; % |i| is a local face number % |vertices| is a string representing a list of vertices % |rgbcolor| is a string representing a color in rgb hexadecimal def set_obj_face(expr i,vertices,rgbcolor)=set_face(face(i),vertices,rgbcolor) enddef; % |i| is a local face number of object |inst| % |rgbcolor| is a string representing a color in rgb hexadecimal def set_obj_face_color(expr inst,i,rgbcolor)= face_color_[face_obj(inst,i)]:=rgbcolor; enddef; %%\newpage %%\title{Compute the vectors corresponding to the observer's viewpoint} % (vectors |ObsI_|,|ObsJ_| and |ObsK_| in the |vec_I|,|vec_J|, % |vec_K| reference; and vectors |IObsI_|,|IObsJ_| and |IObsK_| % which are |vec_I|,|vec_J|,|vec_K| % in the |ObsI_|,|ObsJ_|,|ObsK_| reference) %%\figure{vect-fig.16} %% (here, $\psi>0$, $\theta<0$ and $\phi>0$; moreover, %% $\vert\theta\vert \leq 90^\circ$) def compute_reference(expr psi,theta,phi)= % |ObsI_| defines the direction of observation; % |ObsJ_| and |ObsK_| the orientation % (but one of these two vectors is enough, % since |ObsK_| = |ObsI_| $\land$ |ObsJ_|) % The vectors are found by rotations of |vec_I|,|vec_J|,|vec_K|. vec_def_vec_(ObsI_,vec_I);vec_def_vec_(ObsJ_,vec_J); vec_def_vec_(ObsK_,vec_K); vec_rotate_(ObsI_,ObsK_,psi); vec_rotate_(ObsJ_,ObsK_,psi);% gives ($u$,$v$,$z$) vec_rotate_(ObsI_,ObsJ_,theta); vec_rotate_(ObsK_,ObsJ_,theta);% gives ($Obs_x$,$v$,$w$) vec_rotate_(ObsJ_,ObsI_,phi); vec_rotate_(ObsK_,ObsI_,phi);% gives ($Obs_x$,$Obs_y$,$Obs_z$) % The passage matrix $P$ from |vec_I|,|vec_J|,|vec_K| % to |ObsI_|,|ObsJ_|,|ObsK_| is the matrix % composed of the vectors |ObsI_|,|ObsJ_| and |ObsK_| expressed % in the base |vec_I|,|vec_J|,|vec_K|. % We have $X=P X'$ where $X$ are the coordinates of a point % in |vec_I|,|vec_J|,|vec_K| % and $X'$ the coordinates of the same point in |ObsI_|,|ObsJ_|,|ObsK_|. % In order to get $P^{-1}$, it suffices to build vectors using % the previous rotations in the inverse order. vec_def_vec_(IObsI_,vec_I);vec_def_vec_(IObsJ_,vec_J); vec_def_vec_(IObsK_,vec_K); vec_rotate_(IObsK_,IObsI_,-phi);vec_rotate_(IObsJ_,IObsI_,-phi); vec_rotate_(IObsK_,IObsJ_,-theta);vec_rotate_(IObsI_,IObsJ_,-theta); vec_rotate_(IObsJ_,IObsK_,-psi);vec_rotate_(IObsI_,IObsK_,-psi); enddef; %%\newpage %%\title{Point of view} % This macro computes the three angles necessary for |compute_reference| % |name| = name of an instance of an object % |target| = target point (local to object |name|) % |phi| = angle vardef point_of_view_obj(expr name,target,phi)= define_current_point_offset_(name);% enables |pnt| point_of_view_abs(pnt(target),phi); enddef; % Compute absolute perspective. |target| is an absolute point number % |phi| = angle % This function also computes two vectors needed in case % of an oblique projection. vardef point_of_view_abs(expr target,phi)= save psi,theta; new_vec(v_a); vec_diff_(v_a,target,Obs); vec_mult_(v_a,v_a,1/vec_mod_(v_a)); psi=angle((vec[v_a]x,vec[v_a]y)); theta=-angle((vec[v_a]x++vec[v_a]y,vec[v_a]z)); compute_reference(psi,theta,phi); if projection_type=2: % oblique % we start by checking that at a minimum the three points defining % the projection plane have different indexes; it doesn't mean % the plane if well defined, but if two values are identical, % the plane can't be well defined. if ((projection_plane1<>projection_plane2) and (projection_plane1<>projection_plane3) and (projection_plane2<>projection_plane3)): new_line_(l)(Obs,Obs); vec_sum_(l2,ObsI_,Obs); if def_inter_p_l_pl_(ObliqueCenter_)(l)(projection_plane): project_vectors(ProjK_,ProjJ_)(ObsK_,ObsJ_)(l)(projection_plane); % define the projection direction set_line_(projection_direction)(Obs,ObliqueCenter_); else: message "Anomalous oblique projection:"; message " the observer is watching parallely to the plane"; fi; free_line(l); else: message "Anomalous projection plane; did you define it?"; fi; fi; free_vec(v_a); enddef; % Distance between the observer and point |n| of object |name| % Result is put in |dist| vardef obs_distance(text dist)(expr name,n)= new_vec(v_a); define_current_point_offset_(name);% enables |pnt| dist:=vec_mod_(v_a,pnt(n),Obs); free_vec(v_a); enddef; %%\newpage %%\title{Vector and point allocation} % Allocation is done through a stack of vectors numeric last_vec_; last_vec_=0; % vector allocation % (this must not be a |vardef| because the vector |v| saved is not saved % in this macro, but in the calling context) def new_vec(text v)= save v; new_vec_(v); enddef; def new_vec_(text v)= v:=incr(last_vec_); %|message "Vector " & decimal (last_vec_+1) & " allocated";| enddef; let new_point = new_vec; let new_point_ = new_vec_; def new_points(text p)(expr n)= save p; numeric p[]; for i:=1 upto n:new_point_(p[i]);endfor; enddef; % Free a vector % A vector can only be freed safely when it was the last vector created. def free_vec(expr i)= if i=last_vec_: last_vec_:=last_vec_-1; %|message "Vector " & decimal i & " freed";| else: errmessage("Vector " & decimal i & " can't be freed!"); fi; enddef; let free_point = free_vec; def free_points(text p)(expr n)= for i:=n step-1 until 1:free_point(p[i]);endfor; enddef; %%\title{Debugging} def show_vec(expr t,i)= message "Vector " & t & "=" & "(" & decimal vec[i]x & "," & decimal vec[i]y & "," & decimal vec[i]z & ")"; enddef; % One can write |show_point("2",pnt_obj("obj",2));| let show_point=show_vec; def show_pair(expr t,zz)= message t & "=(" & decimal xpart(zz) & "," & decimal ypart(zz) & ")"; enddef; %%\newpage %%\title{Access to object features} % |a| must be a string representing a class name, such as |"dodecahedron"|. % |b| is the tail of a macro name. def obj_(expr a,b,i)= scantokens begingroup save n;string n;n=a & b & i;n endgroup enddef; def obj_points_(expr name)= obj_(obj_class_(name),"_points",name) enddef; def obj_faces_(expr name)= obj_(obj_class_(name),"_faces",name) enddef; vardef obj_point_offset_(expr name)= obj_(obj_class_(name),"_point_offset",name) enddef; vardef obj_face_offset_(expr name)= obj_(obj_class_(name),"_face_offset",name) enddef; def obj_class_(expr name)=obj_(name,"_class","") enddef; %%\newpage def define_point_offset_(expr name,o)= begingroup save n,tmpdef; string n,tmpdef; n=obj_class_(name) & "_point_offset" & name; expandafter numeric scantokens n; scantokens n:=last_point_offset_; last_point_offset_:=last_point_offset_+o; tmpdef="def " & obj_class_(name) & "_points" & name & "=" & decimal o & " enddef"; scantokens tmpdef; endgroup enddef; def define_face_offset_(expr name,o)= begingroup save n,tmpdef; string n,tmpdef; n=obj_class_(name) & "_face_offset" & name; expandafter numeric scantokens n; scantokens n:=last_face_offset_; last_face_offset_:=last_face_offset_+o; tmpdef="def " & obj_class_(name) & "_faces" & name & "=" & decimal o & " enddef"; scantokens tmpdef; endgroup enddef; def define_current_point_offset_(expr name)= save current_point_offset_; numeric current_point_offset_; current_point_offset_:=obj_point_offset_(name); enddef; def define_current_face_offset_(expr name)= save current_face_offset_; numeric current_face_offset_; current_face_offset_:=obj_face_offset_(name); enddef; %%\newpage %%\title{Drawing an object} % |name| is an object instance vardef draw_obj(expr name)= save tmpdef; string tmpdef; current_obj:=name; tmpdef="draw_" & obj_class_(name); project_obj(name);% compute screen coordinates save overflow; boolean overflow; overflow=false; for $:=1 upto obj_points_(name): if z[ipnt_($)]=(too_big_,too_big_):overflow:=true; x[ipnt_($)] := 10; % so that the figure can be drawn anyway y[ipnt_($)] := 10; % why can't I write z[ipnt_($)]:=(10,10); ? fi; exitif overflow; endfor; if overflow: message "Figure has overflows"; message " (at least one point is not visible "; message " and had to be drawn at a wrong place)"; fi; scantokens tmpdef(name); enddef; %%\title{Normalization of an object} % This macro translates an object so that a list of vertices is centered % on the origin, and the last vertex is put on a sphere whose radius is 1. % |name| is the name of the object and |vertices| is a list % of points whose barycenter will define the center of the object. % (|vertices| need not be the list of all vertices) vardef normalize_obj(expr name)(text vertices)= save nvertices,last; nvertices=0; new_vec(v_a);vec_def_(v_a,0,0,0) forsuffixes $=vertices: vec_sum_(v_a,v_a,pnt($)); nvertices:=nvertices+1; last:=$; endfor; vec_mult_(v_a,v_a,-1/nvertices); translate_obj(name,v_a);% object centered on the origin scale_obj(name,1/vec_mod(last)); free_vec(v_a); enddef; %%\newpage %%\title{General definitions} % Vector arrays numeric vec[]x,vec[]y,vec[]z; % Reference vectors $\vec{0}$, $\vec{\imath}$, $\vec{\jmath}$ and $\vec{k}$ % and their definition new_vec(vec_null);new_vec(vec_I);new_vec(vec_J);new_vec(vec_K); vec_def_(vec_null,0,0,0); vec_def_(vec_I,1,0,0);vec_def_(vec_J,0,1,0);vec_def_(vec_K,0,0,1); numeric point_null; point_null=vec_null; % Observer new_point(Obs); % default value: set_point_(Obs,0,0,20); % Observer's vectors new_vec(ObsI_);new_vec(ObsJ_);new_vec(ObsK_); % default values: vec_def_vec_(ObsI_,vec_I); vec_def_vec_(ObsJ_,vec_J); vec_def_vec_(ObsK_,vec_K); new_vec(IObsI_);new_vec(IObsJ_);new_vec(IObsK_); % These vectors will be vectors of the projection plane, % in case of oblique projections: new_vec(ProjK_);new_vec(ProjJ_); % there is no |ProjI_| % This will be the center of the projection plane, in oblique projections new_point(ObliqueCenter_); % distance observer/plane (must be $>0$) numeric Obs_dist; % represents |Obs_dist| $\times$ |drawing_scale| % default value: Obs_dist=2; % means |Obs_dist| $\times$ |drawing_scale| % current object being drawn string current_obj; % kind of projection: 0 for linear (or central) perspective, 1 for parallel, % 2 for oblique projection % (default is 0) numeric projection_type; projection_type:=0; % Definition of a projection plane (only used in oblique projections) % new_plane_(projection_plane)(1,1,1); % the initial value is irrelevant % Definition of a projection direction (only used in oblique projections) new_line_(projection_direction)(1,1); % the initial value is irrelevant % this positions the observer at vector |p| (the point observed) % + |d| (distance) * (k-(i+j)) def isometric_projection(expr i,j,k,p,d,phi)= trimetric_projection(i,j,k,1,1,1,p,d,phi); enddef; % this positions the observer at vector |p| (the point observed) % + |d| (distance) * (ak-(i+j)) def dimetric_projection(expr i,j,k,a,p,d,phi)= trimetric_projection(i,j,k,1,1,a,p,d,phi); enddef; % this positions the observer at vector |p| (the point observed) % + |d| (distance) * (k-(i+j)) % |a|, |b| and |c| are multiplicative factors to vectors |i|, |j| and |k| vardef trimetric_projection(expr i,j,k,a,b,c,p,d,phi)= save v_a,v_b,v_c; new_vec(v_a);new_vec(v_b);new_vec(v_c); vec_mult_(v_a,i,a);vec_mult_(v_b,j,b);vec_mult_(v_c,k,c); vec_sum_(Obs,v_a,v_b); vec_diff_(Obs,v_c,Obs); vec_mult_(Obs,Obs,d); vec_sum_(Obs,Obs,p); point_of_view_abs(p,phi); projection_type:=1; free_vec(v_c);free_vec(v_b);free_vec(v_a); enddef; % |hor| is an horizontal plane (in the sense that it will represent % the horizontal for the observer) % |p| is the point in space that the observer targets (center of screen) % |a| is an angle (45 degrees corresponds to cavalier drawing) % |b| is an angle (see examples defined below) % |d| is the distance of the observer vardef oblique_projection(text hor)(expr p,a,b,d)= save _l,v_a,v_b,v_c,xxx_,obsJangle_; new_vec(v_a);new_vec(v_b);new_vec(v_c); % we first compute a horizontal line: new_line_(_l)(1,1); if def_inter_l_pl_pl(_l)(hor)(projection_plane): vec_diff_(v_a,_l2,_l1); % horizontal vector % then, we find a normal to the projection plane: def_normal_p_(v_b)(projection_plane); % complete the line and the vector by a third vector (=vertical) vec_prod_(v_c,v_a,v_b); % we make |v_a| a copy of |v_b| since we no longer need |v_b| vec_def_vec_(v_a,v_b); % we rotate |v_b| by an angle |a| around |v_c| vec_rotate_(v_b,v_c,a); % we rotate |v_b| by an angle |b| around |v_a| vec_rotate_(v_b,v_a,b); % we put the observer at the distance |d| of |p| in % the direction of |v_b|: vec_unit_(v_b,v_b); vec_mult_(v_b,v_b,d);vec_sum_(Obs,p,v_b); % We now have to make sure that point |p| and point |Obs| % are on different sides of the projection plane. For this, % we compute two dot products: new_vec(v_d);new_vec(v_e); vec_diff_(v_d,p,_l1);vec_diff_(v_e,Obs,_l1); if vec_dprod_(v_d,v_a)*vec_dprod_(v_e,v_a)>=0: % |p| and |Obs| are on the same side of the projection plane % |Obs| needs to be recomputed. vec_mult_(v_b,v_b,-1); vec_sum_(Obs,p,v_b); fi; free_vec(v_e);free_vec(v_d); projection_type:=2; % needs to be set before |point_of_view_abs| point_of_view_abs(p,90); % this computes |ObliqueCenter_| % and now, make sure the vectors defining the observer are right: % Create the plane containing lines _l and projection_direction % (defined by point_of_view_abs): new_plane_(xxx_)(1,1,1); def_plane_pl_l_l(xxx_)(_l)(projection_direction); % Compute the angle of |ObsK_| with this plane: obsJangle_=vangle_v_pl_(ObsK_)(xxx_); % rotate |ObsJ_| and |ObsK_| by |obsJangle_| around |ObsI_| vec_rotate_(ObsJ_,ObsI_,obsJangle_); vec_rotate_(ObsK_,ObsI_,obsJangle_); if abs(vangle_v_pl_(ObsK_)(xxx_))>1: % the rotation was done % in the wrong direction vec_rotate_(ObsJ_,ObsI_,-2obsJangle_); vec_rotate_(ObsK_,ObsI_,-2obsJangle_); fi; % |vec_rotate_(ObsJ_,ObsI_,45);| % planometric test % |vec_rotate_(ObsK_,ObsI_,45);| % planometric test free_plane(xxx_); % and now, |ProjJ_| and |ProjK_| must be recomputed: project_vectors(ProjK_,ProjJ_)(ObsK_,ObsJ_)% (projection_direction)(projection_plane); else: message "Error: the ``horizontal plane'' cannot be"; message " parallel to the projection plane."; fi; free_line(_l); free_vec(v_c);free_vec(v_b);free_vec(v_a); enddef; % These two are the most common values for the third parameter % of |oblique_projection| numeric CAVALIER;CAVALIER=45; numeric CABINET;CABINET=angle((1,.5)); % atn(.5) % Screen Size % The screen size is defined through two angles: the horizontal field % and the vertical field numeric h_field,v_field; h_field=100; % degrees v_field=70; % degrees % Observer's orientation, defined by three angles numeric Obs_psi,Obs_theta,Obs_phi; % default value: Obs_psi=0;Obs_theta=90;Obs_phi=0; % This array relates an absolute object point number to the % absolute point number (that is, to the |vec| array). % The absolute object point number is the rank of a point % with respect to all object points. The absolute point number % considers in addition the extra points, such as |Obs|, which do % not belong to an object. % If |i| is an absolute object point number, |points_[i]| % is the absolute point number. numeric points_[]; % |name| is the name of an object instance % |npoints| is its number of defining points def new_obj_points(expr name,npoints)= define_point_offset_(name,npoints);define_current_point_offset_(name); for i:=1 upto obj_points_(name):new_point_(pnt(i));endfor; enddef; % |name| is the name of an object instance % |nfaces| is its number of defining faces def new_obj_faces(expr name,nfaces)= define_face_offset_(name,nfaces);define_current_face_offset_(name); redefine_obj_name_(name,current_face_offset_+nfaces); enddef; %%\newpage % Absolute point number corresponding to object point number |i| % This macro must only be used within the function defining an object % (such as |def_cube|) or the function drawing an object (such as % |draw_cube|). def ipnt_(expr i)=i+current_point_offset_ enddef; def pnt(expr i)=points_[ipnt_(i)] enddef; def face(expr i)=(i+current_face_offset_) enddef; % Absolute point number corresponding to local point |n| % in object instance |name| vardef pnt_obj(expr name,n)= points_[n+obj_point_offset_(name)] %hide(define_current_point_offset_(name);) pnt(n) % HAS SIDE EFFECTS enddef; % Absolute face number corresponding to local face |n| % in object instance |name| vardef face_obj(expr name,n)= (n+obj_face_offset_(name)) %hide(define_current_face_offset_(name);) face(n) % HAS SIDE EFFECTS enddef; % Scale numeric drawing_scale; drawing_scale=2cm; % Color % This function is useful when a color is expressed in hexadecimal. % This does the opposite from |tohexcolor| def hexcolor(expr s)= (hex(substring (0,2) of s)/255,hex(substring (2,4) of s)/255, hex(substring (4,6) of s)/255) enddef; % Convert a color triple into a hexadecimal color string. % |rv|, |gv| and |bv| are values between 0 and 1. % This does the opposite from |hexcolor| vardef tohexcolor(expr rv,gv,bv)= save dig;numeric dig[]; hide( dig2=floor(rv*255);dig1=floor((dig2)/16);dig2:=dig2-16*dig1; dig4=floor(gv*255);dig3=floor((dig4)/16);dig4:=dig4-16*dig3; dig6=floor(bv*255);dig5=floor((dig6)/16);dig6:=dig6-16*dig5; for i:=1 upto 6: if dig[i]<10:dig[i]:=dig[i]+48; else:dig[i]:=dig[i]+87; fi; endfor; ) char(dig1)&char(dig2)&char(dig3)&char(dig4)&char(dig5)&char(dig6) enddef; % Conversions % Returns a string encoding the integer |n| as follows: % if $n=10*a+b$ with $b<10$, % |alphabetize|(|n|)=|alphabetize|(|a|) |&| |char (65+b)| % For instance, alphabetize(3835) returns "DIDF" % This function is useful in places where digits are not allowed. def alphabetize(expr n)= if (n>9): alphabetize(floor(n/10)) & fi char(65+n-10*floor(n/10)) enddef; % Filling and contours boolean filled_faces,draw_contours; filled_faces=true; draw_contours=true; numeric contour_width; % thickness of contours contour_width=1pt; color contour_color; % face contours contour_color=black; % Overflow control % An overflow can occur when an object is too close from the observer % or if an object is out of sight. We use a special value to mark % coordinates which would lead to an overflow. numeric too_big_; too_big_=4000; % Object offset (the points defining an object are arranged % in a single array, and the objects are easier to manipulate % if the point numbers are divided into a number and an offset). numeric last_point_offset_,last_face_offset_; last_point_offset_=0;last_face_offset_=0; endinput