/* JTwoD.java A set of classes to parse, represent and display tensegrity models -- see comments at Model3D.load(InputStream) for description of data file format History: 12 Jun 2008 Do not handle exceptions in load(), pass them on. Remove ThreeDClient: messages probably are not useful anymore and should be done with subscribe pattern anyway. Do not close the InputStream in load(). Merge all reset functions into resetTransform(). Add reset() which puts things back in initial state and redraws. Do not report model errors by drawing into display area. In paintBuffer() clear the buffer before testing for empty model and possibly exiting. 10 Jun 2008 Extract Model3D class and rename it Java2DModel. 09 Jun 2008 Delete "token" variable for use with StreamTokenizer. Not needed since value is stored in a publicly accessible field. 29 Dec 2006 In areInverted(), replace the giant if statement with an initial rejection test. Minor comment changes. 09 Aug 2006 Move m_colors and m_strokes array sizing to static block with the rest of their initialization. Rename JTwoD.m_lines as JTwoD.m_lineIndexes. Make Model3D.LineSegment static since it doesn't need non-static capability. 01 Aug 2006 Use JComponent instead of Canvas as display surface. Otherwise, JComboBox drops down behind the drawing, and performance seems enhanced as well. 24 Jul 2006 Allow color to be turned on and off (black and white rendering). 17 Jul 2006 In case 1 of areInverted(), return false instead of falling through if criterion isn't met. Comment better. Round the ends of the struts and change setClip() and areInverted() computations accordingly. Spell lambda right. Change the CLIP_AUGMENTATION_FACTOR from a multiplicative value to an additive one and give it a name. 14 Jul 2006 Change name of m_zmids to more accurate m_zsums. Add code to fix depth-sorting inversions. Allow anti-aliasing and two-toned rendering to be turned on and off. 13 Jul 2006 Created from ThreeD.java. Problems: The depth sorting still gets messed up when two inversions for the same member are near the same location and are inverted with respect to each other. I've seen this happen in the Skew Eight-Prism, but it's so rare and seems like such a nuisance to fix I think I'll let it slide. In general, the Skew Eight-Prism and 9-Fold Equi-tendoned Prism are effective torture tests for the depth sorting. And then there are those annoying visual artifacts that result from the mask not being quite large enough somehow. It usually looks like a dotted line. It's interesting to look at a problem at various magnifications and see how it changes. Perhaps I can experiment with a larger augmentation factor for the mask. It may be the ill-effects of too large a mask are very minor compared to these artifacts. I'd like to have a pull-down panel (like a ComboBox) with all the checkboxes but haven't figured out how to do it. */ import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; /** Manage the way an instance of Java2DModel is drawn on Canvas. Much of the rendering technique and geometry management came originally from the JDK 1.1.1 wireframe example. @author Bob Burkhardt @see Java2DModel */ public class JTwoD extends javax.swing.JComponent { /** The wireframe model. */ Java2DModel m_md; /** Array of indices into the m_md's line array so lines can be accessed by z-value of midpoints. */ private int m_lineIndexes[]; /** Where all graphics are rendered. This needs to be maintained at the same size as the Canvas and is drawn onto the Canvas any time the image needs updating. */ BufferedImage m_graphicsBuffer; /** Graphics context used for rendering the graphics. @see #m_graphicsBuffer */ Graphics2D m_gc; /** Width of the graphics buffer (needs to match that of Canvas). */ int m_width; /** Height of the graphics buffer (needs to match that of Canvas). */ int m_height; /** Scale value which multiplies model coordinates to get the pixel coordinates and z values used to render the wireframe on Canvas. */ float m_scale; /** Margin adjustment for the scale value so there's a little space around the model at magnification 1.0. */ final static float MARGIN_SCALE_ADJUST = 0.96f; /** Transformation matrix applied to the model coordinates to get device coordinates. */ matrix3d m_mat = new matrix3d(); /** Transformed coordinates for all model points (pixel locations + z value) @see matrix3d */ tuple3d m_tverts[]; /** Sum of two z-values for each transformed line. */ float m_zsums[]; /** Max z-value for each transformed line. */ float m_zmaxs[]; /** Additional magnification value applied on top of m_scale. Ranges from 1.0 to MAX_VIEW_MAGNIFICATION. @see #m_scale */ float m_mag; /** Adjustment so dragging moves the front of the figure by more than the length of the drag by the given adjustment factor. */ final static float ROTATION_MAGNIFICATION_FACTOR = 3.0f; /** Rotation matrix which determines the orientation of the model display. Used by updateBuffer as one component for m_mat, the model's transformation matrix. @see #updateBuffer @see #m_mat */ matrix3d m_rot = new matrix3d(); /** Matrix to store shift factor which is an offset applied to the location of the model after any rotation. @see #updateBuffer @see #m_mat */ matrix3d m_shift = new matrix3d(); /** X coordinate of current shift vector. Ranges from -m_md.m_radius to m_md.m_radius. Applied after rotation and before magnification. */ float m_shiftX; /** Y coordinate of current shift vector. Ranges from -m_md.m_radius to m_md.m_radius. Applied after rotation and before magnification. */ float m_shiftY; /** Coordinates of last observed mouse position (only updated during drags). */ int m_x = 0, m_y = 0; /** Dimension of largest square fitting on the Canvas. */ int m_dim = 0; /** Should lines be rendered in two tones depending on relation to the z == 0 plane? */ boolean m_doTwoTone = true; /** Should lines be rendered in color? */ boolean m_doColor = true; /** Should anti-aliasing be done? */ boolean m_doAntiAliasing = true; /** Offset to color index to get faded version. @see #m_colors */ private static final int M_FADE_OFFSET = 10; /** Index of background color. @see #m_colors */ private static final int M_BACKGROUND_COLOR_INDEX = 2*M_FADE_OFFSET; /** Colors used by paintBuffer to render the lines. Lines in the back half of the figure are rendered with faded versions of the colors used to render the lines in the front half of the figure. @see #paintBuffer */ private static final Color m_colors[]; /** BasicStrokes used by paint to render the lines. @see #paintBuffer */ private static final BasicStroke m_strokes[]; /** Width of stroke used for rendering "invisible" tendons and tendon interior. @see #m_strokes @see #paintBuffer */ private static final float M_BASE_WIDTH = 1.0f; /** Index of stroke used for rendering tendons. @see #m_strokes @see #paintBuffer */ private static final int M_TENDON_EXTERIOR_STROKE = 0; /** Scale of stroke width used for rendering tendon exteriors relative to the base width. @see #m_strokes @see #paintBuffer */ private static final float M_TENDON_EXTERIOR_SCALE = 3.0f; /** Scale of stroke width used for rendering strut exteriors relative to the base width. @see #m_strokes @see #paintBuffer */ private static final float M_STRUT_EXTERIOR_SCALE = 9.0f; /** Index of stroke used for rendering strut exteriors. @see #m_strokes @see #paintBuffer */ private static final int M_STRUT_EXTERIOR_STROKE = 1; /** Scale of stroke width used for rendering strut interiors relative to the base width. @see #m_strokes @see #paintBuffer */ private static final float M_STRUT_INTERIOR_SCALE = 6.0f; /** Index of stroke used for rendering strut exteriors. @see #m_strokes @see #paintBuffer */ private static final int M_STRUT_INTERIOR_STROKE = 2; /** Index of stroke used for rendering tendon interiors or "invisible" tendons. @see #m_strokes @see #paintBuffer */ private static final int M_BASE_STROKE = 3; /* Amount to augment the nominal member width when computing a clip area. Making clip area slightly larger than the line reduces some annoying visual artifacts (faint outline of the member that was written over to correct the inversion). */ private static final float CLIP_AUGMENTATION_FACTOR = 1.0f; /** A reusable line. @see #paintBuffer */ Line2D m_line = new Line2D.Float(); /** A reusable path. @see #setClip */ GeneralPath m_path = new GeneralPath(); public JTwoD() { m_md = new Java2DModel(); resetTransform(); } static { m_colors = new Color[2*M_FADE_OFFSET + 1]; m_colors[0] = Color.black; // "invisible" tendons; strut exteriors; m_colors[1] = Color.green; // strut interior m_colors[2] = Color.red; m_colors[3] = Color.blue; m_colors[4] = Color.red; m_colors[5] = Color.blue; m_colors[6] = Color.cyan; m_colors[7] = Color.white; m_colors[8] = Color.orange; // strut interior m_colors[9] = Color.yellow; // strut interior // faded versions of the above m_colors[10] = new Color(128, 128, 128); m_colors[11] = new Color(128, 255, 128); m_colors[12] = new Color(255, 128, 128); m_colors[13] = new Color(128, 128, 255); m_colors[14] = new Color(255, 128, 128); m_colors[15] = new Color(128, 128, 255); m_colors[16] = new Color(128, 255, 255); m_colors[17] = Color.white; m_colors[18] = new Color(255, 192, 128); m_colors[19] = new Color(255, 255, 128); m_colors[20] = Color.lightGray; m_strokes = new BasicStroke[4]; m_strokes[M_TENDON_EXTERIOR_STROKE] = new BasicStroke(M_TENDON_EXTERIOR_SCALE*M_BASE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND); m_strokes[M_STRUT_EXTERIOR_STROKE] = new BasicStroke(M_STRUT_EXTERIOR_SCALE*M_BASE_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); m_strokes[M_STRUT_INTERIOR_STROKE] = new BasicStroke(M_STRUT_INTERIOR_SCALE*M_BASE_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); m_strokes[M_BASE_STROKE] = new BasicStroke(M_BASE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND); } /** Initialize the model geometry from data contained in a stream. Set scaling. @exception IOException Input problem with the stream of model data. @exception Model3D.FileFormatException Format problem with the stream. @exception Model3D.ModelException Problem storing model in Model3D. */ public void load(InputStream is) throws IOException, Model3D.FileFormatException, Model3D.ModelException { resetTransform(); m_md.load(is); float f1 = getWidth()/(2.0f*m_md.m_radius); float f2 = getHeight()/(2.0f*m_md.m_radius); m_scale = MARGIN_SCALE_ADJUST*((f1 < f2) ? f1 : f2); updateBuffer(); repaint(0); } /** Set initial mouse coordinates for start of mouse-drag rotation or shift. @param x x coordinate of mouse @param y y coordinate of mouse */ public void startDrag(int x, int y) { m_x = x; m_y = y; } /** Reset transform to initial state. */ private void resetTransform() { // rotation m_rot.unit(); m_rot.zrot(180.0); // so y-axis points up instead of down // shift setShift(0.0f, 0.0f); // magnification m_mag = 1.0f; } /** Reset transform and model to initial state. */ public void reset() { resetTransform(); m_md.reset(); updateBuffer(); repaint(0); } /** Increment the rotation depending on new mouse coordinates. @param x x coordinate of mouse @param y y coordinate of mouse */ public void updateRotation(int x, int y) { if (x == m_x && y == m_y) return; float deltax = 2.0f*(x - m_x)/m_dim; float deltay = 2.0f*(y - m_y)/m_dim; m_x = x; m_y = y; // angle of rotation (radians) double angle = (float)Math.sqrt(deltax*deltax + deltay*deltay); // axis of rotation (normalized to length == 1; z == 0) float axis_x = (float)(-deltay/angle); float axis_y = (float)(deltax/angle); angle *= (180.0/Math.PI)*ROTATION_MAGNIFICATION_FACTOR/m_mag; // update rotation matrix m_rot.arot(angle, axis_x, axis_y, 0.0f); updateBuffer(); repaint(0); } /** Set the magnification value. @param mag magnification value (1.0 is no magnification; negative values will result in reflection) */ public void setMagnification(float value) { m_mag = value; updateBuffer(); repaint(0); } /** Set the shift vector. Applied after rotation, but before magnification. @param x x component of shift vector @param y y component of shift vector (each clipped to between -m_md.m_radius and m_md.m_radius) */ public void setShift(float x, float y) { if (x < -m_md.m_radius) x = -m_md.m_radius; else if (x > m_md.m_radius) x = m_md.m_radius; if (y < -m_md.m_radius) y = -m_md.m_radius; else if (y > m_md.m_radius) y = m_md.m_radius; m_shiftX = x; m_shiftY = y; m_shift.unit(); m_shift.translate(x, y, 0.0f); } /** Update the shift depending on new mouse coordinates and the current magnification. @param x x coordinate of mouse @param y y coordinate of mouse */ public void updateShift(int x, int y) { if (x == m_x && y == m_y) return; float shiftx = (x - m_x)/(m_scale*m_mag) + m_shiftX; float shifty = (y - m_y)/(m_scale*m_mag) + m_shiftY; m_x = x; m_y = y; setShift(shiftx, shifty); updateBuffer(); repaint(0); } /** Redraw m_graphicsBuffer according to current model geometry and transformations. This function basically sets things up. Most of the actual rendering is done by paintBuffer(). @see #m_graphicsBuffer @see #paintBuffer */ public void updateBuffer() { if (m_md != null) { m_mat.unit(); // center about the origin m_mat.translate(-m_md.m_xmean, -m_md.m_ymean, -m_md.m_zmean); // rotate about the origin m_mat.mult(m_rot); // apply shift m_mat.mult(m_shift); // do reality check on graphics buffer dimensions if (m_graphicsBuffer == null || m_width != getWidth() || m_height != getHeight()) { m_width = getWidth(); m_height = getHeight(); m_dim = m_width < m_height ? m_width : m_height; m_graphicsBuffer = (BufferedImage)createImage(m_width, m_height); if (m_graphicsBuffer != null) m_gc = m_graphicsBuffer.createGraphics(); m_scale = MARGIN_SCALE_ADJUST*m_dim/(2.0f*m_md.m_radius); } if (m_graphicsBuffer != null && m_gc == null) m_gc = m_graphicsBuffer.createGraphics(); // scale graphics according to magnification and viewport m_mat.scale(m_scale*m_mag, m_scale*m_mag, m_scale*m_mag); // move graphics to center of viewport m_mat.translate(m_width/2, m_height/2, 0); // clear buffer and draw graphics in it if (m_graphicsBuffer != null && m_gc != null) paintBuffer(); } } /** Turn antialiasing on and off. */ public void doAntiAliasing(boolean value) { m_doAntiAliasing = value; updateBuffer(); repaint(0); } /** Render in two tones. */ public void doTwoTone(boolean value) { m_doTwoTone = value; updateBuffer(); repaint(0); } /** Render in color. */ public void doColor(boolean value) { m_doColor = value; updateBuffer(); repaint(0); } /** Heap sort m_zsums array into increasing order. Adapted from S. Dvorak and A. Musset, BASIC in Action, p. 170. Transforms to m_zsums array are mirrored in m_lineIndexes array. */ private synchronized void sort() { int i; int shift_index, lineIndex_value; float zsum_value; // Organize array into descending heap (largest value at root) for (i = (m_md.nlines() - 1)/2; i >= 0; i--) shift_up(m_zsums[i], m_lineIndexes[i], i, m_md.nlines()); /* Create array sorted in increasing order (starting from end). As for loop progresses, array gets filled from the end, and the remainder of the array is maintained as a descending heap which steadily becomes smaller. v[0] always contains the largest value of the remaining heap. */ for (i = m_md.nlines() - 1; i > 0; i--) { /* grab value at and index of current array position being filled (it will be inserted into remaining heap) */ zsum_value = m_zsums[i]; lineIndex_value = m_lineIndexes[i]; // put largest value of remaining heap in final position m_zsums[i] = m_zsums[0]; m_lineIndexes[i] = m_lineIndexes[0]; // shift value up into remaining descending heap shift_up(zsum_value, lineIndex_value, 0, i); } } /** Shift zsum_value up into heap until it's not less than successors. Transforms to m_zsums array are mirrored in m_lineIndexes array. @see #sort */ private void shift_up (float zsum_value, int lineIndex_value, int start_index, int max_index) { int j; // initially index of "left" successor of v[start_index] while ((j = start_index + start_index + 1) < max_index) { // grab "right" successor of m_zsums[start_index] if it has larger value if (j + 1 < max_index && m_zsums[j] < m_zsums[j + 1]) j++; if (zsum_value < m_zsums[j]) // shift value less than a successor { // put this successor in shift_value's old position m_zsums[start_index] = m_zsums[j]; m_lineIndexes[start_index] = m_lineIndexes[j]; // consider shift_value to now occupy successor's position start_index = j; } else break; /* shift_value not less than successors */ } // put value in place m_zsums[start_index] = zsum_value; m_lineIndexes[start_index] = lineIndex_value; } /** Transform all the points in this model using m_mat. @see #m_mat @see matrix3d */ private void transform() { if (m_md.nverts() <= 0) return; if (m_tverts == null) { m_tverts = new tuple3d[m_md.nverts()]; for (int i = 0; i < m_md.nverts(); i++) m_tverts[i] = new tuple3d(0f, 0f, 0f); } else if (m_tverts.length < m_md.nverts()) { tuple3d nv[] = new tuple3d[m_md.nverts()]; System.arraycopy(m_tverts, 0, nv, 0, m_tverts.length); m_tverts = nv; for (int i = 0; i < m_md.nverts(); i++) if (m_tverts[i] == null) m_tverts[i] = new tuple3d(0f, 0f, 0f); } m_mat.transform(m_md.verts(), m_tverts, m_md.nverts()); } /** Draw a bicolor line (one that straddles the z == 0 plane). */ void drawSplitLine(int darkColor, float x1, float y1, float z1, float x2, float y2, float z2) { if (z2 < z1) { // fix it so first point is behind the z == 0 plane float itemp; itemp = x1; x1 = x2; x2 = itemp; itemp = y1; y1 = y2; y2 = itemp; itemp = z1; z1 = z2; z2 = itemp; } float zdiff = z2 - z1; float xsplit = z2*(x1 - x2)/zdiff; float ysplit = z2*(y1 - y2)/zdiff; m_gc.setPaint(m_colors[darkColor + M_FADE_OFFSET]); m_line.setLine(x2 + xsplit, y2 + ysplit, x1, y1); m_gc.draw(m_line); m_gc.setPaint(m_colors[darkColor]); m_line.setLine(x2, y2, x2 + xsplit, y2 + ysplit); m_gc.draw(m_line); } /** Find whether a line is a representation of a strut given its type. */ boolean isStrut(int type) { return type%8 == 1; } /** Find width of a line given its type. */ float getWidth(int type) { if (isStrut(type)) { // a strut return M_STRUT_EXTERIOR_SCALE*M_BASE_WIDTH; } else if (type%8 == 0) { // an "invisible" tendon return M_BASE_WIDTH; } else { // a regular tendon return M_TENDON_EXTERIOR_SCALE*M_BASE_WIDTH; } } /** Set clip to outline of line. */ void setClip(int type, float x1, float y1, float x2, float y2) { float w = getWidth(type) + CLIP_AUGMENTATION_FACTOR; float hw = w/2f; // half width float c = hw*1.5f; float dx = x2 - x1; float dy = y2 - y1; float len = (float)Math.sqrt(dx*dx + dy*dy); float ndx = dx/len; float ndy = dy/len; m_path.reset(); m_path.moveTo(x1 + hw*ndy, y1 - hw*ndx); if (isStrut(type)) m_path.curveTo(// first Bezier control pt x1 + hw*ndy - c*ndx, y1 - hw*ndx - c*ndy, // second Bezier control pt x1 - hw*ndy - c*ndx, y1 + hw*ndx - c*ndy, // destination x1 - hw*ndy, y1 + hw*ndx); else m_path.lineTo(x1 - hw*ndy, y1 + hw*ndx); m_path.lineTo(x1 + dx - hw*ndy, y1 + dy + hw*ndx); if (isStrut(type)) m_path.curveTo(// first Bezier control pt x1 + dx - hw*ndy + c*ndx, y1 + dy + hw*ndx + c*ndy, // second Bezier control pt x1 + dx + hw*ndy + c*ndx, y1 + dy - hw*ndx + c*ndy, // destination x1 + dx + hw*ndy, y1 + dy - hw*ndx); else m_path.lineTo(x1 + dx + hw*ndy, y1 + dy - hw*ndx); m_path.closePath(); m_gc.setClip(m_path); } /** Draw a line given its type and coordinates. */ void drawLine(int type, float x1, float y1, float z1, float x2, float y2, float z2) { m_line.setLine(x1, y1, x2, y2); if (isStrut(type)) { // a strut if (!m_doTwoTone || (z1 >= 0 && z2 >= 0)) { // dark line m_gc.setStroke(m_strokes[M_STRUT_EXTERIOR_STROKE]); m_gc.setPaint(m_colors[0]); m_gc.draw(m_line); m_gc.setStroke(m_strokes[M_STRUT_INTERIOR_STROKE]); m_gc.setPaint(m_doColor ? m_colors[getStrutInteriorColor(type)] : Color.white); m_gc.draw(m_line); } else if (z1 < 0 && z2 < 0) { // light line m_gc.setStroke(m_strokes[M_STRUT_EXTERIOR_STROKE]); m_gc.setPaint(m_colors[0 + M_FADE_OFFSET]); m_gc.draw(m_line); m_gc.setStroke(m_strokes[M_STRUT_INTERIOR_STROKE]); m_gc.setPaint(m_doColor ? m_colors[getStrutInteriorColor(type) + M_FADE_OFFSET] : Color.white); m_gc.draw(m_line); } else { // line straddles z == 0 plane; draw it in two colors m_gc.setStroke(m_strokes[M_STRUT_EXTERIOR_STROKE]); drawSplitLine(0, x1, y1, z1, x2, y2, z2); m_gc.setStroke(m_strokes[M_STRUT_INTERIOR_STROKE]); if (m_doColor) drawSplitLine(getStrutInteriorColor(type), x1, y1, z1, x2, y2, z2); else { m_gc.setPaint(Color.white); m_line.setLine(x1, y1, x2, y2); // reset due to drawSplitLine call m_gc.draw(m_line); } } } else if (type%8 == 0) { // an "invisible" tendon m_gc.setStroke(m_strokes[M_BASE_STROKE]); if (!m_doTwoTone || (z1 >= 0 && z2 >= 0)) { // dark line m_gc.setPaint(m_colors[0]); m_gc.draw(m_line); } else if (z1 < 0 && z2 < 0) { // light line m_gc.setPaint(m_colors[0 + M_FADE_OFFSET]); m_gc.draw(m_line); } else { // line straddles z == 0 plane; draw it in two colors drawSplitLine(0, x1, y1, z1, x2, y2, z2); } } else { // a regular tendon if (!m_doTwoTone || (z1 >= 0 && z2 >= 0)) { // dark line m_gc.setStroke(m_strokes[M_TENDON_EXTERIOR_STROKE]); m_gc.setPaint(m_doColor ? m_colors[type%8] : Color.white); m_gc.draw(m_line); m_gc.setStroke(m_strokes[M_BASE_STROKE]); m_gc.setPaint(m_colors[0]); m_gc.draw(m_line); } else if (z1 < 0 && z2 < 0) { // light line m_gc.setStroke(m_strokes[M_TENDON_EXTERIOR_STROKE]); m_gc.setPaint(m_doColor ? m_colors[type%8 + M_FADE_OFFSET] : Color.white); m_gc.draw(m_line); m_gc.setStroke(m_strokes[M_BASE_STROKE]); m_gc.setPaint(m_colors[0 + M_FADE_OFFSET]); m_gc.draw(m_line); } else { // line straddles z == 0 plane; draw it in two colors m_gc.setStroke(m_strokes[M_TENDON_EXTERIOR_STROKE]); if (m_doColor) { drawSplitLine(type%8, x1, y1, z1, x2, y2, z2); } else { m_gc.setPaint(Color.white); m_gc.draw(m_line); } m_gc.setStroke(m_strokes[M_BASE_STROKE]); drawSplitLine(0, x1, y1, z1, x2, y2, z2); } } } /** Are two lines inverted (a line drawn earlier crosses over a line drawn later)? The input should be the post-sort index of the two lines which is appropriate for m_lineIndexes, m_zmaxs and m_zsums, but shouldn't be used directly as an argument to Java2DModel routines -- the index must be retrieved from m_lineIndexes first. */ boolean areInverted(int last, int earlier) { // inversion only possible if the maximum z for line drawn earlier // is greater than the minimum z for the line drawn last if (m_zmaxs[earlier] <= m_zsums[last] - m_zmaxs[last]) return false; int pl1 = m_md.p1(m_lineIndexes[last]); int pl2 = m_md.p2(m_lineIndexes[last]); int pe1 = m_md.p1(m_lineIndexes[earlier]); int pe2 = m_md.p2(m_lineIndexes[earlier]); boolean coincident = false; int temp; if (pl1 == pe1) coincident = true; else if (pl1 == pe2) { coincident = true; temp = pe1; pe1 = pe2; pe2 = temp; } else if (pl2 == pe1) { coincident = true; temp = pl1; pl1 = pl2; pl2 = temp; } else if (pl2 == pe2) { coincident = true; temp = pl1; pl1 = pl2; pl2 = temp; temp = pe1; pe1 = pe2; pe2 = temp; } // pl1 coordinates (last line segment) float xl1 = m_tverts[pl1].x(); float yl1 = m_tverts[pl1].y(); float zl1 = m_tverts[pl1].z(); // pl2 coordinates (last line segment) float xl2 = m_tverts[pl2].x(); float yl2 = m_tverts[pl2].y(); float zl2 = m_tverts[pl2].z(); // pe1 coordinates (earlier line segment) float xe1 = m_tverts[pe1].x(); float ye1 = m_tverts[pe1].y(); float ze1 = m_tverts[pe1].z(); // pe2 coordinates (earlier line segment) float xe2 = m_tverts[pe2].x(); float ye2 = m_tverts[pe2].y(); float ze2 = m_tverts[pe2].z(); float dxl = xl2 - xl1; float dyl = yl2 - yl1; float dzl = zl2 - zl1; float dxe = xe2 - xe1; float dye = ye2 - ye1; float dze = ze2 - ze1; // case 1: two segments joined at the ends if (coincident) { float lenl = (float)Math.sqrt(dxl*dxl + dyl*dyl + dzl*dzl); float lene = (float)Math.sqrt(dxe*dxe + dye*dye + dze*dze); // as long as the XY 2D angle between the two lines is less than 90, // this might make a difference -- more difference the smaller // the angle is and the more the apparent overlap between the two // lines; computing the angle to see if it's less than 90 // seems like it will take more time than it's worth though, so don't return dze/lene > dzl/lenl; } /* Neglect the case where one or both segments have projected length close to zero. If this is the case, both would have to be struts for it to be worth examining (if either one of these is not a strut there is either nothing to draw or no clip window to draw into). If clearances are good, there should be no problem. */ float lenl = (float)Math.sqrt(dxl*dxl + dyl*dyl); float lene = (float)Math.sqrt(dxe*dxe + dye*dye); float tol = m_dim*1e-6f/m_mag; if (lenl < tol || lene < tol) return false; /* case 2: see if the two segments intersect each other in xy-space: a. represent the two line segments as: pl = pl1 + lambdal*(pl2 - pl1) pe = pe1 + lambdae*(pe2 - pe1) where lambdal and lambdae are scalars, and pl1, pl2, pe1 and pe2 are 2D xy-space vectors. b. solve the two-equation system pl1 + lambdal*(pl2 - pl1) = pe1 + lambdae*(pe2 - pe1) or (pl2 - pl1)*lambdal - (pe2 - pe1)*lambdae = pe1 - pl1 for lambdal and lambdae by rewriting the system as Mx = b where x is an xy vector with components lambdal and lambdae, M is a 2x2 matrix and b is a constant xy vector. */ float m11 = dxl; float m12 = -dxe; float m21 = dyl; float m22 = -dye; float b1 = xe1 - xl1; float b2 = ye1 - yl1; // adjustments to allow for overshoot for struts float lambda_adjl = 0f; int type = m_md.type(m_lineIndexes[last]); float rl = getWidth(type)/2f; if (isStrut(type)) lambda_adjl = rl/lenl; float lambda_adje = 0f; type = m_md.type(m_lineIndexes[earlier]); float re = getWidth(type)/2f; if (isStrut(type)) lambda_adje = re/lene; float det = m11*m22 - m21*m12; /* The case of det close to zero (the two line segments are close to parallel in xy space) probably deserves consideration as a separate case. Even with models where members don't intersect each other internally and have adequate clearance, a short line in front of a long line could be eclipsed by it. The area of the quadrilateral defined by the line end points might provide light on this situation. For now, this situation is neglected. This situation results in a false since the lambda solutions will be out of bounds. */ float lambdal = (1f/det)*(m22*b1 - m12*b2); float lambdae; if (lambdal > -lambda_adjl && lambdal < 1f + lambda_adjl) { lambdae = (1f/det)*(m11*b2 - m21*b1); if (lambdae > -lambda_adje && lambdae < 1f + lambda_adje) // compute z values corresponding to intersection point if (zl1 + lambdal*dzl < ze1 + lambdae*dze) { // an inversion since the earlier z is greater than the last z /* float x = xl1 + lambdal*m11; float y = yl1 + lambdal*m21; m_gc.setColor(Color.black); m_gc.setStroke(m_strokes[M_BASE_STROKE]); m_gc.drawRect((int)(x - 10f), (int)(y - 10f), 20, 20); */ return true; } } /* case 3: end of one segment approaches the other segment closely -- this won't be caught be case 2 if the angle between the two segments is small */ float ndxl = dxl/lenl; float ndyl = dyl/lenl; float ref = xl1*ndyl - yl1*ndxl; if (Math.abs(xe1*ndyl - ye1*ndxl - ref) < rl + re) { lambdal = ((xe1 - xl1)*dxl + (ye1 - yl1)*dyl)/(lenl*lenl); if (lambdal > -lambda_adjl && lambdal < 1f + lambda_adjl && zl1 + lambdal*dzl < ze1) return true; } if (Math.abs(xe2*ndyl - ye2*ndxl - ref) < rl + re) { lambdal = ((xe2 - xl1)*dxl + (ye2 - yl1)*dyl)/(lenl*lenl); if (lambdal > -lambda_adjl && lambdal < 1f + lambda_adjl && zl1 + lambdal*dzl < ze2) return true; } float ndxe = dxe/lene; float ndye = dye/lene; ref = xe1*ndye - ye1*ndxe; if (Math.abs(xl1*ndye - yl1*ndxe - ref) < rl + re) { lambdae = ((xl1 - xe1)*dxe + (yl1 - ye1)*dye)/(lene*lene); if (lambdae > -lambda_adje && lambdae < 1f + lambda_adje && ze1 + lambdae*dze > zl1) return true; } if (Math.abs(xl2*ndye - yl2*ndxe - ref) < rl + re) { lambdae = ((xl2 - xe1)*dxe + (yl2 - ye1)*dye)/(lene*lene); if (lambdae > -lambda_adje && lambdae < 1f + lambda_adje && ze1 + lambdae*dze > zl2) return true; } return false; } /** Map strut type to appropriate interior color. */ int getStrutInteriorColor(int type) { int color = (type - 1)/8 + 1; if (color > 3) color = 3; if (color > 1) color += 6; return color; } /** Render m_md in m_graphicsBuffer. m_mat is used to map from model space to screen space. The colors for lines with negative transformed midpoint z-values are lighter than those with positive z-values. */ synchronized void paintBuffer() { m_gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, m_doAntiAliasing ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); m_gc.setColor(m_doColor ? m_colors[M_BACKGROUND_COLOR_INDEX] : Color.white); m_gc.fillRect(0, 0, m_width, m_height); if (m_md.nlines() <= 0 || m_md.nverts() <= 0) return; Line2D.Float line = new Line2D.Float(); transform(); if (m_lineIndexes == null || m_lineIndexes.length < m_md.nlines()) { m_lineIndexes = new int[m_md.nlines()]; m_zsums = new float[m_md.nlines()]; m_zmaxs = new float[m_md.nlines()]; } for (int i = 0; i < m_md.nlines(); i++) { m_zsums[i] = m_tverts[m_md.p1(i)].z() + m_tverts[m_md.p2(i)].z(); m_lineIndexes[i] = i; } sort(); for (int i = 0; i < m_md.nlines(); i++) { // draw the line int p1 = m_md.p1(m_lineIndexes[i]); int p2 = m_md.p2(m_lineIndexes[i]); int type = m_md.type(m_lineIndexes[i]); float z1 = m_tverts[p1].z(); float z2 = m_tverts[p2].z(); drawLine(type, m_tverts[p1].x(), m_tverts[p1].y(), z1, m_tverts[p2].x(), m_tverts[p2].y(), z2); m_zmaxs[i] = (z1 > z2) ? z1 : z2; boolean clipSet = false; // repair any inversions for (int j = 0; j < i; j++) { if (areInverted(i, j)) { if (!clipSet) { setClip(type, m_tverts[p1].x(), m_tverts[p1].y(), m_tverts[p2].x(), m_tverts[p2].y()); clipSet = true; } int q1 = m_md.p1(m_lineIndexes[j]); int q2 = m_md.p2(m_lineIndexes[j]); drawLine(m_md.type(m_lineIndexes[j]), m_tverts[q1].x(), m_tverts[q1].y(), m_tverts[q1].z(), m_tverts[q2].x(), m_tverts[q2].y(), m_tverts[q2].z()); } } if (clipSet) m_gc.setClip(null); } } public void update(Graphics g) { if (m_md != null) { if (m_graphicsBuffer == null || m_width != getWidth() || m_height != getHeight()) updateBuffer(); g.drawImage(m_graphicsBuffer, 0, 0, this); } } public void paint(Graphics g) { update(g); } }