/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import {Controller} from "@hotwired/stimulus";
import {AlertSwal} from "../../helpers/swal";
import {trans} from "../../translator.js";
/**
* Color definitions for the resistor color code.
* digit: significant figure (null if the color can't be used for a digit band)
* multiplier: factor applied by a multiplier band
* tolerance: tolerance in percent (null if not usable as tolerance band)
* temp: temperature coefficient in ppm/K (null if not usable as temp. band)
* hex/text: colors used to draw the band and a readable label on top of it
*/
const RESISTOR_COLORS = {
black: {digit: 0, multiplier: 1e0, tolerance: null, temp: 250, hex: "#000000", text: "#ffffff"},
brown: {digit: 1, multiplier: 1e1, tolerance: 1, temp: 100, hex: "#5c3a21", text: "#ffffff"},
red: {digit: 2, multiplier: 1e2, tolerance: 2, temp: 50, hex: "#c8102e", text: "#ffffff"},
orange: {digit: 3, multiplier: 1e3, tolerance: null, temp: 15, hex: "#f25c05", text: "#000000"},
yellow: {digit: 4, multiplier: 1e4, tolerance: null, temp: 25, hex: "#f2c200", text: "#000000"},
green: {digit: 5, multiplier: 1e5, tolerance: 0.5, temp: 20, hex: "#1a8f3c", text: "#ffffff"},
blue: {digit: 6, multiplier: 1e6, tolerance: 0.25, temp: 10, hex: "#0a4ea3", text: "#ffffff"},
violet: {digit: 7, multiplier: 1e7, tolerance: 0.1, temp: 5, hex: "#6a2c91", text: "#ffffff"},
grey: {digit: 8, multiplier: 1e8, tolerance: 0.05, temp: 1, hex: "#808080", text: "#ffffff"},
white: {digit: 9, multiplier: 1e9, tolerance: null, temp: null, hex: "#f5f5f5", text: "#000000"},
gold: {digit: null, multiplier: 0.1, tolerance: 5, temp: null, hex: "#c2a000", text: "#000000"},
silver: {digit: null, multiplier: 0.01, tolerance: 10, temp: null, hex: "#b3b3b3", text: "#000000"},
};
// Capacitor tolerance letter codes (percent, or absolute in pF for small caps)
const CAP_TOLERANCE = {
B: "±0.10 pF", C: "±0.25 pF", D: "±0.5 pF", F: "±1%", G: "±2%",
J: "±5%", K: "±10%", M: "±20%", Z: "+80% / -20%",
};
// EIA-96 significant value lookup (code 01..96)
const EIA96_VALUES = [
100, 102, 105, 107, 110, 113, 115, 118, 121, 124, 127, 130, 133, 137, 140, 143,
147, 150, 154, 158, 162, 165, 169, 174, 178, 182, 187, 191, 196, 200, 205, 210,
215, 221, 226, 232, 237, 243, 249, 255, 261, 267, 274, 280, 287, 294, 301, 309,
316, 324, 332, 340, 348, 357, 365, 374, 383, 392, 402, 412, 422, 432, 442, 453,
464, 475, 487, 499, 511, 523, 536, 549, 562, 576, 590, 604, 619, 634, 649, 665,
681, 698, 715, 732, 750, 768, 787, 806, 825, 845, 866, 887, 909, 931, 953, 976,
];
const EIA96_MULTIPLIERS = {
Z: 0.001, Y: 0.01, R: 0.01, X: 0.1, S: 0.1, A: 1, B: 10, C: 100, D: 1000, E: 10000, F: 100000,
};
// Typical dimensions of axial THT resistors per power rating.
// len/dia = body length and diameter (mm), pitch = typical lead spacing (mm).
const RESISTOR_POWERS = {
"0.125": {label: "1/8 W", len: 3.4, dia: 1.9, pitch: 7.62, pitchIn: "0.3\""},
"0.25": {label: "1/4 W", len: 6.3, dia: 2.4, pitch: 10.16, pitchIn: "0.4\""},
"0.5": {label: "1/2 W", len: 9.0, dia: 3.2, pitch: 12.7, pitchIn: "0.5\""},
"1": {label: "1 W", len: 11.5, dia: 4.5, pitch: 15.24, pitchIn: "0.6\""},
"2": {label: "2 W", len: 15.5, dia: 5.0, pitch: 20.32, pitchIn: "0.8\""},
};
// Standard SMD (chip) packages: imperial code -> metric code, size (mm), power (W).
const SMD_PACKAGES = {
"0201": {metric: "0603", l: 0.6, w: 0.3, power: 0.05},
"0402": {metric: "1005", l: 1.0, w: 0.5, power: 0.063},
"0603": {metric: "1608", l: 1.6, w: 0.8, power: 0.1},
"0805": {metric: "2012", l: 2.0, w: 1.25, power: 0.125},
"1206": {metric: "3216", l: 3.2, w: 1.6, power: 0.25},
"1210": {metric: "3225", l: 3.2, w: 2.5, power: 0.33},
"2010": {metric: "5025", l: 5.0, w: 2.5, power: 0.5},
"2512": {metric: "6332", l: 6.3, w: 3.2, power: 1.0},
};
// Common lead pitches for radial ceramic capacitors.
const CAP_PITCHES = {
"2.5": "0.1\"",
"2.54": "0.1\"",
"5": "0.2\"",
"5.08": "0.2\"",
"7.5": "",
"10": "",
"15": "",
};
// Lead length presets (extra pixels the leads extend below the body).
const CAP_LEAD_LENGTHS = {short: 48, medium: 84, long: 130};
// Axial resistor lead length presets (pixels each lead extends beyond the body; medium = original look).
const RESISTOR_LEAD_LENGTHS = {short: 40, medium: 90, long: 150};
const DIM_COLOR = "#6b7280";
export default class extends Controller {
static targets = [
"resistorSvg", "bandSelects", "resistorResult", "resistorValueInput", "resistorBodyColor",
"resistorPower", "resistorSpec",
"capValueInput", "capCodeInput", "capTolerance", "capSvg", "capResult", "capBodyColor",
"capPitch", "capDiameter", "capVoltage", "capSpec", "capShape", "capLead",
"smdValueInput", "smdCode3", "smdCode4", "smdEia96",
"smdSvg", "smdResult", "smdBodyColor", "smdPackage", "smdSpec",
"smdIndValueInput", "smdIndCode", "smdIndPackage", "smdIndBodyColor", "smdIndSvg", "smdIndSpec",
"smdCapValueInput", "smdCapPackage", "smdCapBodyColor", "smdCapVoltage", "smdCapTolerance", "smdCapSvg", "smdCapSpec",
"indBandSelects", "indValueInput", "indSvg", "indResult", "indBodyColor",
"previewInput",
];
static values = {
endpoint: String,
csrf: String,
prefillOhms: { type: Number, default: 0 },
prefillFarads: { type: Number, default: 0 },
};
connect() {
this.bandCount = 5;
this.renderBandSelects();
this.updateCapSpec();
// When opened from a part, pre-fill with the part's detected resistance/capacitance;
// otherwise fall back to illustrative demo values so nothing starts empty. Each section
// is isolated so a failure in one can't block the others (and surfaces on screen).
const partOhms = this.prefillOhmsValue > 0 ? this.prefillOhmsValue : null;
const partFarads = this.prefillFaradsValue > 0 ? this.prefillFaradsValue : null;
const resistorOhms = partOhms ?? 4700;
if (!this.setBandsFromValue(resistorOhms, 1) && this.hasResistorValueInputTarget) {
// Not representable as standard color bands: show it in the value input instead.
this.resistorValueInputTarget.value = this.formatOhms(resistorOhms);
}
this.updateResistor();
try {
this.smdMarking = "code3";
// 10 kΩ demo has a clean code in every representation (103 / 1002 / 01C) so the
// EIA-96 field isn't "—" on first open, unlike an E24 value such as 4.7 kΩ.
this.smdOhms = partOhms ?? 10000;
this.setSmdFields(this.smdOhms, null);
this.redrawSmd();
} catch (e) {
console.error("value_calc: SMD init failed", e);
if (this.hasSmdResultTarget) this.smdResultTarget.textContent = "error: " + e.message;
}
try {
this.capPf = (partFarads ?? 100e-9) * 1e12;
this.setCapFields(this.capPf, null);
this.redrawCap();
} catch (e) {
console.error("value_calc: capacitor init failed", e);
if (this.hasCapResultTarget) this.capResultTarget.textContent = "error: " + e.message;
}
try {
//THT inductor colour-band tab: 4 bands read as µH, demo value 100 µH.
this.indBandCount = 4;
if (this.hasIndBandSelectsTarget) {
this.renderBandSelects(this.indBandSelectsTarget, this.indBandCount, "updateInductor");
this.setBandsFromValue(100, 10, this.indBandSelectsTarget, this.indBandCount);
this.updateInductor();
}
} catch (e) {
console.error("value_calc: THT inductor init failed", e);
}
try {
//The SMD inductor tab starts on an illustrative value so it isn't empty on first open.
this.syncSmdInductor();
} catch (e) {
console.error("value_calc: SMD inductor init failed", e);
}
try {
this.syncSmdCap();
} catch (e) {
console.error("value_calc: SMD capacitor init failed", e);
}
// Jump to the tab matching the part's detected type.
if (partFarads !== null && partOhms === null) {
this.activateTab("vc-capacitor-tab");
} else if (partOhms !== null) {
this.activateTab("vc-resistor-tab");
}
}
/** Activates a Bootstrap tab by its button id (no-op if unavailable). */
activateTab(id) {
const btn = document.getElementById(id);
if (!btn) {
return;
}
try {
btn.click();
} catch (e) {
/* ignore — the default tab is fine */
}
}
/**
* Public helper used by the bulk generator: renders the picture for the given component and
* returns its SVG markup (without any tab interaction). `type` is 'resistor', 'smd_resistor'
* or 'capacitor'; `value` is ohms (resistors) or farads (capacitors); `options` may carry
* {voltage, package, tolerance}.
*/
generateSvg(type, value, options = {}) {
try {
if (options.bodyColor) {
if (this.hasCapBodyColorTarget) {
this.capBodyColorTarget.value = options.bodyColor;
}
if (this.hasSmdBodyColorTarget) {
this.smdBodyColorTarget.value = options.bodyColor;
}
if (this.hasResistorBodyColorTarget) {
this.resistorBodyColorTarget.value = options.bodyColor;
}
}
if (type === "capacitor") {
if (this.hasCapDiameterTarget && options.diameter > 0) {
this.capDiameterTarget.value = String(options.diameter);
}
if (this.hasCapPitchTarget && options.pitch) {
this.capPitchTarget.value = String(options.pitch);
}
if (this.hasCapVoltageTarget) {
this.capVoltageTarget.value = options.voltage > 0 ? String(options.voltage) : "";
}
if (this.hasCapShapeTarget && options.shape) {
this.capShapeTarget.value = options.shape;
}
if (this.hasCapLeadTarget && options.leadLength) {
this.capLeadTarget.value = options.leadLength;
}
if (this.hasCapToleranceTarget) {
this.capToleranceTarget.value = options.tolerance ? this.capToleranceLetterForPercent(options.tolerance) : "";
}
this.updateCapSpec();
this.capPf = value * 1e12;
this.setCapFields(this.capPf, null);
this.redrawCap();
return this.hasCapSvgTarget ? this.capSvgTarget.innerHTML.trim() : "";
}
if (type === "smd_resistor" || type === "smd") {
if (options.package && this.hasSmdPackageTarget) {
this.smdPackageTarget.value = options.package;
}
//On SMD resistors the tolerance is expressed by the marking system: 1% (or tighter)
//uses the 4-digit code, looser tolerances use the 3-digit code. Fall back to 3-digit
//if the 4-digit code can't represent the value.
const wants4 = options.tolerance != null && options.tolerance <= 1;
this.smdMarking = wants4 && this.ohmsTo4Digit(value) ? "code4" : "code3";
this.smdOhms = value;
this.smdTolerance = options.tolerance;
this.smdVoltage = options.voltage;
this.setSmdFields(value, null);
this.redrawSmd();
return this.hasSmdSvgTarget ? this.smdSvgTarget.innerHTML.trim() : "";
}
if (type === "inductor") {
//The inductor colour code is the resistor code read as microhenries.
if (options.leadLength) {
this.resistorLead = options.leadLength;
}
const desiredBands = (options.tolerance != null && options.tolerance <= 2) ? 5 : 4;
if (this.hasBandSelectsTarget && this.bandCount !== desiredBands) {
this.bandCount = desiredBands;
this.renderBandSelects();
}
this.setBandsFromValue(value / 1e-6, options.tolerance ?? 10);
this.drawInductor(this.selectedColors(), value, options.bodyColor, null, null, {tolerance: options.tolerance, voltage: options.voltage});
return this.hasResistorSvgTarget ? this.resistorSvgTarget.innerHTML.trim() : "";
}
if (type === "smd_inductor") {
//Molded/shielded SMD power inductor: the printed marking is the 3-digit EIA code in µH.
const marking = this.henriesToInductorCode(value / 1e-6);
const t = this.hasSmdIndSvgTarget ? this.smdIndSvgTarget : this.smdSvgTarget;
this.drawSmdInductor(t, marking, value, options);
return t ? t.innerHTML.trim() : "";
}
if (type === "smd_capacitor") {
//MLCC chip: unmarked, value shown as a caption. `value` is farads.
this.smdCapPf = value * 1e12;
const t = this.hasSmdCapSvgTarget ? this.smdCapSvgTarget : this.capSvgTarget;
this.drawSmdCapacitor(t, {package: options.package || "0805", bodyColor: options.bodyColor, voltage: options.voltage, tolerance: options.tolerance});
return t ? t.innerHTML.trim() : "";
}
if (type === "diode") {
//Bulk-only type (no interactive tab): draws into the shared scratch target, like the inductor.
this.drawDiode(this.resistorSvgTarget, options.subtype || "diode", value, options);
return this.hasResistorSvgTarget ? this.resistorSvgTarget.innerHTML.trim() : "";
}
// Resistor (through-hole colour bands)
this.resistorVoltage = options.voltage; //shown on the picture when the part lists a rated voltage
if (this.hasResistorPowerTarget && options.power) {
this.resistorPowerTarget.value = this.resistorPowerKey(options.power);
}
if (options.leadLength) {
this.resistorLead = options.leadLength;
}
//Band count follows real convention: 6 bands when a temp coefficient is given,
//5 bands for tight tolerance (≤2 %), otherwise 4 bands.
const desiredBands = options.ppm ? 6 : ((options.tolerance != null && options.tolerance <= 2) ? 5 : 4);
if (this.hasBandSelectsTarget && this.bandCount !== desiredBands) {
this.bandCount = desiredBands;
this.renderBandSelects();
}
if (!this.setBandsFromValue(value, options.tolerance ?? 5) && this.hasResistorValueInputTarget) {
this.resistorValueInputTarget.value = this.formatOhms(value);
}
if (options.ppm && this.hasBandSelectsTarget) {
this.applyTempBand(options.ppm);
}
this.updateResistor();
return this.hasResistorSvgTarget ? this.resistorSvgTarget.innerHTML.trim() : "";
} catch (e) {
console.error("value_calc: generateSvg failed", e);
return "";
}
}
/**
* Empties the shared preview SVG targets. Bulk previews call this after copying each generated
* SVG into its own cell, so the last-rendered SVG isn't left here with an id that then collides
* with the copy in the (visible) cell — which made the last preview render unclipped.
*/
clearScratchSvg() {
if (this.hasCapSvgTarget) {
this.capSvgTarget.innerHTML = "";
}
if (this.hasSmdSvgTarget) {
this.smdSvgTarget.innerHTML = "";
}
if (this.hasResistorSvgTarget) {
this.resistorSvgTarget.innerHTML = "";
}
if (this.hasSmdIndSvgTarget) {
this.smdIndSvgTarget.innerHTML = "";
}
if (this.hasSmdCapSvgTarget) {
this.smdCapSvgTarget.innerHTML = "";
}
}
/**
* Posts the currently shown SVG of the chosen picture to the server so it gets attached to
* the part. Uses a background request so the modal can close without navigating away (which
* would otherwise trigger the browser's "unsaved changes" prompt and lose the edit form).
*/
attachToPart(event) {
const active = this.element.querySelector(".tab-pane.active");
if (!active) {
return;
}
const containers = {
"vc-resistor": this.hasResistorSvgTarget ? this.resistorSvgTarget : null,
"vc-capacitor": this.hasCapSvgTarget ? this.capSvgTarget : null,
"vc-smd": this.hasSmdSvgTarget ? this.smdSvgTarget : null,
"vc-inductor": this.hasIndSvgTarget ? this.indSvgTarget : null,
"vc-smdind": this.hasSmdIndSvgTarget ? this.smdIndSvgTarget : null,
"vc-smdcap": this.hasSmdCapSvgTarget ? this.smdCapSvgTarget : null,
};
const container = containers[active.id];
const svg = container ? container.innerHTML.trim() : "";
const name = active.dataset.vcName || "Generated image";
this.doAttach(svg, name, event.currentTarget);
}
/** Sends one SVG to the server to be attached to the part (background request, no navigation). */
doAttach(svg, name, btn) {
if (!this.hasEndpointValue) {
return;
}
if (!svg.includes("