;
; Title:	ZX Spectrum Vector Output Routines
; Author:	Dean Belfield
; Created:	30/06/2012
; Last Updated:	05/07/2012
;
; Requires:	output.asm
;
; Modinfo:
;
; 03/07/2012:	Simplified line draw; now always draws the line down. Relabled to improve clarity
;		Fixed bug in point tables; one entry had 7 bits
; 04/07/2012:	Added Draw_Horz_Line_Solid & Draw_Horz_Line_Texture; special fast case used for drawing polygons
; 05/07/2012:	Added Draw_Circle routine; needs some optimisation, more a proof of concept at the moment
;

;
; Most of these routines will precalculate the screen address in HL and return a bit position in A (0-7).
; The bit position is used as an index into the table Plot_Point; this contains a pixel set in the correct
; position for each bit, so 0=>%10000000, 1=>%01000000 and so on.
;
; The line routine, for example, will only do this slow calculation once and will work relative to that position
; for the rest of the draw. So, to move the pixel right if the data is stored in register D you would RRC D.
; At this point, if the pixel is %00000010 it would then be %00000001. If the pixel is then rotated again, it would
; be %10000000 and the carry bit would be set. This is picked up in the code and an INC or DEC L is used to move
; HL to the next adjacent character position.
;
; The function Pixel_Address_Down takes HL and move down or up one pixel line, taking into account the 
; Spectrum's strange screen layout. Again, this is quicker than calculating the address from scratch each
; time as most of the time it's just doing an INC H (or DEC H).
;
; For the sake of clarity I've used CALLs within loops; I wouldn't normally do this in speed critical code but felt
; that I'd lose clarity if I didn't. Feel free to inline any code that is called.
;
; Finally, this code uses a lot of self-modifying code; the code is in RAM so it is possible to use the code to 
; modify itself. This is used in the line routine to adjust the line drawing loop to draw in all four quadrants.
;

; Plot routine
; B = Y pixel position
; C = X pixel position
;
Plot:			CALL Get_Pixel_Address		; Get screen address in HL, pixel position (0 to 7) in A
			LD BC,Plot_Point		; Address of point lookup table
			ADD A,C				; Add pixel position to get entry in table
			LD C,A
			LD A,(BC)			; Get pixel data from table
			OR (HL)				; OR with screen data
			LD (HL),A			; Write back to screen
			RET


; Unplot routine
; B = Y pixel position
; C = X pixel position
;
Unplot:			CALL Get_Pixel_Address		; Same as Plot...
			LD BC,Unplot_Point
			ADD A,C
			LD C,A
			LD A,(BC)
			AND (HL)			; AND with screen data
			LD (HL),A
			RET

; Draw Circle (Beta - uses Plot to draw the circle outline as a proof of concept)
; B = Y pixel position of circle centre
; C = X pixel position of circle centre
; A = Radius of circle
;
Draw_Circle:		AND A				; Zero radius
			JR Z,Plot			; Just plot the point
			LD (Draw_Circle_M1 + 1),BC	; Store circle origin

			LD IXH,A			; IXH = Y
			LD IXL,0			; IXL = X
;
; Calculate BC (D2) = 3-(R*2)
;
			LD H,0				; HL = R
			LD L,A
			ADD HL,HL			; HL = R*2
			EX DE,HL			; DE = R*2
			LD HL,3
			AND A
			SBC HL,DE			; HL = 3-(R*2)
			LD B,H
			LD C,L
;
; Calculate HL (Delta) = 1-R
;
			LD HL,1
			LD D,0
			LD E,IXL
			AND A
			SBC HL,DE			; HL = 1 - CR
;
; SET DE (D2) = 1
;
			LD DE,1

Draw_Circle_Loop:	LD A,IXH			; Get Y in A
			CP IXL				; Compare with X
			RET C				; Return if X>Y
;
; The routine only calculates an eighth of the circle, so use symnmetry to draw
;			
			EXX
Draw_Circle_M1:		LD DE,0				; Get the circle origin

			LD A,E
			ADD A,IXL
			LD C,A
			LD A,D
			ADD A,IXH
			LD B,A
			CALL Plot			; Plot CX+X,CY+Y
			LD A,E
			SUB IXL
			LD C,A
			LD A,D
			ADD A,IXH
			LD B,A
			CALL Plot			; Plot CX-X,CY+Y
			LD A,E
			ADD A,IXL
			LD C,A
			LD A,D
			SUB IXH
			LD B,A
			CALL Plot			; Plot CX+X,CY-Y
			LD A,E
			SUB IXL
			LD C,A
			LD A,D
			SUB IXH
			LD B,A
			CALL Plot			; Plot CY+X,CX-Y
			LD A,D
			ADD A,IXL
			LD B,A
			LD A,E
			ADD A,IXH
			LD C,A
			CALL Plot			; Plot CY+X,CX+Y
			LD A,D
			SUB IXL
			LD B,A
			LD A,E
			ADD A,IXH
			LD C,A
			CALL Plot			; Plot CY-X,CX+Y
			LD A,D
			ADD A,IXL
			LD B,A
			LD A,E
			SUB IXH
			LD C,A
			CALL Plot			; Plot CY+X,CX-Y
			LD A,D
			SUB IXL
			LD B,A
			LD A,E
			SUB IXH
			LD C,A
			CALL Plot			; Plot CX+X,CY-Y
			EXX
;
; Do the incremental circle thing here
;
			BIT 7,H				; Check for Hl<=0
			JR Z,Draw_Circle_1
			ADD HL,DE			; Delta=Delta+D1
			JR Draw_Circle_2		; 
Draw_Circle_1:		ADD HL,BC			; Delta=Delta+D2
			INC BC
			INC BC				; D2=D2+2
			DEC IXH				; Y=Y-1
Draw_Circle_2:		INC BC				; D2=D2+2
			INC BC
			INC DE				; D1=D1+2
			INC DE	
			INC IXL				; X=X+1
			JR Draw_Circle_Loop
			

; Draw Triangle
; IY = Pointer to 3 bytes worth of coordinate data
;
Draw_Triangle:		LD C,(IY+0)
			LD B,(IY+1)
			LD E,(IY+2)
			LD D,(IY+3)
			CALL Draw_Line
			LD C,(IY+2)
			LD B,(IY+3)
			LD E,(IY+4)
			LD D,(IY+5)
			CALL Draw_Line
			LD C,(IY+4)
			LD B,(IY+5)
			LD E,(IY+0)
			LD D,(IY+1)
			JP Draw_Line

; Erase Triangle
; IY = Pointer to 3 bytes worth of coordinate data
;
Erase_Triangle:		LD C,(IY+0)
			LD B,(IY+1)
			LD E,(IY+2)
			LD D,(IY+3)
			CALL Erase_Line
			LD C,(IY+2)
			LD B,(IY+3)
			LD E,(IY+4)
			LD D,(IY+5)
			CALL Erase_Line
			LD C,(IY+4)
			LD B,(IY+5)
			LD E,(IY+0)
			LD D,(IY+1)
			JP Erase_Line

; Draw Horizontal Line routine
; B = Y coordinate
; C = X pixel position 1
; E = X pixel position 2
;
Draw_Horz_Line_Solid:	LD A,&FF			; Set texture to all 1's and drop through...

; Draw Horizontal Line routine with texture
; B = Y coordinate
; C = X pixel position 1
; E = X pixel position 2
; A = Texture byte
Draw_Horz_Line_Texture:	LD IXL,A			; Store the texture byte
			LD A,E				; Make sure we're always drawing left to right
			CP C
			JR NC,Draw_Horz_Line_1
			LD E,C				; Swap X coordinates round if not
			LD C,A
Draw_Horz_Line_1:	CALL Get_Pixel_Address		; Get the screen address of the left point
			EX AF,AF'			; Store in DE,A'
			EX DE,HL			; So E (X pixel position 2) will now be in L
			LD C,L
			CALL Get_Pixel_Address		; Get the screen address of the right point
;
; At this point, we have:
; DE = Screen address of point 1
; A' = Pixel position of point 1 (0-7)
; HL = Screen address of point 2
; A  = Pixel position of point 2 (0-7)
;
			LD BC,Plot_Line_RHS		; Point BC to the table for the RHS pixel data
			ADD A,C
			LD C,A
			LD A,L				; We have a special case if both points in same character
			CP E
			JR NZ,Draw_Horz_Line_2		; Special case if both points in same character square
			EX AF,AF'
			LD HL,Plot_Line_LHS		; Get the LHS pixel data
			ADD A,L
			LD L,A
			LD A,(BC)			; XOR the RHS and LHS pixel data
			XOR (HL)
			CPL				; CPL it (so all 1's become 0's and 0's become 1's)
			AND IXL				; Mask with the texture byte
			EX DE, HL			; This is now the data we are going to draw
			OR (HL)
			LD (HL),A
			RET				; Nothing more to do at this point, so return
Draw_Horz_Line_2:	LD A,(BC)			; Plot the RHS end
			AND IXL
			OR (HL)
			LD (HL),A
			DEC L				; Go left one character
			LD A,L				; Skip next bit if no between fill to do
			CP E
			JR Z,Draw_Horz_Line_End	
Draw_Horz_Line_M1:	LD D,IXL			; Stick the texture byte in D for speed
Draw_Horz_Line_Loop:	LD (HL),D			; Draw a filler byte
			DEC L				; Go left one character
			LD A,E				; Loop until we reach the left hand point character square
			CP L
			JR NZ,Draw_Horz_Line_Loop
Draw_Horz_Line_End:	EX AF,AF'			; Plot the LHS end
			LD BC,Plot_Line_LHS
			ADD A,C
			LD C,A
			LD A,(BC)
			AND IXL
			OR (HL)
			LD (HL),A
			RET

; Draw Line routine
; B = Y pixel position 1
; C = X pixel position 1
; D = Y pixel position 2
; E = X pixel position 2
;
Draw_Line:		LD A,D				; Check whether we are going to be drawing up
			CP B
			JR NC,Draw_Line_1

			PUSH BC				; If we are, then this neat trick swaps BC and DE
			PUSH DE				; using the stack, forcing the line to be always
			POP BC				; drawn downwards
			POP DE

Draw_Line_1:		CALL Get_Pixel_Address		; Get screen address in HL, pixel position (0-7) in A
;
; At this point we have
;  A = Pixel position (0-7)
; HL = Screen address of the start point
; BC = Start coordinate (B=Y1, C=X1)
; DE = End coordinates  (D=Y2, E=X2)
;
			LD IX,Plot_Point		; Point to the Plot_Point table
			ADD A,IXL			; Add the pixel position to get entry in table
			LD IXL,A

			LD A,D				; Calculate the line height in B (Y2-Y1)
			SUB B
			LD B,A
	
			LD A,E				; Calculate the line width in C (X2-X1)
			SUB C
			JR C,Draw_Line_X1		; If carry set (negative result) then we are drawing from right to left
;
; This bit of code mods the main loop for drawing left to right
;
			LD C,A				; Store the line width
			LD A,&2C			; Code for INC L
			LD (Draw_Line_Q1_M3),A		; Mod the code
			LD (Draw_Line_Q2_M3),A
			LD A,&0A			; Code for RRC D (CB 0A)
			JR Draw_Line_X2			; Skip the next bit
;
; This bit of code mods the main loop for drawing right to left
;
Draw_Line_X1:		NEG				; The width of line is negative, so make it positive again
			LD C,A				; Store the line width
			LD A,&2D			; Code for DEC L
			LD (Draw_Line_Q1_M3),A
			LD (Draw_Line_Q2_M3),A
			LD A,&02			; Code for RLC D (CB 02)
;
; We've got the basic information at this point
;
Draw_Line_X2:		LD (Draw_Line_Q1_M2 + 1),A	; A contains the code for RLC D or RRC D, so make the mods
			LD (Draw_Line_Q2_M2 + 1),A
			LD D,(IX+0)			; Get the pixel data from the Point_Plot table
			LD A,B				; Check if B and C are 0
			OR C
			JR NZ,Draw_Line_Q		; There is a line to draw, so skip to the next bit
			LD A,(HL)			; Here we've got a single point line, so plot and return
			OR D
			LD (HL),A
			RET
;
; At this point
; HL = Screen address of the start point
;  B = Line height
;  C = Line width
;  D = Pixel data
;
Draw_Line_Q:		LD A,B				; Work out which diagonal we are on
			CP C
			JR NC,Draw_Line_Q2
;
; This bit of code draws the line where B<C (more horizontal than vertical)
;
Draw_Line_Q1:		LD A,C
			LD (Draw_Line_Q1_M1 + 1),A	; Self-mod the code again to store the line width
			LD C,B
			LD B,A
			LD E,C				; Calculate the error value
			SRL E
Draw_Line_Q1_L:		LD A,(HL)			; Plot the pixel
			OR D
			LD (HL),A
			LD A,E
			SUB C
			LD E,A
			JR NC,Draw_Line_Q1_M2
Draw_Line_Q1_M1:	ADD A,0				; Add the line height (previously stored; self modifying code)
			LD E,A
			CALL Pixel_Address_Down
Draw_Line_Q1_M2:	RRC D				; Rotate the pixel right or left; more self-modifying code
			JR NC,Draw_Line_Q1_S
Draw_Line_Q1_M3:	INC L				; If we get a carry then move to adjacent screen address; more self modifying code
Draw_Line_Q1_S:		DJNZ Draw_Line_Q1_L		; Loop until the line is drawn
			RET
;
; This bit draws the line where B>=C (more vertical than horizontal, or diagonal)
;
Draw_Line_Q2:		LD (Draw_Line_Q2_M1 + 1),A
			LD E,C				; Calculate the error value
			SRL E
Draw_Line_Q2_L:		LD A,(HL)			; Plot the pixel
			OR D
			LD (HL),A
			LD A,E				; Get the error value
			SUB C				; Add the line length to it (X2-X1)
			JR NC,Draw_Line_Q2_S		; Skip the next bit if we don't get a carry
Draw_Line_Q2_M1: 	ADD A,0				; Add the line height (previously stored; self modifying code)
Draw_Line_Q2_M2:	RRC D				; Rotates the pixel right with carry
			JR NC,Draw_Line_Q2_S
Draw_Line_Q2_M3:	INC L				; If we get a carry then move to adjacent screen address; more self modifying code
Draw_Line_Q2_S:		LD E,A				; Store the error value back in
			CALL Pixel_Address_Down		; And also move down
			DJNZ Draw_Line_Q2_L
			RET

; Erase Line routine
; B = Y pixel position 1
; C = X pixel position 1
; D = Y pixel position 2
; E = X pixel position 2
;
Erase_Line:		LD A,D				; Check whether we are going to be drawing up
			CP B
			JR NC,Erase_Line_1

			PUSH BC				; If we are, then this neat trick swaps BC and DE
			PUSH DE				; using the stack, forcing the line to be always
			POP BC				; drawn downwards
			POP DE

Erase_Line_1:		CALL Get_Pixel_Address		; Get screen address in HL, pixel position (0-7) in A
;
; At this point we have
;  A = Pixel position (0-7)
; HL = Screen address of the start point
; BC = Start coordinate (B=Y1, C=X1)
; DE = End coordinates  (D=Y2, E=X2)
;
			LD IX,Unplot_Point		; Point to the Unplot_Point table
			ADD A,IXL			; Add the pixel position to get entry in table
			LD IXL,A

			LD A,D				; Calculate the line height in B (Y2-Y1)
			SUB B
			LD B,A
	
			LD A,E				; Calculate the line width in C (X2-X1)
			SUB C
			JR C,Erase_Line_X1		; If carry set (negative result) then we are drawing from right to left
;
; This bit of code mods the main loop for drawing left to right
;
			LD C,A				; Store the line width
			LD A,&2C			; Code for INC L
			LD (Erase_Line_Q1_M3),A		; Mod the code
			LD (Erase_Line_Q2_M3),A
			LD A,&0A			; Code for RRC D (CB 0A)
			JR Erase_Line_X2		; Skip the next bit
;
; This bit of code mods the main loop for drawing right to left
;
Erase_Line_X1:		NEG				; The width of line is negative, so make it positive again
			LD C,A				; Store the line width
			LD A,&2D			; Code for DEC L
			LD (Erase_Line_Q1_M3),A
			LD (Erase_Line_Q2_M3),A
			LD A,&02			; Code for RLC D (CB 02)
;
; We've got the basic information at this point
;
Erase_Line_X2:		LD (Erase_Line_Q1_M2 + 1),A	; A contains the code for RLC D or RRC D, so make the mods
			LD (Erase_Line_Q2_M2 + 1),A
			LD D,(IX+0)			; Get the pixel data from the Unplot_Point table
			LD A,B				; Check if B and C are 0
			OR C
			JR NZ,Erase_Line_Q		; There is a line to draw, so skip to the next bit
			LD A,(HL)			; Here we've got a single point line, so plot and return
			AND D
			LD (HL),A
			RET
;
; At this point
; HL = Screen address of the start point
;  B = Line height
;  C = Line width
;  D = Pixel data
;
Erase_Line_Q:		LD A,B				; Work out which diagonal we are on
			CP C
			JR NC,Erase_Line_Q2
;
; This bit of code draws the line where B<C (more horizontal than vertical)
;
Erase_Line_Q1:		LD A,C
			LD (Erase_Line_Q1_M1 + 1),A	; Self-mod the code again to store the line width
			LD C,B
			LD B,A
			LD E,C				; Calculate the error value
			SRL E
Erase_Line_Q1_L:	LD A,(HL)			; Unplot the pixel
			AND D
			LD (HL),A
			LD A,E
			SUB C
			LD E,A
			JR NC,Erase_Line_Q1_M2
Erase_Line_Q1_M1:	ADD A,0				; Add the line height (previously stored; self modifying code)
			LD E,A
			CALL Pixel_Address_Down
Erase_Line_Q1_M2:	RRC D				; Rotate the pixel right or left; more self-modifying code
			JR C,Erase_Line_Q1_S		; Note the change here from the Draw_Line routine
Erase_Line_Q1_M3:	INC L				; If we get no carry then move to adjacent screen address; more self modifying code
Erase_Line_Q1_S:	DJNZ Erase_Line_Q1_L		; Loop until the line is drawn
			RET
;
; This bit draws the line where B>=C (more vertical than horizontal, or diagonal)
;
Erase_Line_Q2:		LD (Erase_Line_Q2_M1 + 1),A
			LD E,C				; Calculate the error value
			SRL E
Erase_Line_Q2_L:	LD A,(HL)			; Unplot the pixel
			AND D
			LD (HL),A
			LD A,E				; Get the error value
			SUB C				; Add the line length to it (X2-X1)
			JR NC,Erase_Line_Q2_S		; Skip the next bit if we don't get a carry
Erase_Line_Q2_M1: 	ADD A,0				; Add the line height (previously stored; self modifying code)
Erase_Line_Q2_M2:	RRC D				; Rotates the pixel right with carry
			JR C,Erase_Line_Q2_S		; Note the change here from the Draw_Line routine
Erase_Line_Q2_M3:	INC L				; If we get no carry then move to adjacent screen address; more self modifying code
Erase_Line_Q2_S:	LD E,A				; Store the error value back in
			CALL Pixel_Address_Down		; And also move down
			DJNZ Erase_Line_Q2_L
			RET

; Note that the functions above only work if each of these tables are in a byte boundary
;
Plot_Point:		DB %10000000,%01000000,%00100000,%00010000,%00001000,%00000100,%00000010,%00000001
Unplot_Point:		DB %01111111,%10111111,%11011111,%11101111,%11110111,%11111011,%11111101,%11111110

Plot_Line_LHS:		DB %11111111,%01111111,%00111111,%00011111,%00001111,%00000111,%00000011,%00000001
Plot_Line_RHS:		DB %10000000,%11000000,%11100000,%11110000,%11111000,%11111100,%11111110,%11111111