about summary refs log blame commit diff stats
path: root/WWW/Library/Implementation/HTWSRC.c
blob: 731ac5c709667f27d5500b68a8717e42e69d6a49 (plain) (tree)
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/*			Parse WAIS Source file			HTWSRC.c
**			======================
**
**	This module parses a stream with WAIS source file
**	format information on it and creates a structured stream.
**	That structured stream is then converted into whatever.
**
**	3 June 93	Bug fix: Won't crash if no description
*/

#include "HTUtils.h"
#include "tcp.h"

#include "HTWSRC.h"


/* #include <sys/types.h>	already in tcp.h */
/* #include <sys/stat.h>  	this too         */
/* #include <stdio.h> included in HTUtils.h -- FM */
#include "HTML.h"
#include "HTParse.h"

#include "LYLeaks.h"

#define FREE(x) if (x) {free(x); x = NULL;}

#define BIG 10000		/* Arbitrary limit to value length */
#define PARAM_MAX BIG
#define CACHE_PERIOD (7*86400)	/* Time to keep .src file in seconds */

#define HEX_ESCAPE '%'

struct _HTStructured {
	CONST HTStructuredClass *	isa;
	/* ... */
};

#define PUTC(c) (*me->target->isa->put_character)(me->target, c)
#define PUTS(s) (*me->target->isa->put_string)(me->target, s)
#define START(e) (*me->target->isa->start_element)(me->target, e, 0, 0, -1, 0)
#define END(e) (*me->target->isa->end_element)(me->target, e, 0)
#define MAYBE_END(e) if (HTML_dtd.tags[e].contents != SGML_EMPTY) \
                        (*me->target->isa->end_element)(me->target, e, 0)


/*	Here are the parameters which can be specified in a  source file
*/
PRIVATE CONST char* par_name[] = {
	"version", 
	"ip-address",
#define PAR_IP_NAME 2
	"ip-name", 
#define PAR_TCP_PORT 3
	"tcp-port", 
#define PAR_DATABASE_NAME 4
	"database-name",
#define PAR_COST 5
	"cost", 
#define PAR_COST_UNIT 6
	"cost-unit", 
#define PAR_FREE 7
	"free",	
#define PAR_MAINTAINER 8
	"maintainer", 	
#define PAR_DESCRIPTION 9
	"description",
	"keyword-list", 	
	"source",
	"window-geometry",
	"configuration",
	"script",
	"update-time",
	"contact-at",
	"last-contacted",
	"confidence",
	"num-docs-to-request",
	"font",
	"font-size",
#define PAR_UNKNOWN 22
	"unknown",
	0,				/* Terminate list */
#define PAR_COUNT 23
} ;


enum tokenstate { beginning, before_tag, colon, before_value,
		value, bracketed_value, quoted_value, escape_in_quoted, done };


/*		Stream Object
**		------------
**
**	The target is the structured stream down which the
**	parsed results will go.
**
**	all the static stuff below should go in here to make it reentrant
*/

struct _HTStream {
	CONST HTStreamClass *	isa;
	HTStructured *		target;
	char *			par_value[PAR_COUNT];
	enum tokenstate 	state;
	char 			param[BIG+1];
	int			param_number;
	int			param_count;
};




PUBLIC CONST char * hex = "0123456789ABCDEF";

/*	Decode one hex character
*/

PUBLIC char from_hex ARGS1(char, c)
{
    return 		  (c>='0')&&(c<='9') ? c-'0'
			: (c>='A')&&(c<='F') ? c-'A'+10
			: (c>='a')&&(c<='f') ? c-'a'+10
			:		       0;
}


/*			State machine
**			-------------
**
** On entry,
**	me->state	is a valid state (see WSRC_init)
**	c		is the next character
** On exit,
** 	returns	1	Done with file
**		0	Continue. me->state is updated if necessary.
**		-1	Syntax error error
*/


/*		Treat One Character
**		-------------------
*/
PRIVATE void WSRCParser_put_character ARGS2(HTStream*, me, char, c)
{
    switch (me->state) {
    case beginning:
        if (c=='(') me->state = before_tag;
	break;
	
    case before_tag:
        if (c==')') {
	    me->state = done;
	    return;			/* Done with input file */
	} else if (c==':') {
	    me->param_count = 0;
	    me->state = colon;
	}				/* Ignore other text */
	break;

    case colon:
        if (WHITE(c)) {
	    me->param[me->param_count++] = 0;	/* Terminate */
	    for(me->param_number = 0; par_name[me->param_number]; me->param_number++) {
		if (0==strcmp(par_name[me->param_number], me->param)) {
		    break;
		}
	    }
	    if (!par_name[me->param_number]) {	/* Unknown field */
	        if (TRACE) fprintf(stderr,
		    "HTWSRC: Unknown field `%s' in source file\n",
		    me->param);
		me->param_number = PAR_UNKNOWN;
		me->state = before_value;	/* Could be better ignore */
		return;
	    }
	    me->state = before_value;
	} else {
	    if (me->param_count < PARAM_MAX)  me->param[me->param_count++] = c;
	}
	break;
	
    case before_value:
        if (c==')') {
	    me->state = done;
	    return;			/* Done with input file */
	}
	if (WHITE(c)) return;		/* Skip white space */
	me->param_count = 0;
	if (c=='"') {
	    me->state = quoted_value;
	    break;
	}
	me->state = (c=='"') ? quoted_value : 
		    (c=='(') ? bracketed_value : value;
	me->param[me->param_count++] = c;	/* Don't miss first character */
	break;

    case value:
        if (WHITE(c)) {
	    me->param[me->param_count] = 0;
	    StrAllocCopy(me->par_value[me->param_number], me->param);
	    me->state = before_tag;
	} else {
	    if (me->param_count < PARAM_MAX)  me->param[me->param_count++] = c;
	}
	break;

    case bracketed_value:
        if (c==')') {
	    me->param[me->param_count] = 0;
	    StrAllocCopy(me->par_value[me->param_number], me->param);
	    me->state = before_tag;
	    break;
	}
        if (me->param_count < PARAM_MAX)  me->param[me->param_count++] = c;
	break;
	
    case quoted_value:
        if (c=='"') {
	    me->param[me->param_count] = 0;
	    StrAllocCopy(me->par_value[me->param_number], me->param);
	    me->state = before_tag;
	    break;
	}
	
	if (c=='\\') {		/* Ignore escape but switch state */
	    me->state = escape_in_quoted;
	    break;
	}
	/* Fall through! */

    case escape_in_quoted:
        if (me->param_count < PARAM_MAX)  me->param[me->param_count++] = c;
	me->state = quoted_value;
	break;
	
    case done:				/* Ignore anything after EOF */
	return;

    } /* switch me->state */
}


/*			Open Cache file
**			===============
**
**   Bugs: Maybe for filesystem-challenged platforms (MSDOS for example) we
**   should make a hash code for the filename.
*/

#ifdef CACHE_FILE_PREFIX
PRIVATE BOOL write_cache ARGS1(HTStream *, me)
{
    FILE * fp;
    char cache_file_name[256];
    char * www_database;
    if (!me->par_value[PAR_DATABASE_NAME]
    	|| !me->par_value[PAR_IP_NAME]
	) return NO;
    
    www_database = HTEscape(me->par_value[PAR_DATABASE_NAME], URL_XALPHAS);
    sprintf(cache_file_name, "%sWSRC-%s:%s:%.100s.txt",
    	CACHE_FILE_PREFIX,
	me->par_value[PAR_IP_NAME],
	me->par_value[PAR_TCP_PORT] ? me->par_value[PAR_TCP_PORT] : "210",
	www_database);
    FREE(www_database);
    fp = fopen(cache_file_name, "w");
    if (!fp) return NO;
    
    if (me->par_value[PAR_DESCRIPTION])
        fputs(me->par_value[PAR_DESCRIPTION], fp);
    else 
        fputs("Description not available\n", fp);
    fclose(fp);
    return YES;
}
#endif

/*			Output equivalent HTML
**			----------------------
**
*/

void give_parameter ARGS2(HTStream *, me, int, p)
{
    PUTS(par_name[p]);
    if (me->par_value[p]) {
	PUTS(": ");
	PUTS(me->par_value[p]);
	PUTS("; ");
    } else {
        PUTS(" NOT GIVEN in source file; ");
    }
}


/*			Generate Outout
**			===============
*/
PRIVATE void WSRC_gen_html ARGS2(HTStream *, me, BOOL, source_file)

{
    if (me->par_value[PAR_DATABASE_NAME]) {
	char * shortname = 0;
	int l;
	StrAllocCopy(shortname, me->par_value[PAR_DATABASE_NAME]);
	l = strlen(shortname);
	if ( l > 4 && !strcasecomp(shortname + l -4, ".src")) {
	    shortname[l-4] = 0;	/* Chop of .src -- boring! */
	}
	
	START(HTML_HEAD);
	PUTS("\n");
	START(HTML_TITLE);
	PUTS(shortname);
	PUTS(source_file ? " WAIS source file" : " index");
	END(HTML_TITLE);
	PUTS("\n");
	END(HTML_HEAD);
    
	START(HTML_H1);
	PUTS(shortname);
	PUTS(source_file ? " description" : " index");
	END(HTML_H1);
	PUTS("\n");
	FREE(shortname);
    }
    
    START(HTML_DL);		/* Definition list of details */
    
    if (source_file) {
	START(HTML_DT);
	PUTS("Access links");
	MAYBE_END(HTML_DT);
	START(HTML_DD);
	if (me->par_value[PAR_IP_NAME] &&
	    me->par_value[PAR_DATABASE_NAME]) {
    
	    char WSRC_address[256];
	    char * www_database;
	    www_database = HTEscape(me->par_value[PAR_DATABASE_NAME],
	    	URL_XALPHAS);
	    sprintf(WSRC_address, "wais://%s%s%s/%s",
		me->par_value[PAR_IP_NAME],
		me->par_value[PAR_TCP_PORT] ? ":" : "",
		me->par_value[PAR_TCP_PORT] ? me->par_value[PAR_TCP_PORT] :"",
		www_database);
	
	    HTStartAnchor(me->target, NULL, WSRC_address);
	    PUTS("Direct access");
	    END(HTML_A);
	    /** Proxy will be used if defined, so let user know that - FM **/
	    PUTS(" (or via proxy server, if defined), or");
	    START(HTML_BR);
	    /** Offer W3 Consortium gateway - FM **/
	    sprintf(WSRC_address, "http://www.w3.org:8001/%s%s%s/%s",
		me->par_value[PAR_IP_NAME],
		me->par_value[PAR_TCP_PORT] ? ":" : "",
		me->par_value[PAR_TCP_PORT] ? me->par_value[PAR_TCP_PORT] :"",
		www_database);
	    HTStartAnchor(me->target, NULL, WSRC_address);
	    PUTS("through W3 Consortium gateway");
	    END(HTML_A);

	    FREE(www_database);
	    
	} else {
	    give_parameter(me, PAR_IP_NAME);
	    give_parameter(me, PAR_DATABASE_NAME);
	}
	MAYBE_END(HTML_DD);
    
    } /* end if source_file */
    
    if (me->par_value[PAR_MAINTAINER]) {
	START(HTML_DT);
	PUTS("Maintainer");
	MAYBE_END(HTML_DT);
	START(HTML_DD);
	PUTS(me->par_value[PAR_MAINTAINER]);
	MAYBE_END(HTML_DD);
    }
    if (me->par_value[PAR_IP_NAME]) {
    	START(HTML_DT);
    	PUTS("Host");
	MAYBE_END(HTML_DT);
    	START(HTML_DD);
    	PUTS(me->par_value[PAR_IP_NAME]);
	MAYBE_END(HTML_DD);
    }

    END(HTML_DL);

    if (me->par_value[PAR_DESCRIPTION]) {
	START(HTML_PRE);		/* Preformatted description */
	PUTS(me->par_value[PAR_DESCRIPTION]);
	END(HTML_PRE);
    }
    
    (*me->target->isa->_free)(me->target);
    
    return;
} /* generate html */


PRIVATE void WSRCParser_put_string ARGS2(HTStream *, context, CONST char*, str)
{
    CONST char *p;
    for(p=str; *p; p++)
        WSRCParser_put_character(context, *p);
}


PRIVATE void WSRCParser_write ARGS3(
		HTStream *, 	context,
		CONST char*, 	str,
		int, 		l)
{
    CONST char *p;
    CONST char *e = str+l;
    for(p=str; p<e; p++)
        WSRCParser_put_character(context, *p);
}


PRIVATE void WSRCParser_free ARGS1(HTStream *, me)
{
    WSRC_gen_html(me, YES);
#ifdef CACHE_FILE_PREFIX
    write_cache(me);
#endif
    {
	int p;
	for (p = 0; par_name[p]; p++) {	/* Clear out old values */
	    FREE(me->par_value[p]);
	}
    }
    FREE(me);
}

PRIVATE void WSRCParser_abort ARGS2(HTStream *, me, HTError, e)
{
    WSRCParser_free(me);
}


/*		Stream subclass		-- method routines
**		---------------
*/

HTStreamClass WSRCParserClass = {
	"WSRCParser",
	WSRCParser_free,
	WSRCParser_abort,
	WSRCParser_put_character,
 	WSRCParser_put_string,
	WSRCParser_write

};


/*		Converter from WAIS Source to whatever
**		--------------------------------------
*/
PUBLIC HTStream* HTWSRCConvert ARGS3(
	HTPresentation *,	pres,
	HTParentAnchor *,	anchor,	
	HTStream *,		sink)
{
    HTStream * me = (HTStream*) malloc(sizeof(*me));
    if (!me) outofmem(__FILE__, "HTWSRCConvert");

    me->isa = &WSRCParserClass;
    me->target = HTML_new(anchor, pres->rep_out, sink);

    {
	int p;
	for(p=0; p < PAR_COUNT; p++) {	/* Clear out parameter values */
	    me->par_value[p] = 0;
	}
    }
    me->state = beginning;

    return me;
}
="o">= Lines.current_drawing assert(drawing.mode == 'drawing') local x, y = love.mouse.getX(), love.mouse.getY() if love.mouse.isDown('1') then if Drawing.in_drawing(drawing, x,y) then if drawing.pending.mode == 'freehand' then table.insert(drawing.pending.points, {x=Drawing.coord(love.mouse.getX()-16), y=Drawing.coord(love.mouse.getY()-drawing.y)}) elseif drawing.pending.mode == 'move' then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) drawing.pending.target_point.x = mx drawing.pending.target_point.y = my end end elseif Current_drawing_mode == 'move' then if Drawing.in_drawing(drawing, x, y) then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) drawing.pending.target_point.x = mx drawing.pending.target_point.y = my end else -- do nothing end end function Drawing.mouse_released(x,y, button) if Current_drawing_mode == 'move' then Current_drawing_mode = Previous_drawing_mode Previous_drawing_mode = nil elseif Lines.current_drawing then local drawing = Lines.current_drawing if drawing.pending then if drawing.pending.mode == 'freehand' then -- the last point added during update is good enough table.insert(drawing.shapes, drawing.pending) elseif drawing.pending.mode == 'line' then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then local j = Drawing.insert_point(drawing.points, mx,my) drawing.pending.p2 = j table.insert(drawing.shapes, drawing.pending) end elseif drawing.pending.mode == 'manhattan' then local p1 = drawing.points[drawing.pending.p1] local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then if math.abs(mx-p1.x) > math.abs(my-p1.y) then local j = Drawing.insert_point(drawing.points, mx, p1.y) drawing.pending.p2 = j else local j = Drawing.insert_point(drawing.points, p1.x, my) drawing.pending.p2 = j end local p2 = drawing.points[drawing.pending.p2] love.mouse.setPosition(16+Drawing.pixels(p2.x), drawing.y+Drawing.pixels(p2.y)) table.insert(drawing.shapes, drawing.pending) end elseif drawing.pending.mode == 'polygon' then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then table.insert(drawing.pending.vertices, Drawing.insert_point(drawing.points, mx,my)) table.insert(drawing.shapes, drawing.pending) end elseif drawing.pending.mode == 'rectangle' then assert(#drawing.pending.vertices <= 2) if #drawing.pending.vertices == 2 then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then local first = drawing.points[drawing.pending.vertices[1]] local second = drawing.points[drawing.pending.vertices[2]] local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my) table.insert(drawing.pending.vertices, Drawing.insert_point(drawing.points, thirdx,thirdy)) table.insert(drawing.pending.vertices, Drawing.insert_point(drawing.points, fourthx,fourthy)) table.insert(drawing.shapes, drawing.pending) end else -- too few points; draw nothing end elseif drawing.pending.mode == 'square' then assert(#drawing.pending.vertices <= 2) if #drawing.pending.vertices == 2 then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then local first = drawing.points[drawing.pending.vertices[1]] local second = drawing.points[drawing.pending.vertices[2]] local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my) table.insert(drawing.pending.vertices, Drawing.insert_point(drawing.points, thirdx,thirdy)) table.insert(drawing.pending.vertices, Drawing.insert_point(drawing.points, fourthx,fourthy)) table.insert(drawing.shapes, drawing.pending) end end elseif drawing.pending.mode == 'circle' then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then local center = drawing.points[drawing.pending.center] drawing.pending.radius = geom.dist(center.x,center.y, mx,my) table.insert(drawing.shapes, drawing.pending) end elseif drawing.pending.mode == 'arc' then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then local center = drawing.points[drawing.pending.center] drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle) table.insert(drawing.shapes, drawing.pending) end elseif drawing.pending.mode == 'move' then -- drop it elseif drawing.pending.mode == 'name' then -- drop it else print(drawing.pending.mode) assert(false) end Lines.current_drawing.pending = {} Lines.current_drawing = nil end end end function Drawing.keychord_pressed(chord) if chord == 'C-p' and not love.mouse.isDown('1') then Current_drawing_mode = 'freehand' elseif chord == 'C-g' and not love.mouse.isDown('1') then Current_drawing_mode = 'polygon' elseif love.mouse.isDown('1') and chord == 'g' then Current_drawing_mode = 'polygon' local drawing = Drawing.current_drawing() if drawing.pending.mode == 'freehand' then drawing.pending.vertices = {Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y)} elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then if drawing.pending.vertices == nil then drawing.pending.vertices = {drawing.pending.p1} end elseif drawing.pending.mode == 'square' or drawing.pending.mode == 'rectangle' then -- reuse existing vertices elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then drawing.pending.vertices = {drawing.pending.center} end drawing.pending.mode = 'polygon' elseif chord == 'C-r' and not love.mouse.isDown('1') then Current_drawing_mode = 'rectangle' elseif love.mouse.isDown('1') and chord == 'r' then Current_drawing_mode = 'rectangle' local drawing = Drawing.current_drawing() if drawing.pending.mode == 'freehand' then drawing.pending.vertices = {Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y)} elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then if drawing.pending.vertices == nil then drawing.pending.vertices = {drawing.pending.p1} end elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then drawing.pending.vertices = {drawing.pending.center} elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then -- reuse existing (1-2) vertices end drawing.pending.mode = 'rectangle' elseif chord == 'C-s' and not love.mouse.isDown('1') then Current_drawing_mode = 'square' elseif love.mouse.isDown('1') and chord == 's' then Current_drawing_mode = 'square' local drawing = Drawing.current_drawing() if drawing.pending.mode == 'freehand' then drawing.pending.vertices = {Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y)} elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then if drawing.pending.vertices == nil then drawing.pending.vertices = {drawing.pending.p1} end elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then drawing.pending.vertices = {drawing.pending.center} elseif drawing.pending.mode == 'rectangle' then -- reuse existing (1-2) vertices elseif drawing.pending.mode == 'polygon' then while #drawing.pending.vertices > 2 do table.remove(drawing.pending.vertices) end end drawing.pending.mode = 'square' elseif love.mouse.isDown('1') and chord == 'p' and (Current_drawing_mode == 'polygon' or Current_drawing_mode == 'rectangle' or Current_drawing_mode == 'square') then local drawing = Drawing.current_drawing() local mx,my = Drawing.coord(love.mouse.getX()-16), Drawing.coord(love.mouse.getY()-drawing.y) local j = Drawing.insert_point(drawing.points, mx,my) table.insert(drawing.pending.vertices, j) elseif chord == 'C-o' and not love.mouse.isDown('1') then Current_drawing_mode = 'circle' elseif love.mouse.isDown('1') and chord == 'a' and Current_drawing_mode == 'circle' then local drawing = Drawing.current_drawing() drawing.pending.mode = 'arc' local mx,my = Drawing.coord(love.mouse.getX()-16), Drawing.coord(love.mouse.getY()-drawing.y) local j = Drawing.insert_point(drawing.points, mx,my) local center = drawing.points[drawing.pending.center] drawing.pending.radius = geom.dist(center.x,center.y, mx,my) drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my) elseif love.mouse.isDown('1') and chord == 'o' then Current_drawing_mode = 'circle' local drawing = Drawing.current_drawing() if drawing.pending.mode == 'freehand' then drawing.pending.center = Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y) elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then drawing.pending.center = drawing.pending.p1 elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then drawing.pending.center = drawing.pending.vertices[1] end drawing.pending.mode = 'circle' elseif love.mouse.isDown('1') and chord == 'l' then Current_drawing_mode = 'line' local drawing = Drawing.current_drawing() if drawing.pending.mode == 'freehand' then drawing.pending.p1 = Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y) elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then drawing.pending.p1 = drawing.pending.center elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then drawing.pending.p1 = drawing.pending.vertices[1] end drawing.pending.mode = 'line' elseif chord == 'C-l' and not love.mouse.isDown('1') then Current_drawing_mode = 'line' elseif love.mouse.isDown('1') and chord == 'm' then Current_drawing_mode = 'manhattan' local drawing = Drawing.select_drawing_at_mouse() if drawing.pending.mode == 'freehand' then drawing.pending.p1 = Drawing.insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y) elseif drawing.pending.mode == 'line' then -- do nothing elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then drawing.pending.p1 = drawing.pending.vertices[1] elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then drawing.pending.p1 = drawing.pending.center end drawing.pending.mode = 'manhattan' elseif chord == 'C-m' and not love.mouse.isDown('1') then Current_drawing_mode = 'manhattan' elseif chord == 'C-s' and not love.mouse.isDown('1') then local drawing,_,shape = Drawing.select_shape_at_mouse() if drawing then smoothen(shape) end elseif chord == 'C-u' and not love.mouse.isDown('1') then local drawing,_,p = Drawing.select_point_at_mouse() if drawing then if Previous_drawing_mode == nil then Previous_drawing_mode = Current_drawing_mode end Current_drawing_mode = 'move' drawing.pending = {mode=Current_drawing_mode, target_point=p} Lines.current_drawing = drawing end elseif love.mouse.isDown('1') and chord == 'v' then local drawing,_,p = Drawing.select_point_at_mouse() if drawing then if Previous_drawing_mode == nil then Previous_drawing_mode = Current_drawing_mode end Current_drawing_mode = 'move' drawing.pending = {mode=Current_drawing_mode, target_point=p} Lines.current_drawing = drawing end elseif chord == 'C-n' and not love.mouse.isDown('1') then local drawing,point_index,p = Drawing.select_point_at_mouse() if drawing then if Previous_drawing_mode == nil then -- don't clobber Previous_drawing_mode = Current_drawing_mode end Current_drawing_mode = 'name' p.name = '' drawing.pending = {mode=Current_drawing_mode, target_point=point_index} Lines.current_drawing = drawing end elseif chord == 'C-d' and not love.mouse.isDown('1') then local drawing,i,p = Drawing.select_point_at_mouse() if drawing then for _,shape in ipairs(drawing.shapes) do if Drawing.contains_point(shape, i) then if shape.mode == 'polygon' then local idx = table.find(shape.vertices, i) assert(idx) table.remove(shape.vertices, idx) if #shape.vertices < 3 then shape.mode = 'deleted' end else shape.mode = 'deleted' end end end drawing.points[i].deleted = true end local drawing,_,shape = Drawing.select_shape_at_mouse() if drawing then shape.mode = 'deleted' end elseif chord == 'C-h' and not love.mouse.isDown('1') then local drawing = Drawing.select_drawing_at_mouse() if drawing then drawing.show_help = true end end end function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y) if firstx == secondx then return x,secondy, x,firsty end if firsty == secondy then return secondx,y, firstx,y end local first_slope = (secondy-firsty)/(secondx-firstx) -- slope of second edge: -- -1/first_slope -- equation of line containing the second edge: -- y-secondy = -1/first_slope*(x-secondx) -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0 -- now we want to find the point on this line that's closest to the mouse pointer. -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation local a = 1/first_slope local c = -secondy - secondx/first_slope local thirdx = ((x-a*y) - a*c) / (a*a + 1) local thirdy = (a*(-x + a*y) - c) / (a*a + 1) -- slope of third edge = first_slope -- equation of line containing third edge: -- y - thirdy = first_slope*(x-thirdx) -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0 -- now we want to find the point on this line that's closest to the first point local a = -first_slope local c = -thirdy + thirdx*first_slope local fourthx = ((firstx-a*firsty) - a*c) / (a*a + 1) local fourthy = (a*(-firstx + a*firsty) - c) / (a*a + 1) return thirdx,thirdy, fourthx,fourthy end function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y) -- use x,y only to decide which side of the first edge to complete the square on local deltax = secondx-firstx local deltay = secondy-firsty local thirdx = secondx+deltay local thirdy = secondy-deltax if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then deltax = -deltax deltay = -deltay thirdx = secondx+deltay thirdy = secondy-deltax end local fourthx = firstx+deltay local fourthy = firsty-deltax return thirdx,thirdy, fourthx,fourthy end function Drawing.current_drawing() local x, y = love.mouse.getX(), love.mouse.getY() for _,drawing in ipairs(Lines) do if drawing.mode == 'drawing' then if Drawing.in_drawing(drawing, x,y) then return drawing end end end return nil end function Drawing.select_shape_at_mouse() for _,drawing in ipairs(Lines) do if drawing.mode == 'drawing' then local x, y = love.mouse.getX(), love.mouse.getY() if Drawing.in_drawing(drawing, x,y) then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) for i,shape in ipairs(drawing.shapes) do assert(shape) if geom.on_shape(mx,my, drawing, shape) then return drawing,i,shape end end end end end end function Drawing.select_point_at_mouse() for _,drawing in ipairs(Lines) do if drawing.mode == 'drawing' then local x, y = love.mouse.getX(), love.mouse.getY() if Drawing.in_drawing(drawing, x,y) then local mx,my = Drawing.coord(x-16), Drawing.coord(y-drawing.y) for i,point in ipairs(drawing.points) do assert(point) if Drawing.near(point, mx,my) then return drawing,i,point end end end end end end function Drawing.select_drawing_at_mouse() for _,drawing in ipairs(Lines) do if drawing.mode == 'drawing' then local x, y = love.mouse.getX(), love.mouse.getY() if Drawing.in_drawing(drawing, x,y) then return drawing end end end end function Drawing.contains_point(shape, p) if shape.mode == 'freehand' then -- not supported elseif shape.mode == 'line' or shape.mode == 'manhattan' then return shape.p1 == p or shape.p2 == p elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then return table.find(shape.vertices, p) elseif shape.mode == 'circle' then return shape.center == p elseif shape.mode == 'arc' then return shape.center == p -- ugh, how to support angles elseif shape.mode == 'deleted' then -- already done else print(shape.mode) assert(false) end end function Drawing.convert_line(drawing, shape) -- Perhaps we should do a more sophisticated "simple linear regression" -- here: -- https://en.wikipedia.org/wiki/Linear_regression#Simple_and_multiple_linear_regression -- But this works well enough for close-to-linear strokes. assert(shape.mode == 'freehand') shape.mode = 'line' shape.p1 = insert_point(drawing.points, shape.points[1].x, shape.points[1].y) local n = #shape.points shape.p2 = insert_point(drawing.points, shape.points[n].x, shape.points[n].y) end -- turn a line either horizontal or vertical function Drawing.convert_horvert(drawing, shape) if shape.mode == 'freehand' then Drawing.convert_line(shape) end assert(shape.mode == 'line') local p1 = drawing.points[shape.p1] local p2 = drawing.points[shape.p2] if math.abs(p1.x-p2.x) > math.abs(p1.y-p2.y) then p2.y = p1.y else p2.x = p1.x end end function Drawing.smoothen(shape) assert(shape.mode == 'freehand') for _=1,7 do for i=2,#shape.points-1 do local a = shape.points[i-1] local b = shape.points[i] local c = shape.points[i+1] b.x = (a.x + b.x + c.x)/3 b.y = (a.y + b.y + c.y)/3 end end end function Drawing.insert_point(points, x,y) for i,point in ipairs(points) do if Drawing.near(point, x,y) then return i end end table.insert(points, {x=x, y=y}) return #points end function Drawing.near(point, x,y) local px,py = Drawing.pixels(x),Drawing.pixels(y) local cx,cy = Drawing.pixels(point.x), Drawing.pixels(point.y) return (cx-px)*(cx-px) + (cy-py)*(cy-py) < 16 end function Drawing.pixels(n) -- parts to pixels return math.floor(n*Line_width/256) end function Drawing.coord(n) -- pixels to parts return math.floor(n*256/Line_width) end function table.find(h, x) for k,v in pairs(h) do if v == x then return k end end end return Drawing