{% include "parts/info/_picture.html.twig" %}
+ {% include "tools/component_image_generator/_generate_modal.html.twig" %}
diff --git a/templates/tools/component_image_generator/_bare.html.twig b/templates/tools/component_image_generator/_bare.html.twig
new file mode 100644
index 000000000..207e7e159
--- /dev/null
+++ b/templates/tools/component_image_generator/_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/component_image_generator/_calculator_body.html.twig b/templates/tools/component_image_generator/_calculator_body.html.twig
new file mode 100644
index 000000000..712cdf804
--- /dev/null
+++ b/templates/tools/component_image_generator/_calculator_body.html.twig
@@ -0,0 +1,527 @@
+
+
+
{% 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/component_image_generator/_generate_modal.html.twig b/templates/tools/component_image_generator/_generate_modal.html.twig
new file mode 100644
index 000000000..0b5fed934
--- /dev/null
+++ b/templates/tools/component_image_generator/_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/component_image_generator/bulk_generate.html.twig b/templates/tools/component_image_generator/bulk_generate.html.twig
new file mode 100644
index 000000000..5fb9aa7ae
--- /dev/null
+++ b/templates/tools/component_image_generator/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/component_image_generator/_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/component_image_generator/image_generator.html.twig b/templates/tools/component_image_generator/image_generator.html.twig
new file mode 100644
index 000000000..bc0769ef9
--- /dev/null
+++ b/templates/tools/component_image_generator/image_generator.html.twig
@@ -0,0 +1,11 @@
+{% extends modalMode|default(false) ? 'tools/component_image_generator/_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/component_image_generator/_calculator_body.html.twig' %}
+{% endblock %}
diff --git a/tests/Controller/ValueCalculatorControllerTest.php b/tests/Controller/ValueCalculatorControllerTest.php
new file mode 100644
index 000000000..85ced8002
--- /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/Attachments/GeneratedImageAttachmentHelperTest.php b/tests/Services/Attachments/GeneratedImageAttachmentHelperTest.php
new file mode 100644
index 000000000..472549cac
--- /dev/null
+++ b/tests/Services/Attachments/GeneratedImageAttachmentHelperTest.php
@@ -0,0 +1,323 @@
+.
+ */
+
+namespace App\Tests\Services\Attachments;
+
+use App\Entity\Attachments\AttachmentType;
+use App\Entity\Attachments\AttachmentUpload;
+use App\Entity\Attachments\PartAttachment;
+use App\Entity\Parts\Part;
+use App\Repository\StructuralDBElementRepository;
+use App\Services\Attachments\AttachmentSubmitHandler;
+use App\Services\Attachments\GeneratedImageAttachmentHelper;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+final class GeneratedImageAttachmentHelperTest extends TestCase
+{
+ private EntityManagerInterface&MockObject $em;
+ private AttachmentSubmitHandler&MockObject $submitHandler;
+ private StructuralDBElementRepository&MockObject $repository;
+ private GeneratedImageAttachmentHelper $service;
+
+ protected function setUp(): void
+ {
+ $this->em = $this->createMock(EntityManagerInterface::class);
+ $this->submitHandler = $this->createMock(AttachmentSubmitHandler::class);
+ $this->repository = $this->createMock(StructuralDBElementRepository::class);
+
+ $this->em->method('getRepository')
+ ->with(AttachmentType::class)
+ ->willReturn($this->repository);
+
+ $this->service = new GeneratedImageAttachmentHelper($this->em, $this->submitHandler);
+ }
+
+ /**
+ * Configures the submit handler mock to behave like a successful upload of a picture file.
+ * Returns an ArrayObject that gets filled with the AttachmentUpload instances the handler
+ * was called with (in call order) as the test runs.
+ *
+ * @return \ArrayObject
+ */
+ private function mockSuccessfulPictureUpload(): \ArrayObject
+ {
+ $seenUploads = new \ArrayObject();
+ $this->submitHandler->method('handleUpload')
+ ->willReturnCallback(function (PartAttachment $attachment, ?AttachmentUpload $upload) use ($seenUploads) {
+ $seenUploads[] = $upload;
+ $attachment->setInternalPath('attachments/1/generated.svg');
+
+ return $attachment;
+ });
+
+ return $seenUploads;
+ }
+
+ private function setId(object $entity, ?int $id): void
+ {
+ $reflection = new \ReflectionClass($entity);
+ $reflection->getProperty('id')->setValue($entity, $id);
+ }
+
+ /**
+ * Builds a fresh AttachmentType named "Generated image" and makes it the result of
+ * getGeneratedImageType(). $persisted controls whether it already has an ID (i.e. already exists in DB).
+ */
+ private function mockGeneratedImageType(bool $persisted): AttachmentType
+ {
+ $type = new AttachmentType();
+ $type->setName('Generated image');
+ if ($persisted) {
+ $this->setId($type, 5);
+ }
+
+ $this->repository->method('findOrCreateForInfoProvider')
+ ->with('Generated image')
+ ->willReturn($type);
+
+ return $type;
+ }
+
+ public function testAttachSvgToPartCreatesAttachmentWithGivenNameAndType(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $uploads = $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+ $attachment = $this->service->attachSvgToPart($part, '', 'My resistor');
+
+ $this->assertSame('My resistor', $attachment->getName());
+ $this->assertSame('Generated image', $attachment->getAttachmentType()?->getName());
+ $this->assertTrue($part->getAttachments()->contains($attachment));
+
+ //The upload pipeline must receive the base64 encoded SVG data as a generated.svg file
+ $this->assertCount(1, $uploads);
+ $this->assertSame(base64_encode(''), $uploads[0]->data);
+ $this->assertSame('generated.svg', $uploads[0]->filename);
+ $this->assertNull($uploads[0]->file);
+ }
+
+ public function testAttachSvgToPartFallsBackToDefaultNameWhenNameIsEmpty(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+ $attachment = $this->service->attachSvgToPart($part, '', '');
+
+ $this->assertSame('Generated image', $attachment->getName());
+ }
+
+ public function testAttachSvgToPartSetsAsPreviewByDefault(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $uploads = $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+ $attachment = $this->service->attachSvgToPart($part, '', 'Preview me');
+
+ $this->assertTrue($uploads[0]->becomePreviewIfEmpty);
+ $this->assertSame($attachment, $part->getMasterPictureAttachment());
+ }
+
+ public function testAttachSvgToPartDoesNotSetPreviewWhenNotRequested(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $uploads = $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+ $this->service->attachSvgToPart($part, '', 'No preview', setAsPreview: false);
+
+ $this->assertFalse($uploads[0]->becomePreviewIfEmpty);
+ $this->assertNull($part->getMasterPictureAttachment());
+ }
+
+ public function testAttachSvgToPartPersistsTheNewAttachment(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $persisted = [];
+ $this->em->method('persist')->willReturnCallback(function (object $entity) use (&$persisted): void {
+ $persisted[] = $entity;
+ });
+
+ $part = new Part();
+ $attachment = $this->service->attachSvgToPart($part, '', 'Test');
+
+ $this->assertContains($attachment, $persisted);
+ }
+
+ public function testAttachSvgToPartDeduplicatesNameOnCollision(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+
+ $existing = new PartAttachment();
+ $existing->setName('Resistor');
+ $existing->setAttachmentType($type);
+ $part->addAttachment($existing);
+
+ $attachment = $this->service->attachSvgToPart($part, '', 'Resistor');
+
+ $this->assertSame('Resistor (2)', $attachment->getName());
+ }
+
+ public function testAttachSvgToPartDeduplicatesNameSkippingAlreadyTakenNumbers(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+
+ foreach (['Resistor', 'Resistor (2)'] as $name) {
+ $existing = new PartAttachment();
+ $existing->setName($name);
+ $existing->setAttachmentType($type);
+ $part->addAttachment($existing);
+ }
+
+ $attachment = $this->service->attachSvgToPart($part, '', 'Resistor');
+
+ $this->assertSame('Resistor (3)', $attachment->getName());
+ }
+
+ public function testAttachSvgToPartDoesNotDeduplicateAgainstOtherAttachmentTypes(): void
+ {
+ $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+
+ $otherType = new AttachmentType();
+ $otherType->setName('Datasheet');
+
+ $existing = new PartAttachment();
+ $existing->setName('Resistor');
+ $existing->setAttachmentType($otherType);
+ $part->addAttachment($existing);
+
+ $attachment = $this->service->attachSvgToPart($part, '', 'Resistor');
+
+ $this->assertSame('Resistor', $attachment->getName());
+ }
+
+ public function testAttachSvgToPartWithoutOverwriteKeepsPreviousGeneratedImages(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+ $previous = new PartAttachment();
+ $previous->setName('Generated image');
+ $previous->setAttachmentType($type);
+ $part->addAttachment($previous);
+
+ $this->em->expects($this->never())->method('remove');
+
+ $this->service->attachSvgToPart($part, '', 'Generated image', overwrite: false);
+
+ $this->assertTrue($part->getAttachments()->contains($previous));
+ }
+
+ public function testAttachSvgToPartWithOverwriteRemovesPreviousGeneratedImagesOnly(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: true);
+ $this->mockSuccessfulPictureUpload();
+
+ $part = new Part();
+
+ $previousGenerated = new PartAttachment();
+ $previousGenerated->setName('Generated image');
+ $previousGenerated->setAttachmentType($type);
+ $part->addAttachment($previousGenerated);
+ $part->setMasterPictureAttachment($previousGenerated);
+
+ $otherType = new AttachmentType();
+ $otherType->setName('Datasheet');
+ $unrelated = new PartAttachment();
+ $unrelated->setName('Datasheet');
+ $unrelated->setAttachmentType($otherType);
+ $part->addAttachment($unrelated);
+
+ $removed = [];
+ $this->em->method('remove')->willReturnCallback(function (object $entity) use (&$removed): void {
+ $removed[] = $entity;
+ });
+
+ $newAttachment = $this->service->attachSvgToPart($part, '', 'Generated image', overwrite: true);
+
+ $this->assertSame([$previousGenerated], $removed);
+ $this->assertFalse($part->getAttachments()->contains($previousGenerated));
+ $this->assertTrue($part->getAttachments()->contains($unrelated));
+ $this->assertTrue($part->getAttachments()->contains($newAttachment));
+ //The new attachment should have become the master picture, replacing the removed one
+ $this->assertSame($newAttachment, $part->getMasterPictureAttachment());
+ //Since the old master picture was removed, the name should not need de-duplication
+ $this->assertSame('Generated image', $newAttachment->getName());
+ }
+
+ public function testGetGeneratedImageTypeConfiguresAndPersistsNewlyCreatedType(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: false);
+ $this->mockSuccessfulPictureUpload();
+
+ $persisted = [];
+ $this->em->method('persist')->willReturnCallback(function (object $entity) use (&$persisted): void {
+ $persisted[] = $entity;
+ });
+
+ $part = new Part();
+ $this->service->attachSvgToPart($part, '', 'Test');
+
+ $this->assertSame('image/*', $type->getFiletypeFilter());
+ $this->assertSame('Generated image', $type->getAlternativeNames());
+ $this->assertContains($type, $persisted);
+ }
+
+ public function testGetGeneratedImageTypeDoesNotReconfigureOrPersistExistingType(): void
+ {
+ $type = $this->mockGeneratedImageType(persisted: true);
+ $type->setFiletypeFilter('');
+ $type->setAlternativeNames(null);
+ $this->mockSuccessfulPictureUpload();
+
+ $persisted = [];
+ $this->em->method('persist')->willReturnCallback(function (object $entity) use (&$persisted): void {
+ $persisted[] = $entity;
+ });
+
+ $part = new Part();
+ $this->service->attachSvgToPart($part, '', 'Test');
+
+ //Existing types must not be modified...
+ $this->assertSame('', $type->getFiletypeFilter());
+ $this->assertNull($type->getAlternativeNames());
+ //...nor persisted again (only the new attachment should be persisted)
+ $this->assertNotContains($type, $persisted);
+ }
+}
diff --git a/tests/Services/Tools/ComponentValueGuesserTest.php b/tests/Services/Tools/ComponentValueGuesserTest.php
new file mode 100644
index 000000000..f76671de9
--- /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.de.xlf b/translations/frontend.de.xlf
index 317b7c7fd..cd1aa5156 100644
--- a/translations/frontend.de.xlf
+++ b/translations/frontend.de.xlf
@@ -79,5 +79,125 @@
Nein
+
+
+ tools.value_calc.band
+ Ring
+
+
+
+
+ tools.value_calc.resistor.band_digit
+ Ziffer
+
+
+
+
+ tools.value_calc.resistor.band_multiplier
+ Multiplikator
+
+
+
+
+ tools.value_calc.resistor.band_tolerance
+ Toleranz
+
+
+
+
+ tools.value_calc.resistor.band_temp
+ Temp.-Koeffizient
+
+
+
+
+ tools.value_calc.color.black
+ Schwarz
+
+
+
+
+ tools.value_calc.color.brown
+ Braun
+
+
+
+
+ tools.value_calc.color.red
+ Rot
+
+
+
+
+ tools.value_calc.color.orange
+ Orange
+
+
+
+
+ tools.value_calc.color.yellow
+ Gelb
+
+
+
+
+ tools.value_calc.color.green
+ Grün
+
+
+
+
+ tools.value_calc.color.blue
+ Blau
+
+
+
+
+ tools.value_calc.color.violet
+ Violett
+
+
+
+
+ tools.value_calc.color.grey
+ Grau
+
+
+
+
+ tools.value_calc.color.white
+ Weiß
+
+
+
+
+ tools.value_calc.color.gold
+ Gold
+
+
+
+
+ tools.value_calc.color.silver
+ Silber
+
+
+
+
+ tools.value_calc.invalid_input
+ Ungültige Eingabe
+
+
+
+
+ tools.value_calc.tolerance
+ Toleranz
+
+
+
+
+ tools.value_calc.attach.nothing
+ Bitte generieren Sie zuerst ein Bild.
+
+
diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf
index d00994931..212e7f05b 100644
--- a/translations/frontend.en.xlf
+++ b/translations/frontend.en.xlf
@@ -55,25 +55,145 @@
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
Estimated time to crack: %time%
-
+
dialog.btn.ok
OK
-
+
dialog.btn.cancel
Cancel
-
+
dialog.btn.deny
No
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index b09f5e98b..ad405a000 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -14259,5 +14259,689 @@ Buerklin-API-Authentication-Server:
OAuth2-Client erfolgreich aktualisiert!
+
+
+ perm.tools.component_image_generator
+ Bauteilbild-Generator
+
+
+
+
+ tools.value_calc.title
+ Bauteilbild-Generator
+
+
+
+
+ tools.bulk_gen.title
+ Bauteilbilder im Stapel generieren
+
+
+
+
+ tools.bulk_gen.intro
+ Ausgewählte Bauteile, die noch kein Bild haben und wie ein Widerstand, SMD-Widerstand oder Kondensator aussehen, werden unten aufgelistet. Überprüfen Sie den automatisch erkannten Typ und Wert und generieren und hängen Sie dann die Bilder in einem Schritt an.
+
+
+
+
+ tools.bulk_gen.pick_location
+ — Lagerort auswählen —
+
+
+
+
+ tools.bulk_gen.load
+ Bauteile laden
+
+
+
+
+ tools.bulk_gen.none
+ Keines der %count% ausgewählten Bauteile konnte als Widerstand, SMD-Widerstand oder Kondensator ohne vorhandenes Bild erkannt werden.
+
+
+
+
+ tools.bulk_gen.found
+ Bauteil(e) bereit zur Bildgenerierung.
+
+
+
+
+ tools.bulk_gen.part
+ Bauteil
+
+
+
+
+ tools.bulk_gen.type
+ Erkannter Typ
+
+
+
+
+ tools.bulk_gen.value
+ Wert
+
+
+
+
+ tools.bulk_gen.wip
+ Überprüfen Sie hier die erkannten Typen und Werte — das Generieren und Anhängen der Bilder ist der nächste Schritt.
+
+
+
+
+ tools.bulk_gen.no_selection
+ Es wurden keine Bauteile ausgewählt. Wählen Sie Bauteile in einer Liste aus und wählen Sie die Aktion "Bauteilbilder generieren".
+
+
+
+
+ tools.bulk_gen.skipped
+ übersprungen: haben bereits ein Bild oder sind nicht klassifizierbar
+
+
+
+
+ tools.bulk_gen.preview
+ Vorschau
+
+
+
+
+ tools.bulk_gen.appearance
+ Bild-Einstellungen
+
+
+
+
+ tools.bulk_gen.apply_all
+ Auf alle Zeilen anwenden
+
+
+
+
+ tools.bulk_gen.back
+ Zurück zu den Bauteilen
+
+
+
+
+ tools.bulk_gen.with_picture
+ %count% ausgewählte(s) Bauteil(e) haben bereits ein Bild und wurden nicht aufgelistet.
+
+
+
+
+ tools.bulk_gen.regenerate
+ Diese %count% neu generieren / überschreiben
+
+
+
+
+ tools.bulk_gen.overwrite_mode
+ Überschreibmodus: Bauteile, die bereits ein Bild haben, werden einbezogen (markiert mit "hat Bild"). Das Generieren ersetzt ein zuvor generiertes Bild und wird zur Vorschau — manuell hochgeladene Bilder bleiben erhalten.
+
+
+
+
+ tools.bulk_gen.has_picture_hint
+ Dieses Bauteil hat bereits ein Bild. Das Generieren setzt das neue Bild als Vorschau und ersetzt ein zuvor generiertes Bild; hochgeladene Fotos bleiben erhalten.
+
+
+
+
+ tools.bulk_gen.overwrite_exit
+ Nur Bauteile ohne Bild
+
+
+
+
+ tools.bulk_gen.has_picture
+ hat Bild
+
+
+
+
+ tools.bulk_gen.help.title
+ So erhalten Sie die beste automatische Erkennung — was in die Details eines Bauteils gehört
+
+
+
+
+ tools.bulk_gen.help.intro
+ Jedes Feld wird zuerst aus den Parametern des Bauteils gelesen, dann aus seinem Namen und der Beschreibung. Fügen Sie eines der unten genannten hinzu, um die Erkennung zu verbessern. Ein Bauteil wird hier nur aufgelistet, wenn es noch kein Bild hat und sein Wert gelesen werden kann.
+
+
+
+
+ tools.bulk_gen.help.col_field
+ Feld
+
+
+
+
+ tools.bulk_gen.help.col_param
+ Parameter mit dem Namen hinzufügen…
+
+
+
+
+ tools.bulk_gen.help.col_text
+ …oder im Namen / der Beschreibung angeben
+
+
+
+
+ tools.bulk_gen.help.value
+ Wert (zur Klassifizierung erforderlich)
+
+
+
+
+ tools.bulk_gen.help.voltage
+ Nennspannung (Kondensatoren)
+
+
+
+
+ tools.bulk_gen.help.tolerance
+ Toleranz
+
+
+
+
+ tools.bulk_gen.help.pitch
+ Anschlussabstand (Kondensatoren)
+
+
+
+
+ tools.bulk_gen.help.diameter
+ Gehäusedurchmesser (Kondensatoren)
+
+
+
+
+ tools.bulk_gen.help.smd
+ SMD-Größe (Widerstände → Footprint)
+
+
+
+
+ tools.bulk_gen.help.footprint_col
+ der zugewiesene Footprint
+
+
+
+
+ tools.bulk_gen.kicad
+ KiCad
+
+
+
+
+ tools.bulk_gen.write_eda
+ KiCad-Einstellungen für ausgewählte Bauteile schreiben
+
+
+
+
+ tools.bulk_gen.eda_written
+ EDA-Einstellungen geschrieben
+
+
+
+
+ tools.bulk_gen.footprint_ph
+ Footprint (optional)
+
+
+
+
+ tools.bulk_gen.reference
+ Referenzpräfix
+
+
+
+
+ tools.bulk_gen.kicad_hint
+ Vorgeschlagene Werte — bearbeiten Sie jedes Feld vor dem Schreiben.
+
+
+
+
+ tools.bulk_gen.generate_attach
+ Bilder an ausgewählte Bauteile anhängen
+
+
+
+
+ tools.bulk_gen.attached
+ angehängt
+
+
+
+
+ tools.bulk_gen.failed
+ fehlgeschlagen
+
+
+
+
+ part_list.action.group.images
+ Bilder
+
+
+
+
+ part_list.action.generate_images
+ Bauteilbilder generieren
+
+
+
+
+ tools.value_calc.explanation
+ Entschlüsseln und visualisieren Sie den Wert gängiger Bauteile — Farbringe (Widerstände, Induktivitäten), Kondensatorcodes und SMD-Chip-Beschriftungen. Lesen Sie ein Bauteil, indem Sie seine Ringe/seinen Code auswählen, oder geben Sie einen Wert ein, um die passenden Ringe/den Code sowie ein Bild zu erzeugen, das Sie an das Bauteil anhängen können.
+
+
+
+
+ tools.value_calc.body_color
+ Gehäusefarbe
+
+
+
+
+ tools.value_calc.appearance
+ Bild-Einstellungen (für das generierte Bild)
+
+
+
+
+ tools.value_calc.body.beige
+ Beige
+
+
+
+
+ tools.value_calc.body.blue
+ Blau
+
+
+
+
+ tools.value_calc.body.green
+ Grün
+
+
+
+
+ tools.value_calc.body.lightblue
+ Hellblau
+
+
+
+
+ tools.value_calc.body.tan
+ Hautfarben
+
+
+
+
+ tools.value_calc.body.red
+ Rot
+
+
+
+
+ tools.value_calc.body.purple
+ Lila
+
+
+
+
+ tools.value_calc.body.black
+ Schwarz
+
+
+
+
+ tools.value_calc.body.grey
+ Grau
+
+
+
+
+ tools.value_calc.body.white
+ Weiß
+
+
+
+
+ tools.value_calc.resistor.title
+ Widerstand
+
+
+
+
+ tools.value_calc.inductor.title
+ Induktivität
+
+
+
+
+ tools.value_calc.inductor.result
+ Induktivität
+
+
+
+
+ tools.value_calc.inductor.intro
+ Bedrahtete axiale Induktivitäten verwenden denselben Farbring-Code wie Widerstände, aber der Wert wird in Mikrohenry (µH) angegeben. Wählen Sie die Ringfarben oder geben Sie einen Wert ein, um sie automatisch zu setzen.
+
+
+
+
+ tools.value_calc.inductor.from_value
+ Aus Wert setzen
+
+
+
+
+ tools.value_calc.inductor.from_value_help
+ Geben Sie eine Induktivität ein, um die Ringe automatisch einzufärben. Eine reine Zahl wird als µH gelesen (z.B. 100 = 100µH); Sie können auch 100µH, 10mH oder 470nH schreiben.
+
+
+
+
+ tools.value_calc.diode.title
+ Diode
+
+
+
+
+ tools.value_calc.smd_inductor.title
+ SMD-Induktivität
+
+
+
+
+ tools.value_calc.smd_inductor.intro
+ Umspritzte/abgeschirmte SMD-Leistungsinduktivität — geben Sie die Induktivität ein (z.B. 100µH, 10mH), um ihren aufgedruckten µH-Code zu sehen.
+
+
+
+
+ tools.value_calc.smd_capacitor.title
+ SMD-Kondensator
+
+
+
+
+ tools.value_calc.smd_capacitor.intro
+ Oberflächenmontierte MLCC-Kondensatoren sind fast immer unbeschriftet, es gibt also nichts zu entschlüsseln — geben Sie einfach Wert und Gehäuse ein, um ein Bild zu erzeugen, das Sie an das Bauteil anhängen können.
+
+
+
+
+ tools.value_calc.body.brown
+ Braun
+
+
+
+
+ tools.value_calc.capacitor.title
+ Kondensator
+
+
+
+
+ tools.value_calc.smd.title
+ SMD-Widerstand
+
+
+
+
+ tools.value_calc.resistor.bands
+ Anzahl der Ringe
+
+
+
+
+ tools.value_calc.resistor.bands_4
+ 4 Ringe
+
+
+
+
+ tools.value_calc.resistor.bands_5
+ 5 Ringe
+
+
+
+
+ tools.value_calc.resistor.bands_6
+ 6 Ringe
+
+
+
+
+ tools.value_calc.attach.context
+ Das generierte Bild kann an das Bauteil "%part%" angehängt werden. Wählen Sie unten ein Bauteil aus und klicken Sie auf "An Bauteil anhängen".
+
+
+
+
+ tools.value_calc.attach.as_preview
+ Als Vorschaubild verwenden
+
+
+
+
+ tools.value_calc.attach.button
+ An Bauteil anhängen
+
+
+
+
+ tools.value_calc.download.button
+ SVG herunterladen
+
+
+
+
+ tools.value_calc.attach.generate_button
+ Bauteilbild generieren
+
+
+
+
+ tools.value_calc.attach.generate_hint
+ Generieren Sie ein Bild für Widerstand, Kondensator oder SMD und hängen Sie es an dieses Bauteil an.
+
+
+
+
+ part.generate_image.flash.success
+ Das generierte Bild wurde an das Bauteil angehängt.
+
+
+
+
+ part.generate_image.flash.invalid
+ Es wurde kein gültiges Bild generiert. Bitte versuchen Sie es erneut.
+
+
+
+
+ tools.value_calc.size
+ Größe
+
+
+
+
+ tools.value_calc.resistor.power
+ Leistung
+
+
+
+
+ tools.value_calc.smd.package
+ Gehäusegröße
+
+
+
+
+ tools.value_calc.cap.diameter
+ Durchmesser
+
+
+
+
+ tools.value_calc.cap.pitch
+ Anschlussabstand
+
+
+
+
+ tools.value_calc.cap.voltage
+ Spannung
+
+
+
+
+ tools.value_calc.cap.shape
+ Form
+
+
+
+
+ tools.value_calc.cap.shape.disc
+ Scheibe
+
+
+
+
+ tools.value_calc.cap.shape.blob
+ Tropfen (MLCC)
+
+
+
+
+ tools.value_calc.cap.lead
+ Anschlussdrähte
+
+
+
+
+ tools.value_calc.cap.lead.short
+ Kurz
+
+
+
+
+ tools.value_calc.cap.lead.medium
+ Mittel
+
+
+
+
+ tools.value_calc.cap.lead.long
+ Lang
+
+
+
+
+ tools.value_calc.resistor.result
+ Widerstand
+
+
+
+
+ tools.value_calc.resistor.from_value_help
+ Geben Sie einen Widerstandswert ein, um die Farbringe zu erzeugen. Sie können Suffixe wie k, M und die RKM-Notation verwenden (z.B. 4k7).
+
+
+
+
+ tools.value_calc.resistor.from_value
+ Wert für Farbcode
+
+
+
+
+ tools.value_calc.resistor.apply_value
+ Ringe generieren
+
+
+
+
+ tools.value_calc.capacitor.intro
+ Funktioniert für die aufgedruckten Zahlencodes auf Keramik- (MLCC), Folien- und ähnlichen Kondensatoren. Kleine Kondensatoren unter 100 pF sind meist direkt aufgedruckt (z.B. 47 oder 4R7), größere verwenden den 3-stelligen Code (z.B. 104 = 100 nF).
+
+
+
+
+ tools.value_calc.smd.on_chip_help
+ Bearbeiten Sie ein beliebiges Feld, um die anderen zu aktualisieren. Verwenden Sie die Radiobuttons, um auszuwählen, welcher Code auf dem Chip aufgedruckt ist.
+
+
+
+
+ tools.value_calc.field.value
+ Wert
+
+
+
+
+ tools.value_calc.field.code
+ Code
+
+
+
+
+ tools.value_calc.field.tolerance
+ Toleranz
+
+
+
+
+ tools.value_calc.field.power
+ Leistung
+
+
+
+
+ tools.value_calc.field.ppm
+ Temp.-Koeffizient (ppm/K)
+
+
+
+
+ tools.value_calc.field.code3
+ 3-stellig
+
+
+
+
+ tools.value_calc.field.code4
+ 4-stellig
+
+
+
+
+ tools.value_calc.field.eia96
+ EIA-96
+
+
+
+
+ tools.value_calc.field.on_chip
+ Auf dem Chip aufgedruckt
+
+
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index fa3998b54..a264201d4 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -13025,6 +13025,690 @@ 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.help.footprint_col
+ the assigned 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.footprint_ph
+ Footprint (optional)
+
+
+
+
+ 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.download.button
+ Download SVG
+
+
+
+
+ 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