Source: nodes/frame.js

import { VIEW_MODES } from "../lib/constants.js";
import ObjectNode from "./objectNode.js";

/**
 * @desc <strong>Constructor for internal use only</strong>
 *
 * FrameNode instances represent mira.frame objects in a Max patcher. Using the FrameNode object, it is possible to see
 * which Max objects intersect a given mira.frame object, in both Patching as well as Presentation Modes.
 * @class
 * @extends ObjectNode
 * @extends XebraNode
 */
class FrameNode extends ObjectNode {

	/**
	 * @param  {number} id - The id of the node
	 * @param  {string} type - Type identifier of the node
	 * @param  {number} creationSeq - The sequence number for the creation of this node
	 * @param  {number} patcherId - The id of the parent node
	 */
	constructor(id, type, creationSeq, patcherId) {
		super(id, type, creationSeq, patcherId);

		this._objects = new Set();
		this._viewMode = VIEW_MODES.LINKED;
		this._patcherViewMode = VIEW_MODES.PATCHING;
	}

	/**
	 * The view mode of the FrameNode. In Patching mode, object positions and visibility will be calculated relative to
	 * the patching_rect of the mira.frame object. In Presentation mode, the presentation_rect will be used. Linked mode
	 * will defer to Max. If Max is in Presentation mode, Xebra will use Presentation mode, and if Max is in Patching
	 * mode, Xebra will use Patching mode as well.
	 * @type {number}
	 * @see Xebra.VIEW_MODES
	 */
	get viewMode() {
		if (this._viewMode === VIEW_MODES.LINKED) return this._patcherViewMode;
		return this._viewMode;
	}

	set viewMode(mode) {
		this._viewMode = mode;
		this.emit("viewmode_change", this, mode);
	}

	/**
	 * Sets the view mode of the containing patcher
	 * @private
	 * @type {number}
	 */
	get patcherViewMode() {
		return this._patcherViewMode;
	}

	set patcherViewMode(mode) {
		this._patcherViewMode = mode;
		if (this.isViewModeLinked()) this.emit("viewmode_change", this, mode);
	}

	// Bound callbacks using fat arrow notation

	/**
	 * @private
	 * @fires XebraState.object_added
	 * @param {ObjectNode} obj - The new object
	 */
	_onObjectInitialized = (obj) => {
		this.emit("object_added", obj);
	}

	/**
	 * @private
	 * @fires XebraState.object_changed
	 * @param {ObjectNode} obj - The changed object
	 * @param {ParamNode} param - The changed parameter
	 */
	_onObjectChange = (obj, param) => {
		if (this.getChild(obj.id)) this.emit("object_changed", obj, param);
	}

	/**
	 * Callback called when a contained object is destroyed.
	 * @private
	 * @param {ObjectNode} obj - The destroyed object
	 */
	_onObjectDestroy = (obj) => {
		this.removeObject(obj.id);
	}

	// End of bound callbacks

	/**
	 * Adds the given object to the frame.
	 * @ignore
	 * @param {ObjectNode} obj
	 * @listens ObjectNode.param_changed
	 * @listens ObjectNode.destroy
	 * @fires XebraState.object_added
	 */
	addObject(obj) {
		this._objects.add(obj.id);
		this.addChild(obj.id, obj);

		obj.on("param_changed", this._onObjectChange);
		obj.on("destroy", this._onObjectDestroy);

		if (obj.isReady) {
			this.emit("object_added", obj);
		} else {
			obj.once("initialized", this._onObjectInitialized);
		}
	}

	/**
	 * Checks whether the frame contains the object identified by the given id.
	 * @param  {Xebra.NodeId} id - The id of the object
	 * @return {boolean}
	 */
	containsObject(id) {
		return this.hasChild(id);
	}

	/**
	 * Boundary check whether the given rect is visible within the frame.
	 * @param  {Xebra.PatchingRect} rect - The rectangle to check
	 * @return {boolean} whether the rect is contained or not
	 */
	containsRect(rect) {
		const frameRect = this.viewMode === VIEW_MODES.PATCHING ? this.getParamValue("patching_rect") : this.getParamValue("presentation_rect");

		if (!frameRect) return false; // don't have the rect yet

		if (rect[0] < frameRect[0] + frameRect[2] && // x
			rect[0] + rect[2] >= frameRect[0] &&
			rect[1] < frameRect[1] + frameRect[3] && // y
			rect[1] + rect[3] >= frameRect[1]
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the object with the given id.
	 * @param  {Xebra.NodeId} id - The id of the object
	 * @return {ObjectNode|null} The object (if contained) or null
	 */
	getObject(id) {
		return this.getChild(id);
	}

	/**
	 * Returns a list of all objects contained in the frame.
	 * @return {ObjectNode[]} An array of all contained objects
	 */
	getObjects() {
		const objects = [];

		this._objects.forEach((id) => {
			objects.push(this.getChild(id));
		}, this);

		return objects;
	}

	/**
	 * Returns the frame of the object relative the the frame, in the current view mode, or null if the object is not in
	 * the frame.
	 * @return {Xebra.PatchingRect|null} Relative object frame.
	 */
	getRelativeRect(object) {
		if (!this.containsObject(object.id)) return null;
		const viewMode = this.viewMode;
		const objectRect = object.getParamValue(viewMode === VIEW_MODES.PATCHING ? "patching_rect" : "presentation_rect");
		const thisRect = this.getParamValue(viewMode === VIEW_MODES.PATCHING ? "patching_rect" : "presentation_rect");
		return [objectRect[0] - thisRect[0], objectRect[1] - thisRect[1], objectRect[2], objectRect[3]];
	}

	/**
	 * Checks whether the current view mode is linked.
	 * @return {boolean} Whether the frame defers to Max for it's viewMode or not
	 */
	isViewModeLinked() {
		return this._viewMode === VIEW_MODES.LINKED;
	}

	/**
	 * Removes the object with the given id from the frame.
	 * @ignore
	 * @fires XebraState.object_removed
	 * @param  {Xebra.NodeId} id - The id of the object to remove
	 */
	removeObject(id) {
		const obj = this.removeChild(id);

		if (obj) {

			this._objects.delete(id);

			// make sure to clean up attached event listeners
			obj.removeListener("param_changed", this._onObjectChange);
			obj.removeListener("destroy", this._onObjectDestroy);
			obj.removeListener("initialized", this._onObjectInitialized);

			if (obj.isReady) this.emit("object_removed", obj);
		}
	}
}

export default FrameNode;