/**
* File : shapers/Shaper.js
* Created : 13/04/2015
* By : Francesc Busquets <francesc@gmail.com>
*
* JClic.js
* An HTML5 player of JClic activities
* https://projectestac.github.io/jclic.js
*
* @source https://github.com/projectestac/jclic.js
*
* @license EUPL-1.2
* @licstart
* (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
*
* Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
* the European Commission- subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
*
* You may obtain a copy of the Licence at:
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* Licence for the specific language governing permissions and limitations
* under the Licence.
* @licend
* @module
*/
import $ from 'jquery';
import { log, attrForEach, getBoolean, getAttr, setAttr } from '../Utils.js';
import { Shape, Rectangle, Ellipse, PathStroke, Path } from '../AWT.js';
/**
* The function of this class and its subclasses is to draw a set of "shapes" that will be used to
* place {@link module:boxes/ActiveBox.ActiveBox ActiveBox} objects at a specific position, and to determine its dimension and
* appearance.
*/
export class Shaper {
/**
* Shaper constructor
* @param {number} nx - Number of columns (in grid-based shapers)
* @param {number} ny - Number of rows (in grid-based shapers)
*/
constructor(nx, ny) {
this.reset(nx, ny);
}
/**
* Registers a new type of shaper
* @param {string} shaperName - The name used to identify this shaper
* @param {function} shaperClass - The shaper class, usually extending Shaper
* @returns {module:shapers/Shaper.Shaper} - The provided shaper class
*/
static registerClass(shaperName, shaperClass) {
Shaper.CLASSES[shaperName] = shaperClass;
return shaperClass;
}
/**
* Factory constructor that returns a Shaper of the requested class.
* @param {string} className - The class name of the requested Shaper.
* @param {number} nx - Number of columns (in grid-based shapers)
* @param {number} ny - Number of rows (in grid-based shapers)
* @returns {module:shapers/Shaper.Shaper}
*/
static getShaper(className, nx, ny) {
const cl = Shaper.CLASSES[(className || '').replace(/^edu\.xtec\.jclic\.shapers\./, '@')];
if (!cl)
log('error', `Unknown shaper: ${className}`);
return cl ? new cl(nx, ny) : null;
}
/**
* Initializes this Shaper to default values
* @param {number} nCols - Number of columns
* @param {number} nRows - Number of rows
*/
reset(nCols, nRows) {
this.nCols = nCols;
this.nRows = nRows;
this.nCells = nRows * nCols;
this.initiated = false;
this.shapeData = [];
for (let i = 0; i < this.nCells; i++)
this.shapeData[i] = new Shape();
}
/**
* Loads this shaper settings from a specific JQuery XML element
* @param {external:jQuery} $xml - The XML element with the shaper data
*/
setProperties($xml) {
attrForEach($xml.get(0).attributes, (name, value) => {
switch (name) {
case 'class':
this.className = value;
break;
case 'cols':
this.nCols = Number(value);
break;
case 'rows':
this.nRows = Number(value);
break;
case 'baseWidthFactor':
case 'toothHeightFactor':
case 'scaleX':
case 'scaleY':
this[name] = Number(value);
break;
case 'randomLines':
case 'showEnclosure':
this[name] = getBoolean(value, true);
break;
}
});
// Reads the 'enclosing'
// (main shape area where the other shape elements are placed)
$xml.children('enclosing:first').each((_n, child) => {
$(child).children('shape:first').each((_n, child2) => {
let sh = Shaper.readShapeData(child2, this.scaleX, this.scaleY);
this.enclosing = sh;
this.showEnclosure = true;
this.hasRemainder = true;
});
});
// Read the shape elements
$xml.children('shape').each((n, child) => {
this.shapeData[n] = Shaper.readShapeData(child, this.scaleX, this.scaleY);
});
// Correction needed for '@Holes' shaper
if (this.shapeData.length > 0 /* && this.shapeData.length !== this.nRows * this.nCols */) {
//this.nRows = this.shapeData.length
//this.nCols = 1
//this.nCells = this.nCols * this.nRows
this.nCells = this.shapeData.length;
}
return this;
}
/**
* Reads an individual shape from an XML element.
* Shapes are arrays of `stroke` objects.
* Each `stroke` has an `action` (_move to_, _line to_, _quad to_...) and associated `data`.
* @param {external:jQuery} $xml - The XML element with the shape data
* @param {number} scaleX
* @param {number} scaleY
* @returns {module:AWT.Shape}
*/
static readShapeData(xml, scaleX, scaleY) {
const shd = [];
let result = null;
$.each(xml.textContent.split('|'), (_n, txt) => {
const sd = txt.split(':');
// Possible strokes are: `rectangle`, `ellipse`, `M`, `L`, `Q`, `B`, `X`
// Also possible, but not currently used in JClic: `roundRectangle` and `pie`
let data = sd.length > 1 ? sd[1].split(',') : null;
//
// Data should be always divided by the scale (X or Y)
if (data)
data = data.map((d, n) => d / (n % 2 ? scaleY : scaleX));
switch (sd[0]) {
case 'rectangle':
result = new Rectangle(data[0], data[1], data[2], data[3]);
break;
case 'ellipse':
result = new Ellipse(data[0], data[1], data[2], data[3]);
break;
default:
// It's an `AWT.PathStroke`
shd.push(new PathStroke(sd[0], data));
break;
}
});
return !result && shd.length > 0 ? new Path(shd) : result;
}
/**
* Gets a object with the basic attributes needed to rebuild this instance excluding functions,
* parent references, constants and also attributes retaining the default value.
* The resulting object is commonly usued to serialize elements in JSON format.
* @returns {object} - The resulting object, with minimal attrributes
*/
getAttributes() {
const fields = [
'className', 'nCols', 'nRows',
'baseWidthFactor', 'toothHeightFactor',
'scaleX', 'scaleY',
'randomLines',
];
if (this.customShapes) {
['showEnclosure', 'hasRemainder',
'enclosing', 'shapeData', // Array of AWT.Rectangle, AWT.Ellipse or (AWT.Path -> AWT.PathStroke)
].forEach(f => fields.push(f));
}
return getAttr(this, fields);
}
/**
* Builds a new shaper, based on the properties specified in a data object
* @param {object} data - The data object to be parsed
* @returns {module:shapers/Shaper.Shaper}
*/
static factory(data) {
const result = Shaper.getShaper(data.className, data.nCols, data.nRows);
setAttr(result, data, [
'className', 'nCols', 'nRows',
'baseWidthFactor', 'toothHeightFactor',
'scaleX', 'scaleY',
'randomLines',
'showEnclosure', 'hasRemainder',
{ key: 'enclosing', fn: Shape },
{ key: 'shapeData', fn: Shape, group: 'array' },
]);
result.nCells = result.shapeData.length || result.nCols * result.nRows;
return result;
}
/**
* Builds the individual shapes that will form this Shaper
*/
buildShapes() {
}
/**
* Gets a clone of the nth Shape object, scaled and located inside a Rectangle
* @param {number} n
* @param {module:AWT.Rectangle} rect
* @returns {module:AWT.Shape}
*/
getShape(n, rect) {
if (!this.initiated)
this.buildShapes();
if (n >= this.nCells || this.shapeData[n] === null)
return null;
return this.shapeData[n].getShape(rect);
}
/**
* Gets the nth Shape data object
* @param {number} n
* @returns {object}
*/
getShapeData(n) {
return n >= 0 && n < this.shapeData.length ? this.shapeData[n] : null;
}
/**
* Gets the AWT.Rectangle that contains all shapes of this Shaper.
* @returns {module:AWT.Rectangle}
*/
getEnclosingShapeData() {
return new Rectangle(0, 0, 1, 1);
}
/**
* When `hasRemainder` is true, this method gets the rectangle containing the full surface where
* the Shaper develops.
* @param {module:AWT.Rectangle} rect - The frame where to move and scale all the shapes
* @returns {module:AWT.Rectangle}
*/
getRemainderShape(rect) {
if (!this.hasRemainder)
return null;
if (!this.initiated)
this.buildShapes();
const sh = this.getEnclosingShapeData();
const r = sh ? sh.getShape(rect) : new Rectangle();
for (let i = 0; i < this.nCells; i++) {
if (this.shapeData[i])
r.add(this.shapeData[i].getShape(rect), false);
}
return r;
}
}
Object.assign(Shaper.prototype, {
/**
* This shaper class name
* @name module:shapers/Shaper.Shaper#className
* @type {string} */
className: 'Shaper',
/**
* Number of columns (useful in grid-based shapers)
* @name module:shapers/Shaper.Shaper#nCols
* @type {number} */
nCols: 0,
/**
* Number of rows (useful in grid-based shapers)
* @name module:shapers/Shaper.Shaper#nRows
* @type {number} */
nRows: 0,
/**
* Number of cells managed by this shaper
* @name module:shapers/Shaper.Shaper#nCells
* @type {number} */
nCells: 0,
/**
* Contains the specific definition of each shape
* @name module:shapers/Shaper.Shaper#shapeData
* @type {object} */
shapeData: null,
/**
* Flag used to check if the `Shaper` has been initialized against a real surface
* @name module:shapers/Shaper.Shaper#initiated
* @type {boolean} */
initiated: false,
//
// Fields used only in JigSaw shapers
/**
* In {@link module:shapers/JigSaw.JigSaw JigSaw}, ratio between the base width of the tooth and the total length of the side.
* @name module:shapers/Shaper.Shaper#baseWidthFactor
* @type {number} */
baseWidthFactor: 1.0 / 3,
/**
* In {@link module:shapers/JigSaw.JigSaw JigSaw}, ratio between the tooth height and the total length of the side.
* @name module:shapers/Shaper.Shaper#toothHeightFactor
* @type {number} */
toothHeightFactor: 1.0 / 6,
/**
* In {@link module:shapers/JigSaw.JigSaw JigSaw}, whether the tooths take random directions or not
* @name module:shapers/Shaper.Shaper#randomLines
* @type {boolean} */
randomLines: false,
//
// Fields used only in the `Holes` shaper
/**
* In {@link module:shapers/Holes.Holes Holes}, scale to be applied to horizontal positions and lengths to achieve the real
* value of the shape placed on a real surface.
* @name module:shapers/Shaper.Shaper#scaleX
* @type {number} */
scaleX: 1.0,
/**
* In {@link module:shapers/Holes.Holes Holes}, scale to be applied to vertical positions and lengths to achieve the real
* value of the shape placed on a real surface.
* @name module:shapers/Shaper.Shaper#scaleY
* @type {number} */
scaleY: 1.0,
/**
* In {@link module:shapers/Holes.Holes Holes}, the enclosing area where all shapes are placed.
* @name module:shapers/Shaper.Shaper#enclosing
* @type {module:AWT.Shape} */
enclosing: null,
/**
* In {@link module:shapers/Holes.Holes Holes}, when `true`, the enclosing area will be drawn
* @name module:shapers/Shaper.Shaper#showEnclosure
* @type {boolean} */
showEnclosure: false,
/**
* Flag indicating if this shaper organizes its cells in rows and columns
* @name module:shapers/Shaper.Shaper#rectangularShapes
* @type {boolean} */
rectangularShapes: false,
/**
* Flag indicating if this Shaper deploys over a surface biggest than the rectangle enclosing
* all its shapes
* @name module:shapers/Shaper.Shaper#hasRemainder
* @type {boolean} */
hasRemainder: false,
/**
* Only the `Holes` shaper has this flag activated
* @name module:shapers/Shaper.Shaper#customShapes
* @type {boolean} */
customShapes: false,
});
/**
* List of known classes derived from Shaper. It should be filled by real shaper classes at
* declaration time.
* @type {object} */
Shaper.CLASSES = {};
export default Shaper;