diff --git a/index.html b/index.html
index 6b65649..ef07ffa 100644
--- a/index.html
+++ b/index.html
@@ -299,6 +299,8 @@
+
+
diff --git a/js/index.js b/js/index.js
index 3ccfe30..bf78890 100644
--- a/js/index.js
+++ b/js/index.js
@@ -112,6 +112,22 @@ const imgCtx = imgCanvas.getContext("2d");
const bgCanvas = document.getElementById("backgroundCanvas"); // gray bg grid
const bgCtx = bgCanvas.getContext("2d");
+// Layering
+const imageCollection = layers.registerCollection("image", {
+ name: "Image Layers",
+ scope: {
+ always: {
+ key: "default",
+ options: {
+ name: "Default Image Layer",
+ },
+ },
+ },
+});
+
+layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true});
+
+//
function startup() {
testHostConfiguration();
testHostConnection();
diff --git a/js/layers.js b/js/layers.js
index e7bb02b..9dff0b0 100644
--- a/js/layers.js
+++ b/js/layers.js
@@ -4,29 +4,169 @@
* It manages canvases and their locations and sizes according to current viewport views
*/
+// Errors
+class LayerNestedScopesError extends Error {
+ // For when a scope is created in another scope
+}
+class LayerNoScopeError extends Error {
+ // For when an action that requires a scope is attempted
+ // in a collection with no scope.
+}
+
const layers = {
- _layers: [],
- layers: {},
+ collections: makeWriteOnce({}, "layers.collections"),
- // Registers a new layer
- registerLayer: (name) => {
- const layer = {
- id: guid(),
- name: layer,
- // This is where black magic starts
- // A proxy for the canvas object
- canvas: new Proxy(document.createElement("canvas"), {}),
+ // Registers a new collection
+ registerCollection: (key, options = {}) => {
+ defaultOpt(options, {
+ // If collection is visible on the Layer View Toolbar
+ visible: true,
+ // Display name for the collection
+ name: key,
+ /**
+ * If layer creates a layer scope
+ *
+ * A layer scope is a context where one, and only one layer inside it or its
+ * subscopes can be active at a time. Nested scopes are not supported.
+ * It receives an object of type:
+ *
+ * {
+ * // If there must be a selected layer, pass information to create the first
+ * always: {
+ * key,
+ * options
+ * }
+ * }
+ */
+ scope: null,
+ // Parent collection
+ parent: null,
+ });
+
+ // Finds the closest parent with a defined scope
+ const findScope = (collection = options.parent) => {
+ if (!collection) return null;
+
+ if (collection.scope) return collection;
+ return findScope(collection._parent);
};
- },
- // Deletes a layer
- deleteLayer: (layer) => {
- if (typeof layer === "object") {
- layers._layers = layers._layers.filter((l) => l.id === layer.id);
- delete layers[layer.id];
- } else if (typeof layer === "string") {
- layers._layers = layers._layers.filter((l) => l.id === layer);
- delete layers[layer];
- }
+ // Path used for logging purposes
+ const _logpath = options.parent
+ ? options.parent + "." + key
+ : "layers.collections." + key;
+
+ // If we have a scope already, we can't add a new scope
+ if (options.scope && findScope())
+ throw new LayerNestedScopesError(`Layer scopes must not be nested`);
+
+ const collection = makeWriteOnce(
+ {
+ _parent: options.parent,
+ _logpath,
+ _layers: [],
+ layers: {},
+
+ name: options.name,
+
+ scope: options.scope,
+ // Registers a new layer
+ registerLayer: (key, options = {}) => {
+ defaultOpt(options, {
+ // Display name for the layer
+ name: key,
+ });
+
+ // Path used for logging purposes
+ const _layerlogpath = _logpath + ".layers." + key;
+ const layer = makeWriteOnce(
+ {
+ _logpath: _layerlogpath,
+ id: guid(),
+ name: options.name,
+
+ state: new Proxy(
+ {visible: true},
+ {
+ set(obj, opt, val) {
+ switch (opt) {
+ case "visible":
+ layer.canvas.style.display = val ? "block" : "none";
+ break;
+ }
+ obj[opt] = val;
+ },
+ }
+ ),
+
+ // This is where black magic will take place in the future
+ // A proxy for the canvas object
+ canvas: new Proxy(document.createElement("canvas"), {}),
+
+ // Activates this layer in the scope
+ activate: () => {
+ const scope = findScope(collection);
+ if (scope) {
+ scope.active = layer;
+ console.debug(
+ `[layers] Layer ${layer._logpath} now active in scope ${scope._logpath}`
+ );
+ }
+ },
+
+ // Deactivates this layer in the scope
+ deactivate: () => {
+ const scope = findScope(collection);
+ if (scope && scope.active === layer) scope.active = null;
+ console.debug();
+ },
+ },
+ _layerlogpath
+ );
+
+ // Add to indexers
+ collection._layers.push(layer);
+ collection.layers[key] = layer;
+
+ console.info(
+ `[layers] Layer '${layer.name}' at ${layer._logpath} registered`
+ );
+ return layer;
+ },
+
+ // Deletes a layer
+ deleteLayer: (layer) => {
+ collection._layers.splice(
+ collection._layers.findIndex(
+ (l) => l.id === layer || l.id === layer.id
+ ),
+ 1
+ );
+ if (typeof layer === "object") {
+ delete collection.layers[layer.id];
+ } else if (typeof layer === "string") {
+ delete collection.layers[layer];
+ }
+
+ console.info(`[layers] Layer '${layer}' deleted`);
+ },
+ },
+ _logpath
+ );
+
+ if (parent) parent[key] = collection;
+ else layers.collections[key] = collection;
+
+ console.info(
+ `[layers] Collection '${options.name}' at ${_logpath} registered`
+ );
+
+ // If always, we must create a layer to select
+ if (options.scope && options.scope.always)
+ collection
+ .registerLayer(options.scope.always.key, options.scope.always.options)
+ .activate();
+
+ return collection;
},
};
diff --git a/js/util.d.ts b/js/util.d.ts
new file mode 100644
index 0000000..e4cb07f
--- /dev/null
+++ b/js/util.d.ts
@@ -0,0 +1,52 @@
+/**
+ * Generates a random string in the following format:
+ *
+ * xxxx-xxxx-xxxx-...-xxxx
+ *
+ * @param size number of character quartets to generate
+ * @return Generated ID
+ */
+declare function guid(size: number): string;
+
+/**
+ * Sets default values for options parameters
+ *
+ * @param options An object received as a parameter
+ * @param defaults An object with default values for each expected key
+ * @return The original options parameter
+ */
+declare function defaultOpt(
+ options: {[key: string]: any},
+ defaults: {[key: string]: any}
+): {[key: string]: any};
+
+/**
+ * Sets default values for options parameters
+ *
+ * @param options An object received as a parameter
+ * @param defaults An object with default values for each expected key
+ * @return The original options parameter
+ */
+declare function makeReadOnly(
+ options: {[key: string]: any},
+ defaults: {[key: string]: any}
+): {[key: string]: any};
+
+/**
+ * Makes an object read-only, throwing an exception when attempting to set
+ *
+ * @param obj Object to be proxied
+ * @param name Name of the object, for logging purposes
+ * @return The proxied object
+ */
+declare function makeReadOnly(obj: object, name?: string): object;
+
+/**
+ * Makes an object have each key be writeable only once, throwing an exception when
+ * attempting to set an existing parameter
+ *
+ * @param obj Object to be proxied
+ * @param name Name of the object, for logging purposes
+ * @return The proxied object
+ */
+declare function makeWriteOnce(obj: object, name?: string): object;