#---------------------------------------------------------------------- # Program to implement 3D Orthographic Projection # # Copyright (C) April 18, 2018 -- Dr. William T. Verts #---------------------------------------------------------------------- #---------------------------------------------------------------------- # Return closest integer to N. This is particularly useful in # graphics, where float values have to be turned into pixel coordinates # and you want to get to the closest pixel. For example, if the X # value of where you want to set a pixel's color is 2.8, then int(X) # would give 2 while INT(X) would give 3, the closer value. #---------------------------------------------------------------------- def INT (N): return int(round(N)) #---------------------------------------------------------------------- # Plot a centered circle with an ring (outline) color and an interior # (fill) color. This is a simple extension to the JES addOval function # that also allows X, Y, and R to be floats, not just ints. #---------------------------------------------------------------------- def addCircle(Canvas, X,Y,R, Outline=black, Fill=white): addOvalFilled(Canvas, INT(X-R), INT(Y-R), INT(2*R), INT(2*R), Fill) addOval (Canvas, INT(X-R), INT(Y-R), INT(2*R), INT(2*R), Outline) return #---------------------------------------------------------------------- # Function to blend between two numeric values. P0, P1, and T may # all be either ints or floats. The return value is a float. # The value is somewhere between P0 and P1 based on T. If T=0 then # the return value is P0 and if T=1 the return value is P1. # If 0 point on the canvas Scale2D = 1.0 # Number of Pixels (screen coordinates) per Unit (world coordinates) Cosine30 = 0.866 # math.cos(math.radians(30.0)) # Projection angle Sine30 = 0.5 # math.sin(math.radians(30.0)) # Projection angle def Project3D (P3D): # Projects P3D [X,Y,Z] (world) into 2 dimensions [X,Y] (screen) global Origin2D,Scale2D X = Origin2D[0] + (P3D[0] + P3D[2] * Cosine30) * Scale2D Y = Origin2D[1] - (P3D[1] + P3D[2] * Sine30) * Scale2D return [X,Y] def SetOrigin2D(P2D): # Sets the position on the Canvas of the center of the 3D coordinates global Origin2D Origin2D = P2D return def SetScale2D (N): # Sets the number of screen pixels per world unit global Scale2D Scale2D = N return #---------------------------------------------------------------------- # Geometry routines that deal with [X,Y,Z] and [X,Y] points #---------------------------------------------------------------------- def addLine2D (Canvas, P0, P1, NewColor=black): # P0 and P1 are [X,Y] 2D points addLine(Canvas, INT(P0[0]), INT(P0[1]), INT(P1[0]), INT(P1[1]), NewColor) return def addCircle2D (Canvas, P, R, Outline=black, Fill=white): # P is an [X,Y] 2D point, R is radius addCircle(Canvas, P[0], P[1], R, Outline, Fill) return def addLine3D (Canvas, P0, P1, NewColor=black): # P0 and P1 are [X,Y,Z] 3D points in World coordinates addLine2D (Canvas, Project3D(P0), Project3D(P1), NewColor) return # CHALLENGE: Can you combine Distance2D and Distance3D (and Distance4D, etc.) # into a single function? def Distance2D (P0,P1): # P0, P1 are both [X,Y] DX = P1[0] - P0[0] # Delta X DY = P1[1] - P0[1] # Delta Y return math.sqrt(DX*DX + DY*DY) # Pythagorean Theorem in 2D def Distance3D (P0,P1): # P0, P1 are both [X,Y,Z] DX = P1[0] - P0[0] # Delta X DY = P1[1] - P0[1] # Delta Y DZ = P1[2] - P0[2] # Delta Z return math.sqrt(DX*DX + DY*DY + DZ*DZ) # Pythagorean Theorem in 3D #---------------------------------------------------------------------- # Draw a colored line between point P0 (with color C0) and point P1 # (with color C1) as a series of filled circles (wirh radius R). The # number of needed circles is adaptively computed based on the distance # between where the two points as projected onto the canvas, and the # radius of the circles. #---------------------------------------------------------------------- def addColoredLine (Canvas, P0, P1, C0, C1, R=3): D = Distance2D(Project3D(P0), Project3D(P1)) N = INT(D / R) for I in range(N): T = I / (N - 1.0) P = BlendPoints(P0, P1, T) C = BlendColors(C0, C1, T) addCircle2D(Canvas, Project3D(P), R, C, C) return #---------------------------------------------------------------------- # Test Driver Program #---------------------------------------------------------------------- def Main(): Canvas = makeEmptyPicture(600,400) SetOrigin2D([300,200]) SetScale2D(15.0) #------------------------------- # 3D Coordinate Axes #------------------------------- addLine3D(Canvas, [-10,0,0], [+10,0,0], black) addLine3D(Canvas, [0,-10,0], [0,+10,0], blue) addLine3D(Canvas, [0,0,-10], [0,0,+10], red) #------------------------------- # Tic Marks along Axes #------------------------------- for I in range(-10,+11): addLine3D(Canvas, [I,0,-0.5], [I,0,+0.5], black) addLine3D(Canvas, [-0.5,0,I], [+0.5,0,I], red) addLine3D(Canvas, [-0.5,I,0], [+0.5,I,0], blue) #------------------------------- # Plot a point in space, with # lines to show where it is # along the axes. #------------------------------- Point = [4.5,5,6] PointY0 = [4.5,0,6] PointX0Y0 = [ 0,0,6] PointY0Z0 = [4.5,0,0] addLine3D(Canvas, Point, PointY0, green) addLine3D(Canvas, PointY0, PointX0Y0, green) addLine3D(Canvas, PointY0, PointY0Z0, green) addCircle2D(Canvas, Project3D(Point), 5, black, green) #------------------------------- # Plot equally-spaced points # between P0 and P1, blending # colors as we go. #------------------------------- P0 = [7,2,-20] P1 = [-7,-2,10] addColoredLine(Canvas, P0, P1, blue, green, 5) #------------------------------- show(Canvas) return