diff --git a/examples/test.gs b/examples/test.gs index 7c0d49b..49f1677 100644 --- a/examples/test.gs +++ b/examples/test.gs @@ -1,3 +1,10 @@ -point(3 | 4) +point(200, 300) -> A +point(400, 300) -> B +[point(300, 128) -> C] -point(6 | 7) \ No newline at end of file +line(A, B) -> AB +line(B, C) +line(C, A) + +circle(A, len(AB)) +circle(B, len(AB)) \ No newline at end of file diff --git a/examples/test.gs.disable b/examples/test.gs.disable new file mode 100644 index 0000000..8b2a065 --- /dev/null +++ b/examples/test.gs.disable @@ -0,0 +1,9 @@ +point(-1, 0) -> A +point(1, 0) -> B +line(A, B) -> AB +len(AB) -> length +circle(A, len(AB)) -> circleA +circle(B, len(AB)) -> circleB +intersection(circleA, circleB, 0) -> C +line(A, C) +line(B, C) diff --git a/src/doc/syntax.md b/src/doc/syntax.md index 14005e0..173f3ac 100644 --- a/src/doc/syntax.md +++ b/src/doc/syntax.md @@ -1,16 +1,31 @@ # Syntax ``` -point(3 | 4) -> A -point(6 | 7) -> B +[point(3, 4) -> A] +point(6, 7) -> B -line(A, B) -> AB -line(0 | 0, 100 | 100) +line[color=red, weight=4](A, B) -> AB +line(point(0, 0), point(100, 100)) circle(A, len(AB)) ``` -## Primitives -* `Point point(x, y)` is a 2D point. It returns an element of type `Point` -* `Line line(Point from, Point to)` is a straight line. It returns an element of type `Line`. -* `Circle circle(Point center, radius)` draws a circle at `center` and `radius` \ No newline at end of file +## Behaviour +Every line is one instruction. It is possible to assign instructions names to re-use them later. +These variables are immutable. Objects do not exist in this script, in fact, variables are more similar to C-style macros than actual variables. + +Lines in brackets `[]` are "hidden". They are parsed, but will not be rendered. + +It is possible to add an optional set of parameters in front of each parameter list, in order to specify the appearance of the element. + +## Primitives vs Functions +Primitives (e.g. `point`, `line`) and Functions (e.g. `len`, `intersection`) are syntactically indistinguishable. +Both can be used as parameters or instructions and can be assigned to variables. The only difference is that Primitives generate a visual output (unless they are surrounded by square brackets) + +## Grammar +``` +instruction ::= identifier({parameter, }) [-> identifer] +parameter ::= instruction | identifier | number +identifier ::= (A-Za-z) +number ::= (0-9)[.(0-9)] +``` diff --git a/src/doc/todo.md b/src/doc/todo.md new file mode 100644 index 0000000..e11c816 --- /dev/null +++ b/src/doc/todo.md @@ -0,0 +1,9 @@ +# TODO (Parser) +* Type checking +* Ignore case in instruction names +* Implement remaining functions +* Abort parsing on error + +# TODO (Renderer) +* Implement shape classes +* Render shape classes \ No newline at end of file diff --git a/src/geometry.ts b/src/geometry.ts index 58abec9..584b633 100644 --- a/src/geometry.ts +++ b/src/geometry.ts @@ -1,10 +1,11 @@ -import { Vector2D } from "./vector.js" -import * as shape from "./shapes.js" +/// +/// +/// function loadScript(filepath: string): string { - var result = null; - var xmlhttp = new XMLHttpRequest(); + let result = null; + let xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", filepath, false); xmlhttp.send(); if (xmlhttp.status==200) { @@ -16,6 +17,7 @@ function loadScript(filepath: string): string class Geometry extends HTMLElement { + private shapes: Shape[]; private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private sourceFile: string; @@ -23,7 +25,7 @@ class Geometry extends HTMLElement constructor() { super(); - + console.log("constructor") if(!this.hasAttribute("src")) { return; @@ -32,37 +34,16 @@ class Geometry extends HTMLElement let sourceFile = this.getAttribute("src"); let content = loadScript(sourceFile); - let lines = content.split("\n"); - for(let line of lines) + let parser = new Parser(content); + if(!parser.good()) { - if(line === "\r") - { - console.log("empty"); - continue; - } - - let instruction = line.split("(")[0]; - - switch(instruction) - { - case instruction: - { - let coords = line.split("(")[1].split("|"); - console.log(coords); - break; - } - - default: - { - console.log("something else"); - break; - } - } + console.error("Failed to create parser for script " + sourceFile); + return; } this.attachShadow({mode: "open"}); let canvas = document.createElement("canvas"); - canvas.width = 500; + canvas.width = 700; canvas.height = 500; let context = canvas.getContext("2d"); @@ -71,13 +52,38 @@ class Geometry extends HTMLElement this.shadowRoot.append(this.canvas); + + this.shapes = [] + for(let instruction of parser.instructions) + { + let value = instruction.eval(); + switch(instruction.getType()) + { + case InstructionType.Line: + { + console.log("New line " + value) + this.shapes.push(new Line(this.context, new Vector2D(value[0].x, value[0].y), new Vector2D(value[1].x, value[1].y))); + break; + } + + case InstructionType.Circle: + { + console.log("New circle " + value) + this.shapes.push(new Circle(this.context, new Vector2D(value[0].x, value[0].y), value[1])); + break; + } + } + } + this.redraw(); } private redraw() { - shape.line(this.context, new Vector2D(), new Vector2D(300, 300)); - shape.circle(this.context, new Vector2D(150, 150), 100); + for (let shape of this.shapes) + { + shape.draw() + } } } diff --git a/src/gfx/polygon.ts b/src/gfx/polygon.ts new file mode 100644 index 0000000..5ce58d4 --- /dev/null +++ b/src/gfx/polygon.ts @@ -0,0 +1,30 @@ +/// + +class Polygon extends Shape +{ + private points: Vector2D[] + + + constructor(ctx, points) { + super(ctx); + if (points.length <3) + { + console.error("cant draw polygon, need min 3 points") + } + this.points = points + } + + public draw() + { + let last_element = this.points[this.points.length-1] + this.ctx.beginPath(); + this.ctx.moveTo(last_element.x, last_element.y); + for (let point of this.points) + { + this.ctx.lineTo(point.x, point.y); + } + this.ctx.lineWidth = this.style.strokeWidth; + this.ctx.strokeStyle = this.style.strokeColor; + this.ctx.stroke(); + } +} \ No newline at end of file diff --git a/src/gfx/shapes.ts b/src/gfx/shapes.ts new file mode 100644 index 0000000..4131d79 --- /dev/null +++ b/src/gfx/shapes.ts @@ -0,0 +1,64 @@ +/// +/// + +abstract class Shape +{ + protected ctx: CanvasRenderingContext2D + protected style: ShapeStyle + + constructor(ctx) { + this.ctx = ctx + this.style = new ShapeStyle() + } + + abstract draw() +} + +class Line extends Shape +{ + private from: Vector2D + private to: Vector2D + + constructor(ctx,from, to) { + super(ctx) + this.from = from + this.to = to + } + + public draw() + { + this.ctx.beginPath(); + this.ctx.moveTo(this.from.x, this.from.y); + this.ctx.lineTo(this.to.x, this.to.y); + + this.ctx.lineWidth = this.style.strokeWidth; + this.ctx.strokeStyle = this.style.strokeColor; + this.ctx.stroke(); + } +} + +class Circle extends Shape +{ + + private center: Vector2D + private radius: number + + constructor(ctx,center, radius) { + super(ctx) + this.center = center + this.radius = radius + } + + public draw() + { + this.ctx.beginPath(); + this.ctx.arc(this.center.x, this.center.y, this.radius, 0, 2 * Math.PI, false); + + this.ctx.fillStyle = this.style.fillColor; + this.ctx.fill(); + + this.ctx.lineWidth = this.style.strokeWidth; + this.ctx.strokeStyle = this.style.strokeColor; + this.ctx.stroke(); + } +} \ No newline at end of file diff --git a/src/parser/instruction.ts b/src/parser/instruction.ts new file mode 100644 index 0000000..87848b9 --- /dev/null +++ b/src/parser/instruction.ts @@ -0,0 +1,88 @@ +/// + +enum InstructionType +{ + Point, + Line, + Circle, + Length +} + +abstract class Instruction +{ + private type: InstructionType; + public params :Parameter[]; + private argc: number; + + constructor(type: InstructionType, argc: number) + { + this.type = type; + this.argc = argc; + this.params = []; + } + + abstract eval(); + public getParameterCount(): number { return this.argc; } + public getType(): InstructionType { return this.type; } +} + +class PointInstruction extends Instruction +{ + constructor() + { + super(InstructionType.Point, 2); + } + + eval() + { + return new Vector2D(this.params[0].eval(), this.params[1].eval()); + } +} + +class LineInstruction extends Instruction +{ + constructor() + { + super(InstructionType.Line, 2); + } + + eval() + { + return [ + this.params[0].eval(), + this.params[1].eval() + ]; + } +} + +class CircleInstruction extends Instruction +{ + constructor() + { + super(InstructionType.Circle, 2); + } + + eval() + { + return [ + this.params[0].eval(), + this.params[1].eval() + ]; + } +} + +class LengthInstruction extends Instruction +{ + constructor() + { + super(InstructionType.Line, 1); + } + + eval() + { + let line = this.params[0].eval(); + let dx = line[1].x - line[0].x; + let dy = line[1].y - line[0].y; + return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); + } +} \ No newline at end of file diff --git a/src/parser/instructionFactory.ts b/src/parser/instructionFactory.ts new file mode 100644 index 0000000..e1e9d5a --- /dev/null +++ b/src/parser/instructionFactory.ts @@ -0,0 +1,19 @@ +/// + +abstract class InstructionFactory +{ + private static symbolDict: { [name: string] : Function } = { + "point": (): PointInstruction => { return new PointInstruction(); }, + "line": (): LineInstruction => { return new LineInstruction(); }, + "circle": (): CircleInstruction => { return new CircleInstruction(); }, + "len": (): LengthInstruction => { return new LengthInstruction(); } + } + + public static createInstruction(name: string): Instruction + { + if(!(name in InstructionFactory.symbolDict)) + return null; + + return InstructionFactory.symbolDict[name](); + } +} \ No newline at end of file diff --git a/src/parser/parameter.ts b/src/parser/parameter.ts new file mode 100644 index 0000000..efce571 --- /dev/null +++ b/src/parser/parameter.ts @@ -0,0 +1,52 @@ +enum ParameterType +{ + Instruction, + Identifier, + Number +} + +abstract class Parameter +{ + public type: ParameterType; + + constructor(type: ParameterType) + { + this.type = type; + } + + abstract eval(); +} + +class NumberParameter extends Parameter +{ + public val: number; + + constructor(val: number) + { + super(ParameterType.Number); + + this.val = val; + } + + eval() + { + return this.val; + } +} + +class InstructionParameter extends Parameter +{ + public instr: Instruction; + + constructor(instr: Instruction) + { + super(ParameterType.Identifier); + + this.instr = instr; + } + + eval() + { + return this.instr.eval(); + } +} \ No newline at end of file diff --git a/src/parser/parser.ts b/src/parser/parser.ts new file mode 100644 index 0000000..3dd1fb5 --- /dev/null +++ b/src/parser/parser.ts @@ -0,0 +1,166 @@ +/// +/// + +/** + * @brief Turns a .gs script into a list of instructions to be passed to the renderer + */ +class Parser +{ + public instructions: Instruction[]; + private macros: { [id: string] : InstructionParameter}; + private success: boolean; + + /** + * Parses each line of the script and turns them into instructions or adds them to the macro list + * + * @param source The source code of the script + */ + constructor(source: string) + { + this.instructions = []; + this.macros = {}; + this.success = false; + + let lines = source.split(/\r?\n/); + let currentLine = 1; + for(let line of lines) + { + let instr; + try + { + instr = this.parseInstruction(line); + } + catch(e) + { + console.error("Error in line " + currentLine); + console.error(e); + + return; + } + + // If the instruction is null it means that the instruction was a function and not a primitive + if(instr !== null) + if(!instr[0]) + this.instructions.push(instr[1]); + + currentLine++; + } + + this.success = true; + } + + public good() { return this.success; } + + private parseInstruction(instruction: string): [boolean, Instruction] + { + // If the instruction is an empty line, do nothing for now + if(instruction === "") + return null; + + // Handle [] syntax. Lines in [] will be processed but not rendered + let hidden = false; + if(instruction[0] === "[" && instruction[instruction.length - 1] === "]") + { + hidden = true; + instruction = instruction.substring(1, instruction.length - 1); + } + + instruction = instruction.split(" ").join(""); // Remove spaces + + // match the pattern "text(text)" + let matches = instruction.match(/[A-Za-z]*\(.*\)/); + if(matches === null) // no match found + throw new Error("Line does not contain a valid instruction."); + + if(matches.length > 1) // more than one match + throw new Error("Line may only contain one instruction"); + + let instr = matches[0]; // get the instruction + let paranthesisPos = instr.search(/\(/); // Find the position of the first opening paranthesis + + + let symbol = instr.substr(0, paranthesisPos); // get function name + let paramlist = instr.substring(paranthesisPos + 1, instr.length - 1); // get parameter list + + // Construct the parameter list + let match; + let params = []; + while((match = paramlist.search(/,(?![^\(]*\))/)) !== -1) + { + params.push(paramlist.substring(0, match)); + paramlist = paramlist.substring(match + 1, paramlist.length); + } + params.push(paramlist); + + // Create appropriate instruction + let newInstruction = InstructionFactory.createInstruction(symbol); + if(newInstruction === null) + throw new Error("Unknown instruction: \"" + symbol + "\""); + + // Check that the number of arguments passed to the function is correct + let expectedArgs = newInstruction.getParameterCount(); + if(expectedArgs !== params.length) + throw new Error("Wrong number of arguments for instruction \"" + symbol + "\". Expected " + expectedArgs + " arguments but received " + params.length + " instead."); + + // Parse the individual parameters + for(let param of params) + { + if(!this.parseParameter(newInstruction, param)) + throw new Error("Error during parameter parsing: \"" + param + "\" failed to be parsed."); + } + + // In case there is an assignment, add the instruction to the macro list + let assignment = instruction.search(/->/); + if(assignment !== -1) + { + let variableName = instruction.substring(assignment + 2, instruction.length); + if(variableName in this.macros) + throw new Error("Redefinition of variable \"" + variableName + "\" is not allowed."); + + this.macros[variableName] = new InstructionParameter(newInstruction); + } + + return [hidden, newInstruction]; + } + + private parseParameter(instr: Instruction, parameter: string): boolean + { + // Parameter is a number + let match = parameter.match(/-?\d*\.?\d*$/); + if(match !== null && match[0] === parameter && match.index === 0) + { + let val = parseFloat(parameter); + let paramObj = new NumberParameter(val); + + instr.params.push(paramObj); + return true; + } + + // Parameter is an identifier (macro) + match = parameter.match(/[A-Za-z]*/) + if(match !== null && match[0] === parameter && match.index === 0) + { + let paramObj = this.macros[parameter]; + if(paramObj === undefined) + { + console.error("Variable \"" + parameter + "\" is not defined"); + return false; + } + + instr.params.push(paramObj); + return true; + } + + // Parameter is another instruction + match = parameter.match(/[A-Za-z]*\(.*\)/) + if(match !== null && match[0] === parameter && match.index === 0) + { + let paramObj = new InstructionParameter(this.parseInstruction(parameter)[1]); + + instr.params.push(paramObj); + return true; + } + + return false; + } +} diff --git a/src/shapeStyle.ts b/src/shapeStyle.ts index 174eaab..3f2ef5f 100644 --- a/src/shapeStyle.ts +++ b/src/shapeStyle.ts @@ -1,5 +1,4 @@ - -export class ShapeStyle +class ShapeStyle { public strokeWidth: number; public strokeColor: string; diff --git a/src/shapes.ts b/src/shapes.ts deleted file mode 100644 index 29454ff..0000000 --- a/src/shapes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Vector2D } from "./vector.js" -import { ShapeStyle } from "./shapeStyle.js"; - -export function line(ctx: CanvasRenderingContext2D, from: Vector2D , to: Vector2D, style: ShapeStyle = new ShapeStyle()) -{ - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - - ctx.lineWidth = style.strokeWidth; - ctx.strokeStyle = style.strokeColor; - ctx.stroke(); -} - -export function circle(ctx: CanvasRenderingContext2D, center: Vector2D, radius: number, style: ShapeStyle = new ShapeStyle()) -{ - ctx.beginPath(); - ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI, false); - - ctx.fillStyle = style.fillColor; - ctx.fill(); - - ctx.lineWidth = style.strokeWidth; - ctx.strokeStyle = style.strokeColor; - ctx.stroke(); -} \ No newline at end of file diff --git a/src/vector.ts b/src/vector.ts index 699eb8e..64a8fdb 100644 --- a/src/vector.ts +++ b/src/vector.ts @@ -1,4 +1,4 @@ -export class Vector2D +class Vector2D { public x: number; public y: number; diff --git a/tsconfig.json b/tsconfig.json index 90de336..2adc248 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,7 @@ { "compilerOptions": { "target": "es6", - // "outFile": "./out/lauchpioos.js", - "outDir": "./out", + "outFile": "./out/geometry.js", "sourceRoot": "./src", "rootDir": "./src", "sourceMap": true