forked from sent/waves
1084 lines
41 KiB
TypeScript
1084 lines
41 KiB
TypeScript
declare module WorldSeedr {
|
|
export interface IWorldSeedrSettings {
|
|
/**
|
|
* A very large listing of possibility schemas, keyed by title.
|
|
*/
|
|
possibilities: IPossibilityContainer;
|
|
|
|
/**
|
|
* Function used to generate a random number, if not Math.random.
|
|
*/
|
|
random?: () => number;
|
|
|
|
/**
|
|
* Function called in this.generateFull to place a child.
|
|
*/
|
|
onPlacement?: (commands: ICommand[]) => void;
|
|
|
|
}
|
|
|
|
export interface IPossibilityContainer {
|
|
[i: string]: IPossibility;
|
|
}
|
|
|
|
export interface IPossibility {
|
|
"width": number;
|
|
"height": number;
|
|
"contents": IPossibilityContents;
|
|
}
|
|
|
|
export interface IPossibilityContents {
|
|
"direction": string;
|
|
"mode": string;
|
|
"snap": string;
|
|
"children": IPossibilityChild[];
|
|
"spacing"?: number | number[]| IPossibilitySpacing;
|
|
"limit"?: number;
|
|
"argumentMap"?: IArgumentMap
|
|
}
|
|
|
|
export interface IPercentageOption {
|
|
"percent": number;
|
|
}
|
|
|
|
export interface IPossibilityChild extends IPercentageOption {
|
|
"title": string;
|
|
"type": string;
|
|
"arguments"?: IArgumentPossibilities[]| any;
|
|
"source"?: string;
|
|
"sizing"?: {
|
|
"width"?: number;
|
|
"height"?: number;
|
|
};
|
|
"stretch"?: {
|
|
"width"?: number;
|
|
"height"?: number;
|
|
};
|
|
}
|
|
|
|
export interface IPossibilitySpacing {
|
|
"min": number;
|
|
"max": number;
|
|
"units"?: number;
|
|
}
|
|
|
|
export interface IPossibilitySpacingOption extends IPercentageOption {
|
|
"value": IPossibilitySpacing;
|
|
}
|
|
|
|
export interface IArgumentPossibilities {
|
|
[i: number]: IArgumentPossibility
|
|
}
|
|
|
|
export interface IArgumentPossibility extends IPercentageOption {
|
|
values: any[];
|
|
}
|
|
|
|
export interface IArgumentMap {
|
|
[i: string]: string;
|
|
}
|
|
|
|
export interface IDirectionsMap {
|
|
"top": string;
|
|
"right": string;
|
|
"bottom": string;
|
|
"left": string;
|
|
}
|
|
|
|
export interface IPosition {
|
|
"width": number;
|
|
"height": number;
|
|
"top": number;
|
|
"right": number;
|
|
"bottom": number;
|
|
"left": number;
|
|
"type"?: string;
|
|
}
|
|
|
|
export interface ICommand extends IPosition {
|
|
"title": string;
|
|
"arguments"?: IArgumentPossibilities | any;
|
|
}
|
|
|
|
export interface IChoice extends ICommand {
|
|
"contents"?: IChoice;
|
|
"children"?: IChoice[];
|
|
}
|
|
|
|
export interface IWorldSeedr {
|
|
getPossibilities(): IPossibilityContainer;
|
|
setPossibilities(possibilities: IPossibilityContainer): void;
|
|
getOnPlacement(): (commands: ICommand[]) => void;
|
|
setOnPlacement(onPlacement: (commands: ICommand[]) => void): void;
|
|
clearGeneratedCommands(): void;
|
|
runGeneratedCommands(): void;
|
|
generate(name: string, command: IPosition | ICommand): IChoice;
|
|
generateFull(schema: ICommand): void;
|
|
}
|
|
}
|
|
|
|
|
|
module WorldSeedr {
|
|
"use strict";
|
|
|
|
/**
|
|
* A randomization utility to automate random, recursive generation of
|
|
* possibilities based on a preset position and probability schema. Each
|
|
* "possibility" in the schema contains a width, height, and instructions on
|
|
* what type of contents it contains, which are either a preset listing or
|
|
* a randomization of other possibilities of certain probabilities. Additional
|
|
* functionality is provided to stagger layout of children, such as spacing
|
|
* between possibilities.
|
|
*/
|
|
export class WorldSeedr {
|
|
/**
|
|
* A very large listing of possibility schemas, keyed by title.
|
|
*/
|
|
private possibilities: IPossibilityContainer;
|
|
|
|
/**
|
|
* Function used to generate a random number
|
|
*/
|
|
private random: () => number;
|
|
|
|
/**
|
|
* Function called in generateFull to place a command.
|
|
*/
|
|
private onPlacement: (commands: ICommand[]) => void;
|
|
|
|
/**
|
|
* Scratch Array of PreThings to be added to during generation.
|
|
*/
|
|
private generatedCommands: ICommand[];
|
|
|
|
/**
|
|
* A constant listing of direction opposites, like top-bottom.
|
|
*/
|
|
private directionOpposites: IDirectionsMap = {
|
|
"top": "bottom",
|
|
"right": "left",
|
|
"bottom": "top",
|
|
"left": "right"
|
|
};
|
|
|
|
/**
|
|
* A constant listing of what direction the sides of areas correspond to.
|
|
*/
|
|
private directionSizing: IDirectionsMap = {
|
|
"top": "height",
|
|
"right": "width",
|
|
"bottom": "height",
|
|
"left": "width"
|
|
};
|
|
|
|
/**
|
|
* A constant Array of direction names.
|
|
*/
|
|
private directionNames: string[] = ["top", "right", "bottom", "left"];
|
|
|
|
/**
|
|
* A constant Array of the dimension descriptors.
|
|
*/
|
|
private sizingNames: string[] = ["width", "height"];
|
|
|
|
/**
|
|
* @param {IWorldSeedrSettings} settings
|
|
*/
|
|
constructor(settings: IWorldSeedrSettings) {
|
|
if (typeof settings.possibilities === "undefined") {
|
|
throw new Error("No possibilities given to WorldSeedr.");
|
|
}
|
|
|
|
this.possibilities = settings.possibilities;
|
|
this.random = settings.random || Math.random.bind(Math);
|
|
this.onPlacement = settings.onPlacement || console.log.bind(console, "Got:");
|
|
|
|
this.clearGeneratedCommands();
|
|
}
|
|
|
|
|
|
/* Simple gets & sets
|
|
*/
|
|
|
|
/**
|
|
* @return {Object} The listing of possibilities that may be generated.
|
|
*/
|
|
getPossibilities(): IPossibilityContainer {
|
|
return this.possibilities;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} possibilitiesNew A new Object to list possibilities
|
|
* that may be generated.
|
|
*/
|
|
setPossibilities(possibilities: IPossibilityContainer): void {
|
|
this.possibilities = possibilities;
|
|
}
|
|
|
|
/**
|
|
* @return {Function} The Function callback for generated possibilities of
|
|
* type "known" to be called in runGeneratedCommands.
|
|
*/
|
|
getOnPlacement(): (commands: ICommand[]) => void {
|
|
return this.onPlacement;
|
|
}
|
|
|
|
/**
|
|
* @param {Function} onPlacementNew A new Function to be used as the
|
|
* onPlacement callback.
|
|
*/
|
|
setOnPlacement(onPlacement: (commands: ICommand[]) => void): void {
|
|
this.onPlacement = onPlacement;
|
|
}
|
|
|
|
|
|
/* Generated commands
|
|
*/
|
|
|
|
/**
|
|
* Resets the generatedCommands Array so runGeneratedCommands can start.
|
|
*/
|
|
clearGeneratedCommands(): void {
|
|
this.generatedCommands = [];
|
|
}
|
|
|
|
/**
|
|
* Runs the onPlacement callback on the generatedCommands Array.
|
|
*/
|
|
runGeneratedCommands(): void {
|
|
this.onPlacement(this.generatedCommands);
|
|
}
|
|
|
|
|
|
/* Hardcore generation functions
|
|
*/
|
|
|
|
/**
|
|
* Generates a collection of randomly chosen possibilities based on the
|
|
* given schema mapping. These does not recursively parse the output; do
|
|
* do that, use generateFull.
|
|
*
|
|
* @param {String} name The name of the possibility schema to start from.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
generate(name: string, command: IPosition | ICommand): IChoice {
|
|
var schema: IPossibility = this.possibilities[name];
|
|
|
|
if (!schema) {
|
|
throw new Error("No possibility exists under '" + name + "'");
|
|
}
|
|
|
|
if (!schema.contents) {
|
|
throw new Error("Possibility '" + name + "' has no possibile outcomes.");
|
|
}
|
|
|
|
return this.generateChildren(schema, this.objectCopy(command));
|
|
}
|
|
|
|
/**
|
|
* Recursively generates a schema. The schema's title and itself are given
|
|
* to this.generate; all outputs of type "Known" are added to the
|
|
* generatedCommands Array, while everything else is recursed upon.
|
|
*
|
|
* @param {Object} schema A simple Object with basic information on the
|
|
* chosen possibility.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
generateFull(schema: ICommand): void {
|
|
var generated: IChoice = this.generate(schema.title, schema),
|
|
child: IChoice,
|
|
i: number;
|
|
|
|
if (!generated || !generated.children) {
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < generated.children.length; i += 1) {
|
|
child = generated.children[i];
|
|
|
|
switch (child.type) {
|
|
case "Known":
|
|
this.generatedCommands.push(child);
|
|
break;
|
|
case "Random":
|
|
this.generateFull(child);
|
|
break;
|
|
default:
|
|
throw new Error("Unknown child type: " + child.type);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates the children for a given schema, position, and direction. This
|
|
* is the real hardcore function called by this.generate, which calls the
|
|
* differnt subroutines based on whether the contents are in "Certain" or
|
|
* "Random" mode.
|
|
*
|
|
* @param {Object} schema A simple Object with basic information on the
|
|
* chosen possibility.
|
|
* @param {Object} position The bounding box for where the children may
|
|
* be generated.
|
|
* @param {String} [direction] A string direction to check the position
|
|
* by ("top", "right", "bottom", or "left")
|
|
* as a default if contents.direction isn't
|
|
* provided.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
private generateChildren(schema: IPossibility, position: IPosition, direction: string = undefined): IChoice {
|
|
var contents: IPossibilityContents = schema.contents,
|
|
spacing: number | number[]| IPossibilitySpacing = contents.spacing || 0,
|
|
objectMerged: IPosition = this.objectMerge(schema, position),
|
|
children: IChoice[];
|
|
|
|
direction = contents.direction || direction;
|
|
|
|
switch (contents.mode) {
|
|
case "Random":
|
|
children = this.generateChildrenRandom(contents, objectMerged, direction, spacing);
|
|
break;
|
|
case "Certain":
|
|
children = this.generateChildrenCertain(contents, objectMerged, direction, spacing);
|
|
break;
|
|
case "Repeat":
|
|
children = this.generateChildrenRepeat(contents, objectMerged, direction, spacing);
|
|
break;
|
|
case "Multiple":
|
|
children = this.generateChildrenMultiple(contents, objectMerged, direction, spacing);
|
|
break;
|
|
default:
|
|
throw new Error("Unknown contents mode: " + contents.mode);
|
|
}
|
|
|
|
return this.wrapChoicePositionExtremes(children);
|
|
}
|
|
|
|
/**
|
|
* Generates a schema's children that are known to follow a set listing of
|
|
* sub-schemas.
|
|
*
|
|
* @param {Object} contents The known possibilities to choose between.
|
|
* @param {Object} position The bounding box for where the children may
|
|
* be generated.
|
|
* @param {String} direction A string direction to check the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Number} spacing How much space there should be between each
|
|
* child.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
private generateChildrenCertain(
|
|
contents: IPossibilityContents,
|
|
position: IPosition,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing): IChoice[] {
|
|
var scope: WorldSeedr = this;
|
|
|
|
return contents.children.map(function (choice: IPossibilityChild): IChoice {
|
|
if (choice.type === "Final") {
|
|
return scope.parseChoiceFinal(contents, choice, position, direction);
|
|
}
|
|
|
|
var output: IChoice = scope.parseChoice(choice, position, direction);
|
|
|
|
if (output) {
|
|
if (output.type !== "Known") {
|
|
output.contents = scope.generate(output.title, position);
|
|
}
|
|
|
|
scope.shrinkPositionByChild(position, output, direction, spacing);
|
|
}
|
|
|
|
return output;
|
|
}).filter(function (child: IChoice): boolean {
|
|
return child !== undefined;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates a schema's children that are known to follow a set listing of
|
|
* sub-schemas, repeated until there is no space left.
|
|
*
|
|
* @param {Object} contents The known possibilities to choose between.
|
|
* @param {Object} position The bounding box for where the children may
|
|
* be generated.
|
|
* @param {String} direction A string direction to check the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Number} spacing How much space there should be between each
|
|
* child.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
private generateChildrenRepeat(
|
|
contents: IPossibilityContents,
|
|
position: IPosition,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing): IChoice[] {
|
|
var choices: IPossibilityChild[] = contents.children,
|
|
children: IChoice[] = [],
|
|
choice: IPossibilityChild,
|
|
child: IChoice,
|
|
i: number = 0;
|
|
|
|
// Continuously loops through the choices and adds them to the output
|
|
// children, so long as there's still room for them
|
|
while (this.positionIsNotEmpty(position, direction)) {
|
|
choice = choices[i];
|
|
|
|
if (choice.type === "Final") {
|
|
child = this.parseChoiceFinal(contents, choice, position, direction);
|
|
} else {
|
|
child = this.parseChoice(choice, position, direction);
|
|
|
|
if (child) {
|
|
if (child.type !== "Known") {
|
|
child.contents = this.generate(child.title, position);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (child && this.choiceFitsPosition(child, position)) {
|
|
this.shrinkPositionByChild(position, child, direction, spacing);
|
|
children.push(child);
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
i += 1;
|
|
if (i >= choices.length) {
|
|
i = 0;
|
|
}
|
|
}
|
|
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
* Generates a schema's children that are known to be randomly chosen from a
|
|
* list of possibilities until there is no more room.
|
|
*
|
|
* @param {Object} contents The Array of known possibilities, with
|
|
* probability percentages.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} direction A string direction to check the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Number} spacing How much space there should be between each
|
|
* child.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
private generateChildrenRandom(
|
|
contents: IPossibilityContents,
|
|
position: IPosition,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing): IChoice[] {
|
|
var children: IChoice[] = [],
|
|
child: IChoice;
|
|
|
|
// Continuously add random choices to the output children as long as
|
|
// there's room in the position's bounding box
|
|
while (this.positionIsNotEmpty(position, direction)) {
|
|
child = this.generateChild(contents, position, direction);
|
|
if (!child) {
|
|
break;
|
|
}
|
|
|
|
this.shrinkPositionByChild(position, child, direction, spacing);
|
|
children.push(child);
|
|
|
|
if (contents.limit && children.length > contents.limit) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
* Generates a schema's children that are all to be placed within the same
|
|
* position. If a direction is provided, each subsequent one is shifted in
|
|
* that direction by spacing.
|
|
*
|
|
* @param {Object} contents The Array of known possibilities, with
|
|
* probability percentages.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} [direction] A string direction to check the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Number} [spacing] How much space there should be between each
|
|
* child.
|
|
* @return {Object} An Object containing a position within the given
|
|
* position and some number of children.
|
|
*/
|
|
private generateChildrenMultiple(
|
|
contents: IPossibilityContents,
|
|
position: IPosition,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing): IChoice[] {
|
|
var scope: WorldSeedr = this;
|
|
|
|
return contents.children.map(function (choice: IPossibilityChild): IChoice {
|
|
var output: IChoice = scope.parseChoice(choice, scope.objectCopy(position), direction);
|
|
|
|
if (direction) {
|
|
(<WorldSeedr>scope).movePositionBySpacing(position, direction, spacing);
|
|
}
|
|
|
|
return output;
|
|
});
|
|
}
|
|
|
|
|
|
/* Choice parsing
|
|
*/
|
|
|
|
/**
|
|
* Shortcut function to choose a choice from an allowed set of choices, and
|
|
* parse it for positioning and sub-choices.
|
|
*
|
|
* @param {Object} contents An Array of choice Objects, each of which must
|
|
* have a .percentage.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} direction A string direction to check the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @return {Object} An Object containing the bounding box position of a
|
|
* parsed child, with the basic schema (.title) info
|
|
* added as well as any optional .arguments.
|
|
*/
|
|
private generateChild(contents: IPossibilityContents, position: IPosition, direction: string): IChoice {
|
|
var choice: IPossibilityChild = this.chooseAmongPosition(contents.children, position);
|
|
|
|
if (!choice) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.parseChoice(choice, position, direction);
|
|
}
|
|
|
|
/**
|
|
* Creates a parsed version of a choice given the position and direction.
|
|
* This is the function that parses and manipulates the positioning of the
|
|
* new choice.
|
|
*
|
|
* @param {Object} choice The simple definition of the Object chosen from
|
|
* a choices array. It should have at least .title,
|
|
* and optionally .sizing or .arguments.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} direction A string direction to shrink the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @return {Object} An Object containing the bounding box position of a
|
|
* parsed child, with the basic schema (.title) info
|
|
* added as well as any optional .arguments.
|
|
*/
|
|
private parseChoice(choice: IPossibilityChild, position: IPosition, direction: string): IChoice {
|
|
var title: string = choice.title,
|
|
schema: IPossibility = this.possibilities[title],
|
|
output: IChoice = {
|
|
"title": title,
|
|
"type": choice.type,
|
|
"arguments": choice.arguments instanceof Array
|
|
? (<IArgumentPossibility>(this.chooseAmong(choice.arguments))).values
|
|
: choice.arguments,
|
|
"width": undefined,
|
|
"height": undefined,
|
|
"top": undefined,
|
|
"right": undefined,
|
|
"bottom": undefined,
|
|
"left": undefined
|
|
};
|
|
|
|
this.ensureSizingOnChoice(output, choice, schema);
|
|
this.ensureDirectionBoundsOnChoice(output, position);
|
|
|
|
output[direction] = output[this.directionOpposites[direction]] + output[this.directionSizing[direction]];
|
|
|
|
switch (schema.contents.snap) {
|
|
case "top":
|
|
output.bottom = output.top - output.height;
|
|
break;
|
|
case "right":
|
|
output.left = output.right - output.width;
|
|
break;
|
|
case "bottom":
|
|
output.top = output.bottom + output.height;
|
|
break;
|
|
case "left":
|
|
output.right = output.left + output.width;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (choice.stretch) {
|
|
if (!output.arguments) {
|
|
output.arguments = {};
|
|
}
|
|
|
|
if (choice.stretch.width) {
|
|
output.left = position.left;
|
|
output.right = position.right;
|
|
output.width = output.right - output.left;
|
|
output.arguments.width = output.width;
|
|
}
|
|
|
|
if (choice.stretch.height) {
|
|
output.top = position.top;
|
|
output.bottom = position.bottom;
|
|
output.height = output.top - output.bottom;
|
|
output.arguments.height = output.height;
|
|
}
|
|
}
|
|
|
|
this.copySchemaArguments(schema, choice, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* should conform to parent (contents) via cannonsmall.snap=bottom
|
|
*/
|
|
private parseChoiceFinal(parent: IPossibilityContents, choice: IPossibilityChild, position: IPosition, direction: string): IChoice {
|
|
var schema: IPossibility = this.possibilities[choice.source],
|
|
output: IChoice = {
|
|
"type": "Known",
|
|
"title": choice.title,
|
|
"arguments": choice.arguments,
|
|
"width": schema.width,
|
|
"height": schema.height,
|
|
"top": position.top,
|
|
"right": position.right,
|
|
"bottom": position.bottom,
|
|
"left": position.left
|
|
};
|
|
|
|
this.copySchemaArguments(schema, choice, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
|
|
/* Randomization utilities
|
|
*/
|
|
|
|
/**
|
|
* From an Array of potential choice objects, returns one chosen at random.
|
|
*
|
|
* @param {Array} choice An Array of objects with .width and .height.
|
|
* @return {Object}
|
|
*/
|
|
private chooseAmong<t extends IPercentageOption>(choices: t[]): t {
|
|
if (!choices.length) {
|
|
return undefined;
|
|
}
|
|
if (choices.length === 1) {
|
|
return choices[0];
|
|
}
|
|
|
|
var choice: number = this.randomPercentage(),
|
|
sum: number = 0,
|
|
i: number;
|
|
|
|
for (i = 0; i < choices.length; i += 1) {
|
|
sum += choices[i].percent;
|
|
if (sum >= choice) {
|
|
return choices[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* From an Array of potential choice objects, filtered to only include those
|
|
* within a certain size, returns one chosen at random.
|
|
*
|
|
* @param {Array} choice An Array of objects with .width and .height.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @return {Object}
|
|
* @remarks Functions that use this will have to react to nothing being
|
|
* chosen. For example, if only 50 percentage is accumulated
|
|
* among fitting ones but 75 is randomly chosen, something should
|
|
* still be returned.
|
|
*/
|
|
private chooseAmongPosition(choices: IPossibilityChild[], position: IPosition): IPossibilityChild {
|
|
var width: number = position.right - position.left,
|
|
height: number = position.top - position.bottom,
|
|
scope: WorldSeedr = this;
|
|
|
|
return this.chooseAmong(choices.filter(function (choice: IPossibilityChild): boolean {
|
|
return scope.choiceFits(scope.possibilities[choice.title], width, height);
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Checks whether a choice can fit within a width and height.
|
|
*
|
|
* @param {Object} choice An Object that contains .width and .height.
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
* @return {Boolean} Whether the choice fits within the position.
|
|
*/
|
|
private choiceFits(choice: IPossibility | IChoice, width: number, height: number): boolean {
|
|
return choice.width <= width && choice.height <= height;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a choice can fit within a position.
|
|
*
|
|
* @param {Object} choice An Object that contains .width and .height.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @return {Boolean} The boolean equivalent of the choice fits
|
|
* within the position.
|
|
* @remarks When calling multiple times on a position (such as in
|
|
* chooseAmongPosition), it's more efficient to store the width
|
|
* and height separately and just use doesChoiceFit.
|
|
*/
|
|
private choiceFitsPosition(choice: IPossibility | IChoice, position: IPosition): boolean {
|
|
return this.choiceFits(choice, position.right - position.left, position.top - position.bottom);
|
|
}
|
|
|
|
/**
|
|
* @return {Number} A number in [1, 100] at random.
|
|
*/
|
|
private randomPercentage(): number {
|
|
return Math.floor(this.random() * 100) + 1;
|
|
}
|
|
|
|
/**
|
|
* @return {Number} A number in [min, max] at random.
|
|
*/
|
|
private randomBetween(min: number, max: number): number {
|
|
return Math.floor(this.random() * (1 + max - min)) + min;
|
|
}
|
|
|
|
|
|
/* Position manipulation utilities
|
|
*/
|
|
|
|
/**
|
|
* Creates and returns a copy of a position (really just a shallow copy).
|
|
*
|
|
* @param {Object} original
|
|
* @return {Object}
|
|
*/
|
|
private objectCopy(original: any): any {
|
|
var output: any = {},
|
|
i: string;
|
|
|
|
for (i in original) {
|
|
if (original.hasOwnProperty(i)) {
|
|
output[i] = original[i];
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Creates a new position with all required attributes taking from the
|
|
* primary source or secondary source, in that order.
|
|
*
|
|
* @param {Object} primary
|
|
* @param {Object} secondary
|
|
* @return {Object}
|
|
*/
|
|
private objectMerge(primary: any, secondary: any): any {
|
|
var output: IPosition = this.objectCopy(primary),
|
|
i: string;
|
|
|
|
for (i in secondary) {
|
|
if (secondary.hasOwnProperty(i) && !output.hasOwnProperty(i)) {
|
|
output[i] = secondary[i];
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Checks and returns whether a position has open room in a particular
|
|
* direction (horizontally for left/right and vertically for top/bottom).
|
|
*
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} direction A string direction to check the position in:
|
|
* "top", "right", "bottom", or "left".
|
|
*/
|
|
private positionIsNotEmpty(position: IPosition, direction: string): boolean {
|
|
if (direction === "right" || direction === "left") {
|
|
return position.left < position.right;
|
|
} else {
|
|
return position.top > position.bottom;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shrinks a position by the size of a child, in a particular direction.
|
|
*
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {Object} child An Object that contains .left, .right, .top, and
|
|
* .bottom.
|
|
* @param {String} direction A string direction to shrink the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Mixed} [spacing] How much space there should be between each
|
|
* child (by default, 0).
|
|
*/
|
|
private shrinkPositionByChild(
|
|
position: IPosition,
|
|
child: IChoice,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing | IPossibilitySpacingOption[]): void {
|
|
switch (direction) {
|
|
case "top":
|
|
position.bottom = child.top + this.parseSpacing(spacing);
|
|
break;
|
|
case "right":
|
|
position.left = child.right + this.parseSpacing(spacing);
|
|
break;
|
|
case "bottom":
|
|
position.top = child.bottom - this.parseSpacing(spacing);
|
|
break;
|
|
case "left":
|
|
position.right = child.left - this.parseSpacing(spacing);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves a position by its parsed spacing. This is only useful for content
|
|
* of type "Multiple", which are allowed to move themselves via spacing
|
|
* between placements.
|
|
*
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
* @param {String} direction A string direction to shrink the position by:
|
|
* "top", "right", "bottom", or "left".
|
|
* @param {Mixed} [spacing] How much space there should be between each
|
|
* child (by default, 0).
|
|
*/
|
|
private movePositionBySpacing(
|
|
position: IPosition,
|
|
direction: string,
|
|
spacing: number | number[]| IPossibilitySpacing | IPossibilitySpacingOption[] = 0): void {
|
|
var space: number = this.parseSpacing(spacing);
|
|
|
|
switch (direction) {
|
|
case "top":
|
|
position.top += space;
|
|
position.bottom += space;
|
|
break;
|
|
case "right":
|
|
position.left += space;
|
|
position.right += space;
|
|
break;
|
|
case "bottom":
|
|
position.top -= space;
|
|
position.bottom -= space;
|
|
break;
|
|
case "left":
|
|
position.left -= space;
|
|
position.right -= space;
|
|
break;
|
|
default:
|
|
throw new Error("Unknown direction: " + direction);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively parses a spacing parameter to eventually return a Number,
|
|
* which will likely be random.
|
|
*
|
|
* @param {Mixed} spacing This may be a Number (returned directly), an
|
|
* Object[] containing choices for chooseAmong, a
|
|
* Number[] containing minimum and maximum values,
|
|
* or an Object containing "min", "max", and
|
|
* "units" to round to.
|
|
* @return {Number}
|
|
*/
|
|
private parseSpacing(spacing: number | number[]| IPossibilitySpacing | IPossibilitySpacingOption[]): number {
|
|
if (!spacing) {
|
|
return 0;
|
|
}
|
|
|
|
switch (spacing.constructor) {
|
|
case Array:
|
|
// Case: [min, max]
|
|
if ((<number[]>spacing)[0].constructor === Number) {
|
|
return this.parseSpacingObject(this.randomBetween((<number[]>spacing)[0], (<number[]>spacing)[1]));
|
|
}
|
|
// Case: IPossibilitySpacingOption[]
|
|
return this.parseSpacingObject(this.chooseAmong(<IPossibilitySpacingOption[]>spacing).value);
|
|
case Object:
|
|
// Case: IPossibilitySpacing
|
|
return this.parseSpacingObject(<IPossibilitySpacing>spacing);
|
|
default:
|
|
// Case: Number
|
|
return <number>spacing;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to parse a spacing Object. The minimum and maximum ("min" and
|
|
* "max", respectively) are the range, and an optional "units" parameter
|
|
* is what Number it should round to.
|
|
*
|
|
* @param {Object} spacing
|
|
* @return {Number}
|
|
*/
|
|
private parseSpacingObject(spacing: number | IPossibilitySpacing): number {
|
|
if (spacing.constructor === Number) {
|
|
return <number>spacing;
|
|
}
|
|
|
|
var min: number = (<IPossibilitySpacing>spacing).min,
|
|
max: number = (<IPossibilitySpacing>spacing).max,
|
|
units: number = (<IPossibilitySpacing>spacing).units || 1;
|
|
|
|
return this.randomBetween(min / units, max / units) * units;
|
|
}
|
|
|
|
/**
|
|
* Generates the bounding box position Object (think rectangle) for a set of
|
|
* children. The top, right, etc. member variables become the most extreme
|
|
* out of all the possibilities.
|
|
*
|
|
* @param {Object} children An Array of Objects with .top, .right,
|
|
* .bottom, and .left.
|
|
* @return {Object} An Object with .top, .right, .bottom, and .left.
|
|
*/
|
|
private wrapChoicePositionExtremes(children: IChoice[]): IChoice {
|
|
var position: IChoice,
|
|
child: IChoice,
|
|
i: number;
|
|
|
|
if (!children || !children.length) {
|
|
return undefined;
|
|
}
|
|
|
|
child = children[0];
|
|
position = {
|
|
"title": undefined,
|
|
"top": child.top,
|
|
"right": child.right,
|
|
"bottom": child.bottom,
|
|
"left": child.left,
|
|
"width": undefined,
|
|
"height": undefined,
|
|
"children": children
|
|
};
|
|
|
|
if (children.length === 1) {
|
|
return position;
|
|
}
|
|
|
|
for (i = 1; i < children.length; i += 1) {
|
|
child = children[i];
|
|
|
|
if (!Object.keys(child).length) {
|
|
return position;
|
|
}
|
|
|
|
position.top = Math.max(position.top, child.top);
|
|
position.right = Math.max(position.right, child.right);
|
|
position.bottom = Math.min(position.bottom, child.bottom);
|
|
position.left = Math.min(position.left, child.left);
|
|
}
|
|
|
|
position.width = position.right - position.left;
|
|
position.height = position.top - position.bottom;
|
|
|
|
return position;
|
|
}
|
|
|
|
/**
|
|
* Copies settings from a parsed choice to its arguments. What settings to
|
|
* copy over are determined by the schema's content's argumentMap attribute.
|
|
*
|
|
* @param {Object} schema A simple Object with basic information on the
|
|
* chosen possibility.
|
|
* @param {Object} choice The simple definition of the Object chosen from
|
|
* a choices array.
|
|
* @param {Object} output The Object (likely a parsed possibility content)
|
|
* having its arguments modified.
|
|
*/
|
|
private copySchemaArguments(schema: IPossibility, choice: IPossibilityChild, output: IChoice): void {
|
|
var map: IArgumentMap = schema.contents.argumentMap,
|
|
i: string;
|
|
|
|
if (!map) {
|
|
return;
|
|
}
|
|
|
|
if (!output.arguments) {
|
|
output.arguments = {};
|
|
}
|
|
|
|
for (i in map) {
|
|
if (map.hasOwnProperty(i)) {
|
|
output.arguments[map[i]] = choice[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures an output from parseChoice contains all the necessary size
|
|
* measurements, as listed in this.sizingNames.
|
|
*
|
|
* @param {Object} output The Object (likely a parsed possibility content)
|
|
* having its arguments modified.
|
|
* @param {Object} choice The simple definition of the Object chosen from
|
|
* a choices array.
|
|
* @param {Object} schema A simple Object with basic information on the
|
|
* chosen possibility.
|
|
*/
|
|
private ensureSizingOnChoice(output: IChoice, choice: IPossibilityChild, schema: IPossibility): void {
|
|
var name: string,
|
|
i: string;
|
|
|
|
for (i in this.sizingNames) {
|
|
if (!this.sizingNames.hasOwnProperty(i)) {
|
|
continue;
|
|
}
|
|
|
|
name = this.sizingNames[i];
|
|
|
|
output[name] = (choice.sizing && typeof choice.sizing[name] !== "undefined")
|
|
? choice.sizing[name]
|
|
: schema[name];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures an output from parseChoice contains all the necessary position
|
|
* bounding box measurements, as listed in this.directionNames.
|
|
*
|
|
* @param {Object} output The Object (likely a parsed possibility content)
|
|
* having its arguments modified.
|
|
* chosen possibility.
|
|
* @param {Object} position An Object that contains .left, .right, .top,
|
|
* and .bottom.
|
|
*/
|
|
private ensureDirectionBoundsOnChoice(output: IChoice, position: IPosition): void {
|
|
var i: string;
|
|
|
|
for (i in this.directionNames) {
|
|
if (this.directionNames.hasOwnProperty(i)) {
|
|
output[this.directionNames[i]] = position[this.directionNames[i]];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|