Source: schematic.mjs

import { Writable as writeableStream } from "node:stream";

import { v4 as uuid } from "uuid";

import { Component } from "./component.mjs";
import { Coordinate } from "./coordinate.mjs";
import { Net } from "./net.mjs";
import { Pin } from "./pin.mjs";
import { Wire } from "./wire.mjs";

import { atoLaTex } from "./physQuantityParser.mjs";
import { ADS_COMPONENTS_MAP } from "./components.mjs";
import { Converter } from "./converter.mjs";

/**
 * @class
 * Class representing a schematic.
 * 
 * @example 
 * // node: the XML schematic view node
 * let schematic = Schematic.fromXML(node);	// parses the node
 * schematic.printToStream(process.stdout);	// serializes as TikZ code
 * @hideconstructor
 */
class Schematic {
	/** @type {Coordinate[]} */
	#coords;
	/** @type {Map<String, Net>} */
	#nets;
	/** @type {Wire[]} */
	#wires;
	/** @type {Component[]} */
	#components;

	/**
	 * @typedef {object} parameterParseSetting settings for parsing of component parameters.
	 * @property {boolean} parse - set to true to parse and "siunitx-ify"
	 * @property {string} [suggestedUnit] - if no unit can be found while parsing, `suggestedUnit` will be used
	 * @property {string} [forceUnit] - the unit will be replaced with `forceUnit`
	 */

	/**
	 * Settings for parsing components parameters.
	 * @constant
	 * @type {Map<string, parameterParseSetting>}
	 * */
	#parameterParserSettings = new Map([
		["C", { parse: true, suggestedUnit: "uF" }],
		["F", { parse: true, suggestedUnit: "Hz" }],
		["R", { parse: true, suggestedUnit: "Volt" }],
		["L", { parse: true, suggestedUnit: "nH" }],
	]);

	/**
	 * Use `fromXML `to create a schematic.
	 */
	constructor() {
		this.#coords = [];
		this.#nets = new Map();
		this.#wires = [];
		this.#components = [];
	}

	/**
	 * Parses a schematic view node and creates an instance of schematic.
	 *
	 * @param {Element} node - the node to parse
	 * @returns {Schematic} - the parsed schematic
	 */
	static fromXML(node) {
		let schematic = new Schematic();
		schematic.#parse(node);
		return schematic;
	}

	/**
	 * Internal parser function for an schematic view node.
	 *
	 * @param {Element} schematicView - the node to parse
	 * @param {number} [scale=2.54] - the scale factor (inch --> cm)
	 */
	#parse(schematicView, scale = 2.54) {
		//-- 1. get list of XML nodes to parse
		/** @type {Element} */
		const shapes = Converter.getNamedTag(schematicView, "shapes");
		Converter.assertTagFound(shapes, "shapes");
		/** @type {Element[]} */ // list of wire tags; may be empty
		const wireArray = Converter.getNamedTags(shapes, "wire");

		/** @type {Element} */
		const instances = Converter.getNamedTag(schematicView, "instances");
		Converter.assertTagFound(shapes, "instances");
		/** @type {Element[]} */ // list of components
		const instanceArray = Converter.getNamedTags(instances, "instance");

		//-- 2. parse wires -----------------------
		this.#wires = wireArray.map((wireXml) => {
			// Generate or get net
			const nameNode = Converter.getNamedTag(wireXml, "net");
			const netName = (nameNode ? nameNode.getAttribute("name") : "") || ""; // jsdoc doesn't like "?."
			let net = this.#nets.get(netName);
			if (!net) this.#nets.set(netName, (net = new Net(netName)));

			/** @type {Element} */ // node containing coordinates as a string
			const points = ["genpolyline", "centerline", "points"].reduce((node, tagname) => {
				node = Converter.getNamedTag(node, tagname);
				Converter.assertTagFound(node, tagname);
				return node;
			}, wireXml);

			// Parse coordinates
			const wireCoords = points.textContent.split(" ").reduce(
				/**
				 * @param {Coordinate[]} wireCoords
				 * @param {string} coordString
				 * @returns {Coordinate[]}
				 */
				(wireCoords, coordString) => {
					let [x, y] = coordString.split(",", 2);
					x = parseFloat(x);
					y = parseFloat(y);
					if (isFinite(x) && isFinite(y)) {
						let coord = new Coordinate(scale * x, scale * y);
						const oldCoord = this.#coords.find((existingCoord) => coord.equals(existingCoord));
						if (!oldCoord) this.#coords.push(coord);
						wireCoords.push(oldCoord || coord);
					} else {
						console.error("Couldn't parse wire coordinate: " + coordString);
					}
					return wireCoords;
				},
				[]
			);

			// Create wire
			const wire = new Wire(net, wireCoords);
			net.wires.push(wire);

			return wire;
		});

		// 3. Parse components -----------------------
		this.#components = instanceArray.reduce(
			/**
			 * @param {Component[]} components
			 * @param {Element} instanceXml
			 */
			(components, instanceXml) => {
				// get componentStencil
				/** @type {string} */
				const libraryName = instanceXml.getAttribute("libraryName") || "";
				/** @type {string} */
				const cellName = instanceXml.getAttribute("cellName") || "";
				/** @type {string} */ // R1 etc
				const instanceName = instanceXml.getAttribute("instanceName") || "";

				const componentStencil =
					ADS_COMPONENTS_MAP.get(libraryName + ":" + cellName) || ADS_COMPONENTS_MAP.get(cellName);
				if (!componentStencil) {
					console.error(
						"Skipping not identified component %s: %s:%s",
						instanceName.padStart(10),
						libraryName,
						cellName
					);
					return components; // <-- just skips this component
				}

				// extract attributes & parameters & name
				/** @type {Map<string,string>} */ // list/map of component attributes (inside instance <...>)
				const attributes = new Map(
					Array.prototype.map.call(instanceXml.attributes, (attribute) => [
						attribute.name,
						attribute.value || null,
					])
				);

				const parametersNode = Converter.getNamedTag(instanceXml, "parameters") || null;
				const parameterNodes = parametersNode
					? Converter.getNamedTags(
							parametersNode,
							"parameter",
							(param) => param.getAttribute && param.getAttribute("visible") == "true"
					  )
					: [];
				/** @type {Map<string,string>} */ // parameters, e.g. V for the voltage of a source
				const parameters = new Map(
					parameterNodes.map((param) => {
						const key = param.getAttribute("name");
						const parameterParserSetting = this.#parameterParserSettings.get(key);
						let value = param.getAttribute("value");
						if (parameterParserSetting && parameterParserSetting.parse)
							atoLaTex(value, parameterParserSetting.suggestedUnit, parameterParserSetting.forceUnit);
						return [key, value];
					})
				);

				// get placement
				// <abl:PlacementTransform x="1.50000" y="-0.12500" angle="0.00000" xScale="1.00000" yScale="1.00000" mirrorX="false" mirrorY="false"/>
				const placementXml = Converter.getNamedTag(instanceXml, "placementtransform");

				/** struct containing all placement information */
				const placement = placementXml
					? {
							x: parseFloat(placementXml.getAttribute("x")) || 0,
							y: parseFloat(placementXml.getAttribute("y")) || 0,
							angle: parseFloat(placementXml.getAttribute("angle")) || 0,
							xScale: parseFloat(placementXml.getAttribute("xScale")) || 1,
							yScale: parseFloat(placementXml.getAttribute("yScale")) || 1,
							mirrorX: placementXml.getAttribute("mirrorX") == "true",
							mirrorY: placementXml.getAttribute("mirrorY") == "true",
							scaling: scale,
					  }
					: {
							x: 0,
							y: 0,
							angle: 0,
							xScale: 1,
							yScale: 1,
							mirrorX: 1,
							mirrorY: 1,
							scaling: scale,
					  };

				// get pins (no coord yet)
				const instPinsXml = Converter.getNamedTag(instanceXml, "instpins");
				const instPinArray = instPinsXml ? Converter.getNamedTags(instPinsXml, "instpin") : [];
				const pins = instPinArray.map((pinXml) => {
					const instTermNumber = Number.parseInt(pinXml.getAttribute("instTermNumber")) || 0;
					const pinName = pinXml.getAttribute("pinName") || "";
					const netNameNode = Converter.getNamedTag(pinXml, "net");
					let netName = netNameNode ? netNameNode.getAttribute("name") || "" : "";
					if (!netName) netName = uuid();
					let net = this.#nets.get(netName);
					if (!net) this.#nets.set(netName, (net = new Net(netName)));

					return new Pin(null, pinName, instTermNumber, net);
				});

				// stencil --> component
				/** @type {Component | Component[] | null} */
				const component = componentStencil.useAsStencil(
					libraryName,
					cellName,
					instanceName,
					attributes,
					pins,
					placement,
					this.#wires,
					this.#nets,
					this.#coords
				);

				if (component) {
					if (Array.isArray(component)) component.forEach((item) => components.push(item));
					else components.push(component);
				}

				// still in reduce --> return array for next loop
				return components;
			},
			[]
		);
	}

	/**
	 * Serializes the schematic and prints it to an writeable stream. The stream won't be closed.
	 *
	 * @param {writeableStream} out - the stream to write to
	 * @throws {Error} on error writing to `out`
	 */
	async printToStream(out) {
		/**
		 * Prints a line to the output stream and returns a promise.
		 *
		 * @param {string} line - the line to write/print
		 * @returns {Promise<void>}
		 */
		const println = (line) =>
			new Promise((resolve, reject) => {
				out.once("error", reject);
				if (out.write(line + "\n")) {
					out.off("error", reject);
					resolve();
				} else
					out.once("drain", () => {
						// stream "full" --> wait for draining
						out.off("error", reject);
						resolve();
					});
			});

		await println("\\begin{tikzpicture}");
		for (const wire of this.#wires) await println(wire.serialize(1)); // indent 1 tab
		await println(""); // empty line
		for (const component of this.#components) await println(component.serialize(1)); // indent 1 tab
		await println("\\end{tikzpicture}");
	}
}

export { Schematic };