Skip to content

Commit 9414bf9

Browse files
committed
Tweak treePyramid renderer (add 'rule' annotation)
1 parent 0561670 commit 9414bf9

File tree

3 files changed

+141
-37
lines changed

3 files changed

+141
-37
lines changed

src/renderers/treePyramid.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Svg } from "@svgdotjs/svg.js";
22
import { IPoint } from "../grids/_base";
3-
import { AnnotationTree, APRenderRep, PiecesTree, TreeNode } from "../schemas/schema";
3+
import { AnnotationTree, APRenderRep, BoardBasic, PiecesTree, TreeNode } from "../schemas/schema";
44
import { x2uid } from "../common/glyph2uid";
55
import { IRendererOptionsIn, RendererBase} from "./_base";
66
import { projectPoint, usePieceAt } from "../common/plotting";
@@ -12,6 +12,7 @@ import { projectPoint, usePieceAt } from "../common/plotting";
1212
export class TreePyramidRenderer extends RendererBase {
1313

1414
public static readonly rendererName: string = "tree-pyramid";
15+
public padding = this.cellsize * 0.1;
1516

1617
public render(json: APRenderRep, draw: Svg, options: IRendererOptionsIn): void {
1718
this.jsonPrechecks(json);
@@ -23,7 +24,9 @@ export class TreePyramidRenderer extends RendererBase {
2324

2425
// Delegate to style-specific renderer
2526
if (this.json.board !== null) {
26-
throw new Error("For this renderer, the board must be null.");
27+
if (!("style" in this.json.board) || (this.json.board as BoardBasic).style !== "other") {
28+
throw new Error("For this renderer, the board must be null or set to the `other` style.");
29+
}
2730
}
2831

2932
// Load all the pieces in the legend (have to do this first so the glyphs are available for marking the board)
@@ -56,9 +59,11 @@ export class TreePyramidRenderer extends RendererBase {
5659
const bgcolour = this.options.colourContext.background;
5760
const bgopacity = 1;
5861
const borderBuffer = 5;
59-
const padding = 5;
60-
const width = (pieces[0].length * this.cellsize) + ((pieces[0].length - 1) * padding) + (borderBuffer * 2);
61-
const height = (pieces.length * this.cellsize) + ((pieces.length - 1) * padding) + (borderBuffer * 2);
62+
if (this.json.board !== null && this.json.board !== undefined && ("tileSpacing" in this.json.board) && this.json.board.tileSpacing !== undefined) {
63+
this.padding = this.json.board.tileSpacing * this.cellsize;
64+
}
65+
const width = (pieces[0].length * this.cellsize) + ((pieces[0].length - 1) * this.padding) + (borderBuffer * 2);
66+
const height = (pieces.length * this.cellsize) + ((pieces.length - 1) * this.padding) + (borderBuffer * 2);
6267
const field = this.rootSvg.nested().id("board").viewbox(ox - borderBuffer, oy - borderBuffer, width + (borderBuffer*2), height + (borderBuffer*2)).move(ox, oy);
6368
field.rect(width, height).id("aprender-backfill").move(ox, oy).fill({color: bgcolour, opacity: bgopacity}).back();
6469
// background isn't clickable
@@ -83,7 +88,7 @@ export class TreePyramidRenderer extends RendererBase {
8388
// handle root nodes first
8489
if (node.parents === null) {
8590
const col = node2xy.size;
86-
x = xStart + (col * (this.cellsize + padding));
91+
x = xStart + (col * (this.cellsize + this.padding));
8792
y = yStart;
8893
}
8994
// then children
@@ -93,7 +98,7 @@ export class TreePyramidRenderer extends RendererBase {
9398
throw new Error(`Could not find x,y coordinates for a parent of node ${node.id}`);
9499
}
95100
x = parents.reduce((acc, curr) => acc + curr!.x, 0) / parents.length;
96-
y = parents[0]!.y + this.cellsize + padding;
101+
y = parents[0]!.y + this.cellsize + this.padding;
97102
}
98103
node2xy.set(node.id, {x, y});
99104

@@ -117,7 +122,7 @@ export class TreePyramidRenderer extends RendererBase {
117122

118123
// annotations
119124
if (this.options.showAnnotations) {
120-
this.annotateField(field, node2xy);
125+
this.annotateField(field, node2xy, pieces);
121126
}
122127

123128
// // if there's a board backfill, it needs to be done before rotation
@@ -160,7 +165,7 @@ export class TreePyramidRenderer extends RendererBase {
160165
// }
161166
}
162167

163-
protected annotateField(field: Svg, node2xy: Map<string, IPoint>) {
168+
protected annotateField(field: Svg, node2xy: Map<string, IPoint>, pieces: TreeNode[][]) {
164169
if (this.json === undefined) {
165170
throw new Error("Object in an invalid state!");
166171
}
@@ -171,12 +176,13 @@ export class TreePyramidRenderer extends RendererBase {
171176
for (const note of this.json.annotations as AnnotationTree[]) {
172177
if ( (! ("type" in note)) || (note.type === undefined) ) {
173178
throw new Error("Invalid annotation format found.");
174-
} const cloned = {...note};
179+
}
180+
const cloned = {...note};
175181
if ("targets" in cloned) {
176182
// @ts-expect-error (only used to generate UUID)
177183
delete cloned.id;
178184
}
179-
if ( (note.type !== undefined) && (note.type === "enter" || note.type === "exit") ) {
185+
if (note.type === "enter" || note.type === "exit") {
180186
let colour = this.options.colourContext.annotations;
181187
if ( ("colour" in note) && (note.colour !== undefined) ) {
182188
colour = this.resolveColour(note.colour) as string;
@@ -255,6 +261,38 @@ export class TreePyramidRenderer extends RendererBase {
255261
.attr({ 'pointer-events': 'none' });
256262
}
257263
}
264+
} else if (note.type === "rule") {
265+
let colour = this.options.colourContext.annotations;
266+
if ( ("colour" in note) && (note.colour !== undefined) ) {
267+
colour = this.resolveColour(note.colour) as string;
268+
}
269+
let strokeWeight = this.cellsize * 0.05;
270+
if (this.json.board !== null && this.json.board !== undefined && "strokeWeight" in this.json.board && this.json.board.strokeWeight !== undefined) {
271+
strokeWeight = this.json.board.strokeWeight;
272+
}
273+
let dasharray: string|undefined;
274+
if (note.dashed !== undefined && note.dashed !== null) {
275+
dasharray = (note.dashed).join(" ");
276+
}
277+
let opacity = 1;
278+
if ( ("opacity" in note) && note.opacity !== undefined) {
279+
opacity = note.opacity;
280+
}
281+
let row = 0;
282+
if (("row" in note) && note.row !== undefined) {
283+
row = note.row;
284+
}
285+
const lroot = pieces[0][0];
286+
const lx = node2xy.get(lroot.id)!.x - (this.cellsize / 2) - (this.padding / 2);
287+
const rroot = pieces[0][pieces[0].length - 1];
288+
const rx = node2xy.get(rroot.id)!.x + (this.cellsize / 2) + (this.padding / 2);
289+
const node = pieces[row][0];
290+
const y = node2xy.get(node.id)!.y + (this.cellsize / 2) + (this.padding / 2);
291+
notes.line(lx, y, rx, y)
292+
.addClass(`aprender-annotation-${x2uid(cloned)}`)
293+
.fill("none")
294+
.stroke({color: colour, width: strokeWeight, linecap: "round", linejoin: "round", opacity, dasharray: dasharray !== undefined ? dasharray : undefined})
295+
.attr({ 'pointer-events': 'none' });
258296
}
259297
}
260298
}

src/schemas/schema.d.ts

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export type IsometricPieces =
3333
| "lintelNS"
3434
| "lintelEW"
3535
| "spaceCube";
36+
/**
37+
* Describes the board to be drawn. The `other` style is used for special renderers like `tree-*` where we want optional access to some properties, but otherwise no board is being drawn.
38+
*/
3639
export type BoardStyles =
3740
| "squares"
3841
| "squares-checkered"
@@ -68,7 +71,8 @@ export type BoardStyles =
6871
| "pyramid-hex"
6972
| "heightmap-squares"
7073
| "dvgc"
71-
| "dvgc-checkered";
74+
| "dvgc-checkered"
75+
| "other";
7276
/**
7377
* The patterns known by the renderer
7478
*/
@@ -159,30 +163,48 @@ export type AnnotationFreespace =
159163
/**
160164
* Annotations specifically for the `tree-*` renderers.
161165
*/
162-
export type AnnotationTree = {
163-
type: "enter" | "exit";
164-
/**
165-
* The ids of the nodes to highlight.
166-
*
167-
* @minItems 1
168-
*/
169-
nodes: [string, ...string[]];
170-
/**
171-
* Only meaningful for the `enter` and `exit` notations. Determines the shape of the dotted line.
172-
*/
173-
shape?: "square" | "circle" | "hexf" | "hexp";
174-
style?: "solid" | "dashed";
175-
/**
176-
* The width of the line, expressed as a percentage of cell size.
177-
*/
178-
strokeWidth?: number;
179-
opacity?: number;
180-
colour?: Colourstrings | Colourfuncs | PositiveInteger;
181-
/**
182-
* A valid `dasharray` appropriate for the game's display.
183-
*/
184-
dashed?: number[];
185-
};
166+
export type AnnotationTree =
167+
| {
168+
type: "enter" | "exit";
169+
/**
170+
* The ids of the nodes to highlight.
171+
*
172+
* @minItems 1
173+
*/
174+
nodes: [string, ...string[]];
175+
/**
176+
* Only meaningful for the `enter` and `exit` notations. Determines the shape of the dotted line.
177+
*/
178+
shape?: "square" | "circle" | "hexf" | "hexp";
179+
style?: "solid" | "dashed";
180+
/**
181+
* The width of the line, expressed as a percentage of cell size.
182+
*/
183+
strokeWidth?: number;
184+
opacity?: number;
185+
colour?: Colourstrings | Colourfuncs | PositiveInteger;
186+
/**
187+
* A valid `dasharray` appropriate for the game's display.
188+
*/
189+
dashed?: number[];
190+
}
191+
| {
192+
type: "rule";
193+
/**
194+
* The zero-based row after which to place the rule (`0` is the first row). There must be at least one node on this row to draw the rule.
195+
*/
196+
row?: number;
197+
/**
198+
* The width of the line, expressed as a percentage of cell size.
199+
*/
200+
strokeWidth?: number;
201+
opacity?: number;
202+
colour?: Colourstrings | Colourfuncs | PositiveInteger;
203+
/**
204+
* A valid `dasharray` appropriate for the game's display.
205+
*/
206+
dashed?: number[];
207+
};
186208

187209
/**
188210
* Games on the Abstract Play service must produce representations of the play area based on this schema. The front-end renderer will then translate that into various forms. Detailed documentation is difficult within a JSON document (e.g., no multi-line strings allowed), so see the website for standalone documentation.

src/schemas/schema.json

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"additionalProperties": false
138138
},
139139
"boardStyles": {
140+
"description": "Describes the board to be drawn. The `other` style is used for special renderers like `tree-*` where we want optional access to some properties, but otherwise no board is being drawn.",
140141
"enum": [
141142
"squares",
142143
"squares-checkered",
@@ -172,7 +173,8 @@
172173
"pyramid-hex",
173174
"heightmap-squares",
174175
"dvgc",
175-
"dvgc-checkered"
176+
"dvgc-checkered",
177+
"other"
176178
]
177179
},
178180
"gradientStop": {
@@ -693,6 +695,48 @@
693695
},
694696
"required": ["type", "nodes"],
695697
"additionalProperties": false
698+
},
699+
{
700+
"description": "For drawing enter/exit notations",
701+
"properties": {
702+
"type": {
703+
"enum": ["rule"]
704+
},
705+
"row": {
706+
"description": "The zero-based row after which to place the rule (`0` is the first row). There must be at least one node on this row to draw the rule.",
707+
"type": "integer",
708+
"default": 0
709+
},
710+
"strokeWidth": {
711+
"description": "The width of the line, expressed as a percentage of cell size.",
712+
"type": "number",
713+
"minimum": 0,
714+
"default": 0.05
715+
},
716+
"opacity": {
717+
"type": "number",
718+
"minimum": 0,
719+
"maximum": 1,
720+
"default": 1
721+
},
722+
"colour": {
723+
"anyOf": [
724+
{"$ref": "#/$defs/colourstrings"},
725+
{"$ref": "#/$defs/colourfuncs"},
726+
{"$ref": "#/$defs/positiveInteger"}
727+
],
728+
"default": "#000"
729+
},
730+
"dashed": {
731+
"description": "A valid `dasharray` appropriate for the game's display.",
732+
"type": "array",
733+
"items": {
734+
"type": "number"
735+
}
736+
}
737+
},
738+
"required": ["type"],
739+
"additionalProperties": false
696740
}
697741
]
698742
},

0 commit comments

Comments
 (0)