{% include "parts/info/_picture.html.twig" %}
+ {% include "tools/value_calculator/_generate_modal.html.twig" %}
diff --git a/templates/tools/value_calculator/_bare.html.twig b/templates/tools/value_calculator/_bare.html.twig
new file mode 100644
index 000000000..207e7e159
--- /dev/null
+++ b/templates/tools/value_calculator/_bare.html.twig
@@ -0,0 +1,5 @@
+{# Parent layout used when the calculator is embedded in a modal: it renders only the
+ card_content block, wrapped in the Turbo frame the modal lazy-loads. #}
+
+ {% block card_content %}{% endblock %}
+
diff --git a/templates/tools/value_calculator/_calculator_body.html.twig b/templates/tools/value_calculator/_calculator_body.html.twig
new file mode 100644
index 000000000..633304e5b
--- /dev/null
+++ b/templates/tools/value_calculator/_calculator_body.html.twig
@@ -0,0 +1,348 @@
+
{% trans %}tools.value_calc.explanation{% endtrans %}
+
+
+ {% if part is not null %}
+
+
+
+ {% trans with {'%part%': part.name} %}tools.value_calc.attach.context{% endtrans %}
+
+
+
+
+
+
+ {% endif %}
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ {# ---------------- Resistor color code ---------------- #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# Band color selects are rendered by the Stimulus controller #}
+
+
+
+
+
+
+
+
+
+
+
{% trans %}tools.value_calc.resistor.from_value_help{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+ {# ---------------- Capacitor code ---------------- #}
+
+
{% trans %}tools.value_calc.capacitor.intro{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans %}tools.value_calc.cap.shape{% endtrans %}
+
+
+
+ {% trans %}tools.value_calc.cap.lead{% endtrans %}
+
+
+
+
+
+
+
+
+ {% trans %}tools.value_calc.cap.diameter{% endtrans %}
+
+ mm
+
+
+
+ {% trans %}tools.value_calc.cap.pitch{% endtrans %}
+
+
+
+ {% trans %}tools.value_calc.cap.voltage{% endtrans %}
+
+ V
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# ---------------- SMD resistor code ---------------- #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans %}tools.value_calc.smd.on_chip_help{% endtrans %}
+
+
+
+
+
+
+
+ {% if part is not null %}
+
+
+
+ {% endif %}
+
diff --git a/templates/tools/value_calculator/_generate_button.html.twig b/templates/tools/value_calculator/_generate_button.html.twig
new file mode 100644
index 000000000..6c1a7cf44
--- /dev/null
+++ b/templates/tools/value_calculator/_generate_button.html.twig
@@ -0,0 +1,7 @@
+{# A trigger button that opens the generator modal (which must be included once via
+ _generate_modal.html.twig). Expects `part` in scope. Optional `btn_class` overrides the styling. #}
+{% if part.id is not null and is_granted('edit', part) and is_granted('@tools.value_calculator') %}
+
+{% endif %}
diff --git a/templates/tools/value_calculator/_generate_modal.html.twig b/templates/tools/value_calculator/_generate_modal.html.twig
new file mode 100644
index 000000000..724f1befb
--- /dev/null
+++ b/templates/tools/value_calculator/_generate_modal.html.twig
@@ -0,0 +1,23 @@
+{# The value-calculator generator modal (lazy-loaded via a Turbo frame). Include ONCE per page
+ where a generate trigger button is shown. Expects `part` in scope. #}
+{% if part.id is not null and is_granted('edit', part) and is_granted('@tools.value_calculator') %}
+
+{% endif %}
diff --git a/templates/tools/value_calculator/bulk_generate.html.twig b/templates/tools/value_calculator/bulk_generate.html.twig
new file mode 100644
index 000000000..3cc183eff
--- /dev/null
+++ b/templates/tools/value_calculator/bulk_generate.html.twig
@@ -0,0 +1,446 @@
+{% extends "main_card.html.twig" %}
+
+{% block title %}{% trans %}tools.bulk_gen.title{% endtrans %}{% endblock %}
+
+{% block card_title %}
+
{% trans %}tools.bulk_gen.title{% endtrans %}
+{% endblock %}
+
+{% block card_content %}
+
{% trans %}tools.bulk_gen.intro{% endtrans %}
+
+
+
+ {% trans %}tools.bulk_gen.help.title{% endtrans %}
+
+ {# Example values as blue "chips", parameter names as grey chips — theme-aware (Bootstrap 5.3 subtle utilities). #}
+ {% set chip = 'badge fw-normal font-monospace bg-primary-subtle text-primary-emphasis border border-primary-subtle' %}
+ {% set pchip = 'badge fw-normal font-monospace bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle' %}
+
+
{% trans %}tools.bulk_gen.help.intro{% endtrans %}
+
+
+
+
+ | {% trans %}tools.bulk_gen.help.col_field{% endtrans %} |
+ {% trans %}tools.bulk_gen.help.col_param{% endtrans %} |
+ {% trans %}tools.bulk_gen.help.col_text{% endtrans %} |
+
+
+
+
+ | {% trans %}tools.bulk_gen.help.value{% endtrans %} |
+ Resistance Capacitance Ω / F |
+ 10nF0.1µF4n74k7470R10k1M |
+
+
+ | {% trans %}tools.bulk_gen.help.voltage{% endtrans %} |
+ Voltage |
+ 50V100V |
+
+
+ | {% trans %}tools.bulk_gen.help.tolerance{% endtrans %} |
+ Tolerance |
+ ±5%10% |
+
+
+ | {% trans %}tools.bulk_gen.help.pitch{% endtrans %} |
+ Pitch RM Lead spacing |
+ pitch 5mmRM55mm pitch |
+
+
+ | {% trans %}tools.bulk_gen.help.diameter{% endtrans %} |
+ Diameter |
+ ⌀5mmdiameter 5mm |
+
+
+ | {% trans %}tools.bulk_gen.help.smd{% endtrans %} |
+ {% trans %}tools.bulk_gen.help.footprint_col{% endtrans %} |
+ 04020603080512061210 |
+
+
+
+
+
+
+
+ {% if with_picture > 0 and not overwrite %}
+
+
{% trans with {'%count%': with_picture} %}tools.bulk_gen.with_picture{% endtrans %}
+
+ {% trans with {'%count%': with_picture} %}tools.bulk_gen.regenerate{% endtrans %}
+
+
+ {% elseif overwrite %}
+
+
{% trans %}tools.bulk_gen.overwrite_mode{% endtrans %}
+
{% trans %}tools.bulk_gen.overwrite_exit{% endtrans %}
+
+ {% endif %}
+
+ {% if selected_count == 0 or candidates is empty %}
+ {% if selected_count == 0 %}
+
{% trans %}tools.bulk_gen.no_selection{% endtrans %}
+ {% elseif with_picture == 0 %}
+
{% trans with {'%count%': selected_count} %}tools.bulk_gen.none{% endtrans %}
+ {% endif %}
+
+ {% trans %}tools.bulk_gen.back{% endtrans %}
+
+ {% else %}
+
+ {# One hidden value-calculator, reused to render every preview below. #}
+
+ {% set part = null %}
+ {% include 'tools/value_calculator/_calculator_body.html.twig' %}
+
+
+
+
+ {{ candidates|length }} {% trans %}tools.bulk_gen.found{% endtrans %}
+ {%- if skipped > 0 %} ({{ skipped }} {% trans %}tools.bulk_gen.skipped{% endtrans %}){% endif %}
+
+
+ {% trans %}tools.bulk_gen.back{% endtrans %}
+
+
+
+ {% set first = candidates|first %}
+ {% set batch_color = first.color ?: (first.type == 'capacitor' ? '#e0a63a' : (first.type == 'resistor' ? '#e8d9b5' : '#20242a')) %}
+
+
+
{% trans %}tools.bulk_gen.apply_all{% endtrans %}:
+ {% if has_caps %}
+
+ {% trans %}tools.value_calc.cap.shape{% endtrans %}
+
+
+ {% endif %}
+ {% if has_caps or has_tht_resistors %}
+
+ {% trans %}tools.value_calc.cap.lead{% endtrans %}
+
+
+ {% endif %}
+ {% if has_caps %}
+
+ {% trans %}tools.value_calc.cap.diameter{% endtrans %}
+
+ mm
+
+
+ {% trans %}tools.value_calc.cap.pitch{% endtrans %}
+
+
+
+ {% trans %}tools.value_calc.cap.voltage{% endtrans %}
+
+ V
+
+ {% endif %}
+ {% if has_tht_resistors %}
+
+ {% trans %}tools.value_calc.field.power{% endtrans %}
+
+
+
+ {% trans %}tools.value_calc.field.ppm{% endtrans %}
+
+
+ {% endif %}
+ {% if has_smd_resistors %}
+
+ {% trans %}tools.value_calc.smd.package{% endtrans %}
+
+
+ {% endif %}
+
+ {% trans %}tools.value_calc.field.tolerance{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# Editable KiCad column suggestions: pick a common symbol/footprint or type your own. #}
+
+
+
+
{% trans %}tools.bulk_gen.kicad_hint{% endtrans %}
+
+
+
+
+ |
+ {% trans %}tools.bulk_gen.part{% endtrans %} |
+ {% trans %}tools.bulk_gen.type{% endtrans %} |
+ {% trans %}tools.bulk_gen.value{% endtrans %} |
+ {% trans %}tools.bulk_gen.appearance{% endtrans %} |
+ {% trans %}tools.value_calc.field.tolerance{% endtrans %} |
+ {% trans %}tools.bulk_gen.kicad{% endtrans %} |
+ {% trans %}tools.bulk_gen.preview{% endtrans %} |
+
+
+
+ {% for c in candidates %}
+ {% set name_key = c.type == 'capacitor' ? 'tools.value_calc.capacitor.title' : (c.type == 'resistor' ? 'tools.value_calc.resistor.title' : 'tools.value_calc.smd.title') %}
+
+ |
+
+ {{ c.part.name }}
+ {%- if c.has_picture %} {% trans %}tools.bulk_gen.has_picture{% endtrans %}{% endif %}
+ |
+ {{ c.type }} |
+
+ {%- if c.type == 'capacitor' -%}
+ {%- if c.value >= 1e-6 %}{{ (c.value / 1e-6)|round(3) }} µF
+ {%- elseif c.value >= 1e-9 %}{{ (c.value / 1e-9)|round(3) }} nF
+ {%- else %}{{ (c.value * 1e12)|round(1) }} pF{% endif -%}
+ {%- else -%}
+ {%- if c.value >= 1e6 %}{{ (c.value / 1e6)|round(3) }} MΩ
+ {%- elseif c.value >= 1e3 %}{{ (c.value / 1e3)|round(3) }} kΩ
+ {%- else %}{{ c.value|round(3) }} Ω{% endif -%}
+ {%- endif -%}
+ {% if c.package %} · {{ c.package }}{% endif %}
+ |
+
+ {% set rowcolor = c.color ?: (c.type == 'capacitor' ? '#e0a63a' : (c.type == 'resistor' ? '#e8d9b5' : '#20242a')) %}
+
+
+ {% if c.type == 'capacitor' %}
+
+ {% endif %}
+
+ {% if c.type == 'capacitor' %}
+ {% set rp = c.pitch ?: 5.08 %}
+
+
+
+
+ {% elseif c.type == 'resistor' %}
+ {% set rpow = c.power ?: 0.25 %}
+
+
+
+
+ {% elseif c.type == 'smd_resistor' %}
+ {% set rpkg = c.package ?: '0805' %}
+
+ {% endif %}
+ |
+
+ {% set rtol = c.tolerance ? c.tolerance|replace({'±': '', '%': '', ' ': '', 'pF': '', 'PF': ''})|trim : '' %}
+
+ |
+
+
+
+
+
+
+ |
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/templates/tools/value_calculator/value_calculator.html.twig b/templates/tools/value_calculator/value_calculator.html.twig
new file mode 100644
index 000000000..fddbb671a
--- /dev/null
+++ b/templates/tools/value_calculator/value_calculator.html.twig
@@ -0,0 +1,11 @@
+{% extends modalMode|default(false) ? 'tools/value_calculator/_bare.html.twig' : 'main_card.html.twig' %}
+
+{% block title %}{% trans %}tools.value_calc.title{% endtrans %}{% endblock %}
+
+{% block card_title %}
+
{% trans %}tools.value_calc.title{% endtrans %}
+{% endblock %}
+
+{% block card_content %}
+ {% include 'tools/value_calculator/_calculator_body.html.twig' %}
+{% endblock %}
diff --git a/tests/Controller/ValueCalculatorControllerTest.php b/tests/Controller/ValueCalculatorControllerTest.php
new file mode 100644
index 000000000..cd9a2906d
--- /dev/null
+++ b/tests/Controller/ValueCalculatorControllerTest.php
@@ -0,0 +1,154 @@
+.
+ */
+
+namespace App\Tests\Controller;
+
+use App\Entity\Parts\Part;
+use App\Entity\UserSystem\User;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\Attributes\Group;
+use Symfony\Bundle\FrameworkBundle\KernelBrowser;
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+use Symfony\Component\DomCrawler\Crawler;
+
+/**
+ * Functional smoke tests for the value calculator tool and the "Generate component images"
+ * bulk action. They exercise the controller entry points (access control, classification loop,
+ * EDA suggestions, rendering) end to end. Part edits are rolled back by DAMADoctrineTestBundle.
+ */
+#[Group('DB')]
+#[Group('slow')]
+final class ValueCalculatorControllerTest extends WebTestCase
+{
+ private function loginAdmin(): KernelBrowser
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $admin = $em->getRepository(User::class)->findOneBy(['name' => 'admin']);
+ if ($admin === null) {
+ $this->markTestSkipped("Fixture user 'admin' not found.");
+ }
+ $client->loginUser($admin);
+ $client->followRedirects(false);
+
+ return $client;
+ }
+
+ public function testValueCalculatorPageLoads(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/value_calc');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testValueCalculatorPrefillsFromPart(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/value_calc?part=1');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testBulkGenerateWithoutSelection(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/bulk_generate_images');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testBulkGenerateClassifiesAResistor(): void
+ {
+ $client = $this->loginAdmin();
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $part = $em->find(Part::class, 1);
+ if ($part === null) {
+ $this->markTestSkipped('Fixture part #1 not found.');
+ }
+ //Make the part look like a resistor without a picture so it becomes a candidate and the
+ //classification + EDA-suggestion code path runs.
+ $part->setName('Resistor 10kΩ 0.25W 1% blue body 50ppm');
+ $part->setMasterPictureAttachment(null);
+ $em->flush();
+
+ $client->request('GET', '/en/tools/bulk_generate_images?ids=1');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testBulkGenerateOverwriteMode(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/bulk_generate_images?ids=1&overwrite=1');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testGenerateImageAttachesPicture(): void
+ {
+ $client = $this->loginAdmin();
+ $row = $this->resistorCandidateRow($client);
+ //The row carries the per-part generate endpoint and its CSRF token (as the JS uses them).
+ $client->request('POST', (string) $row->attr('data-endpoint'), [
+ 'svg' => '
',
+ 'name' => 'Test generated image',
+ 'preview' => '1',
+ '_token' => (string) $row->attr('data-csrf'),
+ ], [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
+
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testSetEdaWritesKicadFields(): void
+ {
+ $client = $this->loginAdmin();
+ $row = $this->resistorCandidateRow($client);
+ $client->request('POST', (string) $row->attr('data-eda-endpoint'), [
+ 'kicad_symbol' => 'Device:R',
+ 'reference_prefix' => 'R',
+ 'kicad_footprint' => 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal',
+ '_token' => (string) $row->attr('data-eda-csrf'),
+ ], [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
+
+ self::assertResponseIsSuccessful();
+ }
+
+ /**
+ * Renames fixture part #1 into a pictureless resistor and returns its candidate row from the
+ * bulk review page — which carries the generate/set-eda endpoints and their CSRF tokens.
+ */
+ private function resistorCandidateRow(KernelBrowser $client): Crawler
+ {
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $part = $em->find(Part::class, 1);
+ if ($part === null) {
+ $this->markTestSkipped('Fixture part #1 not found.');
+ }
+ $part->setName('Resistor 10kΩ 0.25W 1% blue body');
+ $part->setMasterPictureAttachment(null);
+ $em->flush();
+
+ $crawler = $client->request('GET', '/en/tools/bulk_generate_images?ids=1');
+ self::assertResponseIsSuccessful();
+ $row = $crawler->filter('tr[data-csrf]')->first();
+ self::assertGreaterThan(0, $row->count(), 'Expected a classified candidate row on the bulk review page.');
+
+ return $row;
+ }
+}
diff --git a/tests/Services/Tools/ComponentValueGuesserTest.php b/tests/Services/Tools/ComponentValueGuesserTest.php
new file mode 100644
index 000000000..f03405603
--- /dev/null
+++ b/tests/Services/Tools/ComponentValueGuesserTest.php
@@ -0,0 +1,256 @@
+.
+ */
+
+namespace App\Tests\Services\Tools;
+
+use App\Entity\Parameters\PartParameter;
+use App\Entity\Parts\Part;
+use App\Services\Tools\ComponentValueGuesser;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Unit tests for the classification/parsing logic behind the value calculator and bulk image
+ * generator. The guesser has no dependencies, so the tests construct plain Part entities in memory.
+ */
+class ComponentValueGuesserTest extends TestCase
+{
+ private ComponentValueGuesser $guesser;
+
+ protected function setUp(): void
+ {
+ $this->guesser = new ComponentValueGuesser();
+ }
+
+ private function part(string $name, ?string $description = null): Part
+ {
+ $part = new Part();
+ $part->setName($name);
+ if ($description !== null) {
+ $part->setDescription($description);
+ }
+
+ return $part;
+ }
+
+ private function partWithParameter(string $paramName, float $value, string $unit): Part
+ {
+ $part = new Part();
+ $part->setName('Some part');
+ $param = new PartParameter();
+ $param->setName($paramName);
+ $param->setValueTypical($value);
+ $param->setUnit($unit);
+ $part->addParameter($param);
+
+ return $part;
+ }
+
+ /**
+ * @dataProvider resistorValueProvider
+ */
+ public function testResistorValueFromName(string $name, float $expectedOhms): void
+ {
+ [$ohms, $farads] = $this->guesser->extractValue($this->part($name));
+ self::assertNull($farads, "Expected no capacitance for '$name'");
+ self::assertNotNull($ohms, "Expected a resistance for '$name'");
+ self::assertEqualsWithDelta($expectedOhms, $ohms, $expectedOhms * 1e-9 + 1e-9);
+ }
+
+ public static function resistorValueProvider(): \Generator
+ {
+ yield 'plain ohm with space' => ['100 Ω', 100.0];
+ yield 'plain ohm no space' => ['470Ω', 470.0];
+ yield 'R notation' => ['470R', 470.0];
+ yield 'kilo with space (regression: Ω is a PCRE word char under /u)' => ['1 kΩ', 1000.0];
+ yield 'kilo no space' => ['10kΩ', 10000.0];
+ yield 'mega with space' => ['1 MΩ', 1_000_000.0];
+ yield 'decimal kilo' => ['4.7 kΩ', 4700.0];
+ yield 'RKM kilo' => ['4k7', 4700.0];
+ yield 'RKM mega' => ['2M2', 2_200_000.0];
+ yield 'bare magnitude letter' => ['10k', 10000.0];
+ yield 'realistic imported name' => ['Resistor 10 kΩ 0.25W 1% Metal Film', 10000.0];
+ yield 'realistic mega name' => ['Resistor 1 MΩ 0.25W 1% Metal Film', 1_000_000.0];
+ }
+
+ /**
+ * @dataProvider capacitorValueProvider
+ */
+ public function testCapacitorValueFromName(string $name, float $expectedFarads): void
+ {
+ [$ohms, $farads] = $this->guesser->extractValue($this->part($name));
+ self::assertNull($ohms, "Expected no resistance for '$name'");
+ self::assertNotNull($farads, "Expected a capacitance for '$name'");
+ self::assertEqualsWithDelta($expectedFarads, $farads, $expectedFarads * 1e-6);
+ }
+
+ public static function capacitorValueProvider(): \Generator
+ {
+ yield 'nanofarad' => ['10nF', 10e-9];
+ yield 'microfarad greek mu' => ['0.1µF', 0.1e-6];
+ yield 'picofarad' => ['100pF', 100e-12];
+ yield 'RKM nano' => ['4n7', 4.7e-9];
+ yield 'RKM pico' => ['2p2', 2.2e-12];
+ yield 'named ceramic cap' => ['Ceramic capacitor 100nF', 100e-9];
+ }
+
+ public function testValueFromResistanceParameter(): void
+ {
+ $part = $this->partWithParameter('Resistance', 4.7, 'kΩ');
+ [$ohms, $farads] = $this->guesser->extractValue($part);
+ self::assertNull($farads);
+ self::assertEqualsWithDelta(4700.0, $ohms, 1e-6);
+ }
+
+ public function testValueFromCapacitanceParameter(): void
+ {
+ $part = $this->partWithParameter('Capacitance', 100.0, 'nF');
+ [$ohms, $farads] = $this->guesser->extractValue($part);
+ self::assertNull($ohms);
+ self::assertEqualsWithDelta(100e-9, $farads, 1e-15);
+ }
+
+ public function testClassifiesThroughHoleResistor(): void
+ {
+ $guess = $this->guesser->guess($this->part('Resistor 10 kΩ 0.25W 1% blue body'));
+ self::assertNotNull($guess);
+ self::assertSame('resistor', $guess['type']);
+ self::assertEqualsWithDelta(10000.0, $guess['value'], 1e-6);
+ self::assertSame(0.25, $guess['power']);
+ self::assertSame('±1%', $guess['tolerance']);
+ self::assertSame('#2f6db0', $guess['color']);
+ }
+
+ public function testClassifiesSmdResistorFromPackage(): void
+ {
+ $guess = $this->guesser->guess($this->part('Resistor 4.7 kΩ 0805 1% SMD'));
+ self::assertNotNull($guess);
+ self::assertSame('smd_resistor', $guess['type']);
+ self::assertSame('0805', $guess['package']);
+ }
+
+ public function testClassifiesCapacitor(): void
+ {
+ $guess = $this->guesser->guess($this->part('Ceramic capacitor 100nF 50V'));
+ self::assertNotNull($guess);
+ self::assertSame('capacitor', $guess['type']);
+ self::assertSame(50, $guess['voltage']);
+ }
+
+ public function testUnclassifiableReturnsNull(): void
+ {
+ self::assertNull($this->guesser->guess($this->part('Arduino Uno R3 development board')));
+ }
+
+ /**
+ * @dataProvider powerProvider
+ */
+ public function testDetectPower(string $name, float $expected): void
+ {
+ $guess = $this->guesser->guess($this->part($name));
+ self::assertNotNull($guess);
+ self::assertSame($expected, $guess['power']);
+ }
+
+ public static function powerProvider(): \Generator
+ {
+ yield 'decimal watt' => ['Resistor 1k 0.25W', 0.25];
+ yield 'fractional watt' => ['Resistor 1k 1/4W', 0.25];
+ yield 'half watt spaced' => ['Resistor 1k 0.5 W', 0.5];
+ yield 'one watt' => ['Resistor 1k 1W', 1.0];
+ }
+
+ /**
+ * @dataProvider ppmProvider
+ */
+ public function testDetectPpm(string $name, ?int $expected): void
+ {
+ $guess = $this->guesser->guess($this->part($name));
+ self::assertNotNull($guess);
+ self::assertSame($expected, $guess['ppm']);
+ }
+
+ public static function ppmProvider(): \Generator
+ {
+ yield 'plain ppm' => ['Resistor 1k 50ppm', 50];
+ yield 'ppm per celsius' => ['Resistor 1k 100 ppm/°C', 100];
+ yield 'no ppm' => ['Resistor 1k', null];
+ }
+
+ /**
+ * @dataProvider toleranceProvider
+ */
+ public function testDetectTolerance(string $name, ?string $expected): void
+ {
+ $guess = $this->guesser->guess($this->part($name));
+ self::assertNotNull($guess);
+ self::assertSame($expected, $guess['tolerance']);
+ }
+
+ public static function toleranceProvider(): \Generator
+ {
+ yield 'plus-minus percent' => ['Resistor 1k ±5%', '±5%'];
+ yield 'bare percent' => ['Resistor 1k 1%', '±1%'];
+ yield 'sub-percent' => ['Resistor 1k 0.1%', '±0.1%'];
+ }
+
+ /**
+ * @dataProvider colorProvider
+ */
+ public function testDetectBodyColor(string $description, ?string $expected): void
+ {
+ $guess = $this->guesser->guess($this->part('Resistor 1k', $description));
+ self::assertNotNull($guess);
+ self::assertSame($expected, $guess['color']);
+ }
+
+ public static function colorProvider(): \Generator
+ {
+ yield 'blue body' => ['blue body metal film', '#2f6db0'];
+ yield 'green' => ['green body', '#2e7d4f'];
+ yield 'no colour word' => ['axial resistor', null];
+ yield 'colour word inside another word is ignored' => ['tantalum resistor', null];
+ }
+
+ public function testEdaSuggestionForThroughHoleResistor(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'resistor', 'package' => null, 'pitch' => null, 'diameter' => null]);
+ self::assertSame('Device:R', $eda['symbol']);
+ self::assertSame('R', $eda['reference']);
+ self::assertStringContainsString('Resistor_THT:R_Axial', (string) $eda['footprint']);
+ }
+
+ public function testEdaSuggestionForSmdResistor(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'smd_resistor', 'package' => '0805', 'pitch' => null, 'diameter' => null]);
+ self::assertSame('Device:R', $eda['symbol']);
+ self::assertSame('Resistor_SMD:R_0805_2012Metric', $eda['footprint']);
+ }
+
+ public function testEdaSuggestionForCapacitorUsesPitch(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'capacitor', 'package' => null, 'pitch' => 5.08, 'diameter' => 5.0]);
+ self::assertSame('Device:C', $eda['symbol']);
+ self::assertSame('C', $eda['reference']);
+ self::assertStringContainsString('P5.00mm', (string) $eda['footprint']);
+ }
+}
diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf
index d00994931..4fa29ab40 100644
--- a/translations/frontend.en.xlf
+++ b/translations/frontend.en.xlf
@@ -55,6 +55,120 @@
Go!
+
+
+ tools.value_calc.resistor.band_digit
+ Digit
+
+
+
+
+ tools.value_calc.resistor.band_multiplier
+ Multiplier
+
+
+
+
+ tools.value_calc.resistor.band_tolerance
+ Tolerance
+
+
+
+
+ tools.value_calc.resistor.band_temp
+ Temp. coeff.
+
+
+
+
+ tools.value_calc.color.black
+ Black
+
+
+
+
+ tools.value_calc.color.brown
+ Brown
+
+
+
+
+ tools.value_calc.color.red
+ Red
+
+
+
+
+ tools.value_calc.color.orange
+ Orange
+
+
+
+
+ tools.value_calc.color.yellow
+ Yellow
+
+
+
+
+ tools.value_calc.color.green
+ Green
+
+
+
+
+ tools.value_calc.color.blue
+ Blue
+
+
+
+
+ tools.value_calc.color.violet
+ Violet
+
+
+
+
+ tools.value_calc.color.grey
+ Grey
+
+
+
+
+ tools.value_calc.color.white
+ White
+
+
+
+
+ tools.value_calc.color.gold
+ Gold
+
+
+
+
+ tools.value_calc.color.silver
+ Silver
+
+
+
+
+ tools.value_calc.invalid_input
+ Invalid input
+
+
+
+
+ tools.value_calc.tolerance
+ Tolerance
+
+
+
+
+ tools.value_calc.attach.nothing
+ Please generate an image first.
+
+
user.password_strength.crack_time
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 360218689..3e4577a3a 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -12995,6 +12995,612 @@ Buerklin-API Authentication server:
Last stocktake
+
+
+ perm.tools.value_calculator
+ Resistor & capacitor calculator
+
+
+
+
+ tools.value_calc.title
+ Resistor & capacitor calculator
+
+
+
+
+ tools.bulk_gen.title
+ Bulk generate component images
+
+
+
+
+ tools.bulk_gen.intro
+ Selected parts that have no picture yet and look like a resistor, SMD resistor or capacitor are listed below. Review the auto-detected type and value, then generate and attach pictures in one go.
+
+
+
+
+ tools.bulk_gen.pick_location
+ — Select a storage location —
+
+
+
+
+ tools.bulk_gen.load
+ Load parts
+
+
+
+
+ tools.bulk_gen.none
+ None of the %count% selected part(s) could be classified as a resistor, SMD resistor or capacitor without an existing picture.
+
+
+
+
+ tools.bulk_gen.found
+ part(s) ready to illustrate.
+
+
+
+
+ tools.bulk_gen.part
+ Part
+
+
+
+
+ tools.bulk_gen.type
+ Detected type
+
+
+
+
+ tools.bulk_gen.value
+ Value
+
+
+
+
+ tools.bulk_gen.wip
+ Review the detected types and values here — generating and attaching the pictures is the next step.
+
+
+
+
+ tools.bulk_gen.no_selection
+ No parts were selected. Select parts in a list and choose the "Generate component images" action.
+
+
+
+
+ tools.bulk_gen.skipped
+ skipped: already have a picture or not classifiable
+
+
+
+
+ tools.bulk_gen.preview
+ Preview
+
+
+
+
+ tools.bulk_gen.appearance
+ Picture appearance
+
+
+
+
+ tools.bulk_gen.apply_all
+ Apply to all rows
+
+
+
+
+ tools.bulk_gen.back
+ Back to parts
+
+
+
+
+ tools.bulk_gen.with_picture
+ %count% selected part(s) already have a picture and were not listed.
+
+
+
+
+ tools.bulk_gen.regenerate
+ Re-generate / overwrite those %count%
+
+
+
+
+ tools.bulk_gen.overwrite_mode
+ Overwrite mode: parts that already have a picture are included (marked "has picture"). Generating replaces an earlier generated image and becomes the preview — manually uploaded pictures are kept.
+
+
+
+
+ tools.bulk_gen.has_picture_hint
+ This part already has a picture. Generating sets the new image as the preview and replaces any earlier generated image; uploaded photos are kept.
+
+
+
+
+ tools.bulk_gen.overwrite_exit
+ Only parts without a picture
+
+
+
+
+ tools.bulk_gen.has_picture
+ has picture
+
+
+
+
+ tools.bulk_gen.help.title
+ How to get the most auto-filled — what to put in a part's details
+
+
+
+
+ tools.bulk_gen.help.intro
+ Each field is read from the part's parameters first, then from its name and description. Add any of the below to improve detection. A part is only listed here if it has no picture yet and its value can be read.
+
+
+
+
+ tools.bulk_gen.help.col_field
+ Field
+
+
+
+
+ tools.bulk_gen.help.col_param
+ Add a parameter named…
+
+
+
+
+ tools.bulk_gen.help.col_text
+ …or write in the name / description
+
+
+
+
+ tools.bulk_gen.help.value
+ Value (required to classify)
+
+
+
+
+ tools.bulk_gen.help.voltage
+ Rated voltage (capacitors)
+
+
+
+
+ tools.bulk_gen.help.tolerance
+ Tolerance
+
+
+
+
+ tools.bulk_gen.help.pitch
+ Lead pitch (capacitors)
+
+
+
+
+ tools.bulk_gen.help.diameter
+ Body diameter (capacitors)
+
+
+
+
+ tools.bulk_gen.help.smd
+ SMD size (resistors → footprint)
+
+
+
+
+
+ tools.bulk_gen.kicad
+ KiCad
+
+
+
+
+ tools.bulk_gen.write_eda
+ Write KiCad settings to checked parts
+
+
+
+
+ tools.bulk_gen.eda_written
+ EDA settings written
+
+
+
+
+
+ tools.bulk_gen.reference
+ Reference prefix
+
+
+
+
+ tools.bulk_gen.kicad_hint
+ Suggested values — edit any field before writing.
+
+
+
+
+ tools.bulk_gen.generate_attach
+ Attach pictures to the checked parts
+
+
+
+
+ tools.bulk_gen.attached
+ attached
+
+
+
+
+ tools.bulk_gen.failed
+ failed
+
+
+
+
+ part_list.action.group.images
+ Images
+
+
+
+
+ part_list.action.generate_images
+ Generate component images
+
+
+
+
+ tools.value_calc.explanation
+ Decode and visualize resistor color bands, capacitor codes and SMD resistor markings. Pick the band colors to read a resistor, or enter a value to generate the matching color code.
+
+
+
+
+ tools.value_calc.body_color
+ Body color
+
+
+
+
+ tools.value_calc.body.beige
+ Beige
+
+
+
+
+ tools.value_calc.body.blue
+ Blue
+
+
+
+
+ tools.value_calc.body.green
+ Green
+
+
+
+
+ tools.value_calc.body.lightblue
+ Light blue
+
+
+
+
+ tools.value_calc.body.tan
+ Tan
+
+
+
+
+ tools.value_calc.body.red
+ Red
+
+
+
+
+ tools.value_calc.body.purple
+ Purple
+
+
+
+
+ tools.value_calc.body.black
+ Black
+
+
+
+
+ tools.value_calc.body.grey
+ Grey
+
+
+
+
+ tools.value_calc.body.white
+ White
+
+
+
+
+ tools.value_calc.resistor.title
+ Resistor color code
+
+
+
+
+ tools.value_calc.capacitor.title
+ Capacitor code
+
+
+
+
+ tools.value_calc.smd.title
+ SMD resistor code
+
+
+
+
+ tools.value_calc.resistor.bands
+ Number of bands
+
+
+
+
+ tools.value_calc.resistor.bands_4
+ 4 bands
+
+
+
+
+ tools.value_calc.resistor.bands_5
+ 5 bands
+
+
+
+
+ tools.value_calc.resistor.bands_6
+ 6 bands
+
+
+
+
+ tools.value_calc.attach.context
+ The generated image can be attached to part "%part%". Pick a component below and click "Attach to part".
+
+
+
+
+ tools.value_calc.attach.as_preview
+ Use as preview image
+
+
+
+
+ tools.value_calc.attach.button
+ Attach to part
+
+
+
+
+ tools.value_calc.attach.generate_button
+ Generate component image
+
+
+
+
+ tools.value_calc.attach.generate_hint
+ Generate a resistor, capacitor or SMD picture and attach it to this part.
+
+
+
+
+ part.generate_image.flash.success
+ Generated image was attached to the part.
+
+
+
+
+ part.generate_image.flash.invalid
+ No valid image was generated. Please try again.
+
+
+
+
+ tools.value_calc.size
+ Size
+
+
+
+
+ tools.value_calc.resistor.power
+ Power rating
+
+
+
+
+ tools.value_calc.smd.package
+ Package size
+
+
+
+
+ tools.value_calc.cap.diameter
+ Diameter
+
+
+
+
+ tools.value_calc.cap.pitch
+ Pitch
+
+
+
+
+ tools.value_calc.cap.voltage
+ Voltage
+
+
+
+
+ tools.value_calc.cap.shape
+ Shape
+
+
+
+
+ tools.value_calc.cap.shape.disc
+ Disc
+
+
+
+
+ tools.value_calc.cap.shape.blob
+ Blob (MLCC)
+
+
+
+
+ tools.value_calc.cap.lead
+ Leads
+
+
+
+
+ tools.value_calc.cap.lead.short
+ Short
+
+
+
+
+ tools.value_calc.cap.lead.medium
+ Medium
+
+
+
+
+ tools.value_calc.cap.lead.long
+ Long
+
+
+
+
+ tools.value_calc.resistor.result
+ Resistance
+
+
+
+
+ tools.value_calc.resistor.from_value_help
+ Enter a resistance value to generate the color bands. You can use suffixes like k, M and the RKM notation (e.g. 4k7).
+
+
+
+
+ tools.value_calc.resistor.from_value
+ Value to color code
+
+
+
+
+ tools.value_calc.resistor.apply_value
+ Generate bands
+
+
+
+
+ tools.value_calc.capacitor.intro
+ Works for the printed number codes on ceramic (MLCC), film and similar capacitors. Small caps below 100 pF are usually printed directly (e.g. 47 or 4R7), larger ones use the 3-digit code (e.g. 104 = 100 nF).
+
+
+
+
+ tools.value_calc.smd.on_chip_help
+ Edit any field to update the others. Use the radio to pick which code is printed on the chip.
+
+
+
+
+ tools.value_calc.field.value
+ Value
+
+
+
+
+ tools.value_calc.field.code
+ Code
+
+
+
+
+ tools.value_calc.field.tolerance
+ Tolerance
+
+
+
+
+ tools.value_calc.field.power
+ Power
+
+
+
+
+ tools.value_calc.field.ppm
+ Temp. coeff. (ppm/K)
+
+
+
+
+ tools.value_calc.field.code3
+ 3-digit
+
+
+
+
+ tools.value_calc.field.code4
+ 4-digit
+
+
+
+
+ tools.value_calc.field.eia96
+ EIA-96
+
+
+
+
+ tools.value_calc.field.on_chip
+ Printed on the chip
+
+
part.table.eda_reference