{% 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 00000000..207e7e15
--- /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 00000000..87656157
--- /dev/null
+++ b/templates/tools/value_calculator/_calculator_body.html.twig
@@ -0,0 +1,550 @@
+
+
+
+
{% trans %}tools.value_calc.explanation{% endtrans %}
+
+
+
+ {% if part is not null %}
+
+
+
+ {% trans with {'%part%': part.name} %}tools.value_calc.attach.context{% endtrans %}
+
+
+
+
+
+
+ {% endif %}
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ {# min-height keeps the card the same size on every tab, so switching tabs doesn't resize the panel #}
+
+ {# ---------------- Resistor color code ---------------- #}
+
+
+
+
+
+
+
+
+ {# Band color selects are rendered by the Stimulus controller #}
+
+
+
+
+
+
+
+
+
+
{% trans %}tools.value_calc.resistor.from_value_help{% endtrans %}
+
+
+
+
+ {% trans %}tools.value_calc.appearance{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# ---------------- Capacitor code ---------------- #}
+
+
{% trans %}tools.value_calc.capacitor.intro{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans %}tools.value_calc.appearance{% 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 %}
+
+
+
+ {% trans %}tools.value_calc.appearance{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# ---------------- THT inductor colour code ---------------- #}
+
+
{% trans %}tools.value_calc.inductor.intro{% endtrans %}
+
+
+
+
+
+
+ {# Band colour selects are rendered by the Stimulus controller #}
+
+
+
+
+
+
+
+
+
+
{% trans %}tools.value_calc.inductor.from_value_help{% endtrans %}
+
+
+
+
+ {% trans %}tools.value_calc.appearance{% endtrans %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# ---------------- SMD inductor code ---------------- #}
+
+
{% trans %}tools.value_calc.smd_inductor.intro{% endtrans %}
+
+
+
+
+
+ {% trans %}tools.value_calc.appearance{% 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 00000000..ebe5f993
--- /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.component_image_generator') %}
+
+{% 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 00000000..0b5fed93
--- /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.component_image_generator') %}
+
+{% 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 00000000..75762455
--- /dev/null
+++ b/templates/tools/value_calculator/bulk_generate.html.twig
@@ -0,0 +1,484 @@
+{% 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 or has_inductors %}
+
+ {% 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 %}
+
+
+ {% endif %}
+ {% if has_tht_resistors %}
+
+ {% trans %}tools.value_calc.field.power{% endtrans %}
+
+
+
+ {% trans %}tools.value_calc.field.ppm{% endtrans %}
+
+
+ {% endif %}
+ {% if has_smd_resistors or has_smd_inductors or has_smd_capacitors %}
+
+ {% trans %}tools.value_calc.smd.package{% endtrans %}
+
+
+ {% endif %}
+
+ {% trans %}tools.value_calc.cap.voltage{% endtrans %}
+
+ V
+
+
+ {% 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_keys = {
+ 'capacitor': 'tools.value_calc.capacitor.title',
+ 'smd_capacitor': 'tools.value_calc.capacitor.title',
+ 'inductor': 'tools.value_calc.inductor.title',
+ 'smd_inductor': 'tools.value_calc.inductor.title',
+ 'resistor': 'tools.value_calc.resistor.title',
+ 'diode': 'tools.value_calc.diode.title',
+ } %}
+ {% set name_key = name_keys[c.type]|default('tools.value_calc.smd.title') %}
+
+ |
+
+ {{ c.part.name }}
+ {%- if c.has_picture %} {% trans %}tools.bulk_gen.has_picture{% endtrans %}{% endif %}
+ |
+ {{ c.subtype ?: c.type }} |
+
+ {%- if c.type in ['capacitor', 'smd_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 -%}
+ {%- elseif c.type in ['inductor', 'smd_inductor'] -%}
+ {%- if c.value >= 1 %}{{ c.value|round(3) }} H
+ {%- elseif c.value >= 1e-3 %}{{ (c.value / 1e-3)|round(3) }} mH
+ {%- elseif c.value >= 1e-6 %}{{ (c.value / 1e-6)|round(3) }} µH
+ {%- else %}{{ (c.value / 1e-9)|round(1) }} nH{% endif -%}
+ {%- elseif c.type == 'diode' -%}
+ {%- if c.marking %}{{ c.marking }}
+ {%- elseif c.voltage %}{{ c.voltage }} V {{ (c.subtype ?: 'diode')|capitalize }}
+ {%- else %}{{ (c.subtype ?: 'diode')|capitalize }}{% 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 == 'smd_capacitor' ? '#c8a37a' : (c.type == 'resistor' ? '#e8d9b5' : (c.type == 'inductor' ? '#2f6f4c' : (c.type == 'smd_inductor' ? '#38332e' : '#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 in ['smd_resistor', 'smd_inductor', 'smd_capacitor'] %}
+ {% set rpkg = c.package ?: (c.type == 'smd_inductor' ? '1210' : '0805') %}
+
+ {% endif %}
+ {# Rated voltage — printed on the generated image. Not shown for LEDs (no voltage marking). #}
+ {% if not (c.type == 'diode' and c.subtype == 'led') %}
+
+
+ V
+
+ {% endif %}
+ |
+
+ {% if c.type in ['diode', 'smd_capacitor'] %}
+ —
+ {% else %}
+ {% set rtol = c.tolerance ? c.tolerance|replace({'±': '', '%': '', ' ': '', 'pF': '', 'PF': ''})|trim : '' %}
+
+ {% endif %}
+ |
+
+
+
+
+
+
+ |
+ |
+
+ {% 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 00000000..fddbb671
--- /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 00000000..85ced800
--- /dev/null
+++ b/tests/Controller/ValueCalculatorControllerTest.php
@@ -0,0 +1,228 @@
+.
+ */
+
+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
+ {
+ return $this->loginAs('admin');
+ }
+
+ private function loginAs(string $username): KernelBrowser
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $user = $em->getRepository(User::class)->findOneBy(['name' => $username]);
+ if ($user === null) {
+ $this->markTestSkipped("Fixture user '$username' not found.");
+ }
+ $client->loginUser($user);
+ $client->followRedirects(false);
+
+ return $client;
+ }
+
+ /**
+ * Part-DB answers an unauthorized request with 401/403 or a redirect (to the login/permission
+ * page) depending on context, so accept any of those — the point is that access is refused.
+ */
+ private function assertDenied(KernelBrowser $client): void
+ {
+ $code = $client->getResponse()->getStatusCode();
+ $this->assertTrue(
+ $code === 401 || $code === 403 || $client->getResponse()->isRedirect(),
+ "Expected 401/403/redirect for an unauthorized request, got $code"
+ );
+ }
+
+ public function testValueCalculatorPageLoads(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/component_image_generator');
+ self::assertResponseIsSuccessful();
+ }
+
+ public function testValueCalculatorPrefillsFromPart(): void
+ {
+ $client = $this->loginAdmin();
+ $client->request('GET', '/en/tools/component_image_generator?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();
+ $payload = json_decode((string) $client->getResponse()->getContent(), true);
+ self::assertIsArray($payload);
+ self::assertTrue($payload['success'] ?? false, 'Expected {success: true} from the generate-image endpoint.');
+
+ //The picture must actually be attached and (preview=1) set as the master picture.
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $em->clear();
+ $part = $em->find(Part::class, 1);
+ self::assertNotNull($part->getMasterPictureAttachment(), 'Generated image should be set as the master picture.');
+ }
+
+ public function testGenerateImageRejectsInvalidCsrf(): void
+ {
+ $client = $this->loginAdmin();
+ $row = $this->resistorCandidateRow($client);
+ $client->request('POST', (string) $row->attr('data-endpoint'), [
+ 'svg' => '
',
+ 'name' => 'Should be rejected',
+ '_token' => 'definitely-not-a-valid-token',
+ ], [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
+
+ $this->assertDenied($client);
+ }
+
+ public function testValueCalculatorDeniedWithoutPermission(): void
+ {
+ $client = $this->loginAs('noread');
+ $client->request('GET', '/en/tools/component_image_generator');
+ $this->assertDenied($client);
+ }
+
+ public function testBulkGenerateDeniedWithoutPermission(): void
+ {
+ $client = $this->loginAs('noread');
+ $client->request('GET', '/en/tools/bulk_generate_images?ids=1');
+ $this->assertDenied($client);
+ }
+
+ public function testGenerateImageDeniedWithoutEditPermission(): void
+ {
+ $client = $this->loginAs('noread');
+ $client->request('POST', '/en/part/1/generate_image', [
+ 'svg' => '
',
+ '_token' => 'irrelevant-edit-is-checked-first',
+ ], [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
+ $this->assertDenied($client);
+ }
+
+ 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();
+ $payload = json_decode((string) $client->getResponse()->getContent(), true);
+ self::assertTrue($payload['success'] ?? false, 'Expected {success: true} from the set-eda endpoint.');
+
+ //The EDA fields must actually be written to the part.
+ $em = static::getContainer()->get(EntityManagerInterface::class);
+ $em->clear();
+ $eda = $em->find(Part::class, 1)->getEdaInfo();
+ self::assertSame('Device:R', $eda->getKicadSymbol());
+ self::assertSame('R', $eda->getReferencePrefix());
+ self::assertSame('Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal', $eda->getKicadFootprint());
+ }
+
+ /**
+ * 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 00000000..f76671de
--- /dev/null
+++ b/tests/Services/Tools/ComponentValueGuesserTest.php
@@ -0,0 +1,462 @@
+.
+ */
+
+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 testClassifiesSmdCapacitorFromPackage(): void
+ {
+ $guess = $this->guesser->guess($this->part('MLCC capacitor 100nF 0805 X7R'));
+ self::assertNotNull($guess);
+ self::assertSame('smd_capacitor', $guess['type']);
+ self::assertSame('0805', $guess['package']);
+ self::assertEqualsWithDelta(100e-9, $guess['value'], 1e-15);
+
+ $eda = $this->guesser->edaSuggestion($guess);
+ self::assertSame('Device:C', $eda['symbol']);
+ self::assertSame('Capacitor_SMD:C_0805_2012Metric', $eda['footprint']);
+ }
+
+ 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']);
+ }
+
+ /**
+ * @dataProvider inductorValueProvider
+ */
+ public function testClassifiesInductor(string $name, float $expectedHenries): void
+ {
+ $guess = $this->guesser->guess($this->part($name));
+ self::assertNotNull($guess, "Expected '$name' to classify");
+ self::assertSame('inductor', $guess['type']);
+ self::assertEqualsWithDelta($expectedHenries, $guess['value'], $expectedHenries * 1e-6);
+ }
+
+ public static function inductorValueProvider(): \Generator
+ {
+ yield 'microhenry µ' => ['Inductor 100µH', 100e-6];
+ yield 'microhenry u' => ['Choke 4.7uH', 4.7e-6];
+ yield 'millihenry' => ['Coil 10mH', 10e-3];
+ yield 'nanohenry' => ['100nH inductor', 100e-9];
+ yield 'henry' => ['1H filter choke', 1.0];
+ }
+
+ public function testInductanceFromParameter(): void
+ {
+ $part = $this->partWithParameter('Inductance', 100.0, 'µH');
+ $guess = $this->guesser->guess($part);
+ self::assertNotNull($guess);
+ self::assertSame('inductor', $guess['type']);
+ self::assertEqualsWithDelta(100e-6, $guess['value'], 1e-12);
+ }
+
+ public function testMegahertzIsNotMistakenForInductance(): void
+ {
+ //"100MHz" must not parse as 100 mH — the (?![a-zA-Z0-9]) guard prevents it.
+ self::assertNull($this->guesser->guess($this->part('Crystal oscillator 100MHz')));
+ }
+
+ public function testEdaSuggestionForInductor(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'inductor', 'package' => null, 'pitch' => null, 'diameter' => null]);
+ self::assertSame('Device:L', $eda['symbol']);
+ self::assertSame('L', $eda['reference']);
+ self::assertNull($eda['footprint']);
+ }
+
+ public function testClassifiesSmdInductorFromPackage(): void
+ {
+ $guess = $this->guesser->guess($this->part('Inductor 10µH 0805 SMD'));
+ self::assertNotNull($guess);
+ self::assertSame('smd_inductor', $guess['type']);
+ self::assertSame('0805', $guess['package']);
+ self::assertEqualsWithDelta(10e-6, $guess['value'], 1e-12);
+ }
+
+ public function testEdaSuggestionForSmdInductor(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'smd_inductor', 'package' => '1210', 'pitch' => null, 'diameter' => null]);
+ self::assertSame('Device:L', $eda['symbol']);
+ self::assertSame('L', $eda['reference']);
+ self::assertSame('Inductor_SMD:L_1210_3225Metric', $eda['footprint']);
+ }
+
+ /**
+ * @dataProvider diodeProvider
+ */
+ public function testClassifiesDiode(string $name, string $expectedSubtype): void
+ {
+ $guess = $this->guesser->guess($this->part($name));
+ self::assertNotNull($guess, "Expected '$name' to classify");
+ self::assertSame('diode', $guess['type']);
+ self::assertSame($expectedSubtype, $guess['subtype']);
+ }
+
+ public static function diodeProvider(): \Generator
+ {
+ yield 'led word' => ['LED red 5mm 20mA', 'led'];
+ yield 'light emitting' => ['Light-emitting diode green', 'led'];
+ yield 'zener word' => ['Zener diode 5.1V', 'zener'];
+ yield 'zener BZX family' => ['BZX55C5V1', 'zener'];
+ yield 'zener 1N47xx' => ['1N4733A', 'zener'];
+ yield 'schottky word' => ['Schottky barrier diode', 'schottky'];
+ yield 'schottky BAT family' => ['BAT54', 'schottky'];
+ yield 'schottky 1N58xx' => ['1N5819', 'schottky'];
+ yield 'tvs word' => ['TVS diode array', 'tvs'];
+ yield 'tvs SMBJ family' => ['SMBJ15A', 'tvs'];
+ yield 'generic rectifier' => ['Rectifier diode', 'diode'];
+ yield '1N4148 small signal' => ['1N4148 switching', 'diode'];
+ yield '1N4007 rectifier' => ['1N4007', 'diode'];
+ yield 'BAV family' => ['BAV99 dual', 'diode'];
+ }
+
+ public function testLedUsesEmissionColor(): void
+ {
+ $guess = $this->guesser->guess($this->part('LED blue 5mm'));
+ self::assertNotNull($guess);
+ self::assertSame('diode', $guess['type']);
+ self::assertSame('led', $guess['subtype']);
+ self::assertSame('#2f6db0', $guess['color']);
+ }
+
+ public function testZenerCarriesVoltage(): void
+ {
+ $guess = $this->guesser->guess($this->part('Zener diode 5.1V 0.5W'));
+ self::assertNotNull($guess);
+ self::assertSame('zener', $guess['subtype']);
+ self::assertSame(5, $guess['voltage']);
+ }
+
+ public function testResistorForLedStaysResistor(): void
+ {
+ //"220R" yields a resistance, which is classified before the diode fallback ever runs.
+ $guess = $this->guesser->guess($this->part('220R resistor for LED indicator'));
+ self::assertNotNull($guess);
+ self::assertSame('resistor', $guess['type']);
+ }
+
+ /**
+ * @dataProvider diodeEdaProvider
+ */
+ public function testEdaSuggestionForDiode(string $subtype, string $expectedSymbol): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'diode', 'subtype' => $subtype, 'package' => null, 'pitch' => null, 'diameter' => null]);
+ self::assertSame($expectedSymbol, $eda['symbol']);
+ self::assertSame('D', $eda['reference']);
+ self::assertNull($eda['footprint']);
+ }
+
+ public static function diodeEdaProvider(): \Generator
+ {
+ yield 'generic' => ['diode', 'Device:D'];
+ yield 'led' => ['led', 'Device:LED'];
+ yield 'zener' => ['zener', 'Device:D_Zener'];
+ yield 'schottky' => ['schottky', 'Device:D_Schottky'];
+ yield 'tvs' => ['tvs', 'Device:D_TVS'];
+ }
+
+ public function testEdaSuggestionForSmdDiodeFootprint(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'diode', 'subtype' => 'diode', 'package' => '0805', 'pitch' => null, 'diameter' => null]);
+ self::assertSame('Diode_SMD:D_0805_2012Metric', $eda['footprint']);
+ }
+
+ public function testEdaSuggestionForSmdLedFootprint(): void
+ {
+ $eda = $this->guesser->edaSuggestion(['type' => 'diode', 'subtype' => 'led', 'package' => '0805', 'pitch' => null, 'diameter' => null]);
+ self::assertSame('LED_SMD:LED_0805_2012Metric', $eda['footprint']);
+ }
+
+ public function testDetectsThtDiodePackageAndMarking(): void
+ {
+ //Real-world case: importing a 1N400x rectifier kit, whose names spell out the THT package.
+ $guess = $this->guesser->guess($this->part('1N4001 Rectifier Diode 1A 50V DO-41'));
+ self::assertNotNull($guess);
+ self::assertSame('diode', $guess['type']);
+ self::assertSame('diode', $guess['subtype']);
+ self::assertSame('DO-41', $guess['package']);
+ self::assertSame('1N4001', $guess['marking']);
+
+ $eda = $this->guesser->edaSuggestion($guess);
+ self::assertSame('Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal', $eda['footprint']);
+ }
+
+ public function testDetectsSotSchottkyPackage(): void
+ {
+ $guess = $this->guesser->guess($this->part('BAT54 Schottky diode SOT-23'));
+ self::assertNotNull($guess);
+ self::assertSame('schottky', $guess['subtype']);
+ self::assertSame('SOT-23', $guess['package']);
+ self::assertSame('BAT54', $guess['marking']);
+
+ $eda = $this->guesser->edaSuggestion($guess);
+ self::assertSame('Diode_SMD:D_SOT-23', $eda['footprint']);
+ }
+
+ public function testDetectsLedDomeSizeFootprint(): void
+ {
+ $guess = $this->guesser->guess($this->part('LED red 5mm diffused'));
+ self::assertNotNull($guess);
+ self::assertSame('led', $guess['subtype']);
+ self::assertSame('5MM', $guess['package']);
+ //LEDs aren't normally printed with a part number.
+ self::assertNull($guess['marking']);
+
+ $eda = $this->guesser->edaSuggestion($guess);
+ self::assertSame('LED_THT:LED_D5.0mm', $eda['footprint']);
+ }
+
+ public function testMarkingNullWhenNoRecognisablePartNumber(): void
+ {
+ $guess = $this->guesser->guess($this->part('Generic rectifier diode'));
+ self::assertNotNull($guess);
+ self::assertSame('diode', $guess['subtype']);
+ self::assertNull($guess['marking']);
+ }
+}
diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf
index d0099493..7b030df9 100644
--- a/translations/frontend.en.xlf
+++ b/translations/frontend.en.xlf
@@ -55,6 +55,126 @@
Go!
+
+
+ tools.value_calc.band
+ band
+
+
+
+
+ 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 d4d40359..c57dde00 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -12995,6 +12995,684 @@ Buerklin-API Authentication server:
Last stocktake
+
+
+ perm.tools.component_image_generator
+ Component image generator
+
+
+
+
+ tools.value_calc.title
+ Component image generator
+
+
+
+
+ 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 the value of common components — colour bands (resistors, inductors), capacitor codes and SMD chip markings. Read a part by picking its bands/code, or enter a value to generate the matching bands/code and a picture you can attach to the part.
+
+
+
+
+ tools.value_calc.body_color
+ Body color
+
+
+
+
+ tools.value_calc.appearance
+ Picture appearance (for the generated image)
+
+
+
+
+ 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
+
+
+
+
+ tools.value_calc.inductor.title
+ Inductor
+
+
+
+
+ tools.value_calc.inductor.result
+ Inductance
+
+
+
+
+ tools.value_calc.inductor.intro
+ Molded axial inductors use the same colour-band code as resistors, but the value is read in microhenries (µH). Pick the band colours, or type a value to set them.
+
+
+
+
+ tools.value_calc.inductor.from_value
+ Set from value
+
+
+
+
+ tools.value_calc.inductor.from_value_help
+ Enter an inductance to colour the bands automatically. A bare number is read as µH (e.g. 100 = 100µH); you can also write 100µH, 10mH or 470nH.
+
+
+
+
+ tools.value_calc.diode.title
+ Diode
+
+
+
+
+ tools.value_calc.smd_inductor.title
+ SMD inductor
+
+
+
+
+ tools.value_calc.smd_inductor.intro
+ Molded/shielded SMD power inductor — enter the inductance (e.g. 100µH, 10mH) to see its printed µH code.
+
+
+
+
+ tools.value_calc.smd_capacitor.title
+ SMD capacitor
+
+
+
+
+ tools.value_calc.smd_capacitor.intro
+ Surface-mount MLCC capacitors are almost always unmarked, so there's nothing to decode — just enter the value and package to generate a picture you can attach to the part.
+
+
+
+
+ tools.value_calc.body.brown
+ Brown
+
+
+
+
+ tools.value_calc.capacitor.title
+ Capacitor
+
+
+
+
+ tools.value_calc.smd.title
+ SMD resistor
+
+
+
+
+ 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