diff --git a/.docker/symfony.conf b/.docker/symfony.conf index de87ceb4..9569f80c 100644 --- a/.docker/symfony.conf +++ b/.docker/symfony.conf @@ -42,6 +42,7 @@ PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE + PassEnv EDA_KICAD_CATEGORY_DEPTH # For most configuration files from conf-available/, which are # enabled or disabled at a global level, it is possible to diff --git a/.env b/.env index c1a3d63c..47919cb7 100644 --- a/.env +++ b/.env @@ -159,6 +159,15 @@ PROVIDER_MOUSER_SEARCH_LIMIT=50 # Used when searching for keywords in the language specified when you signed up for Search API. PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true' +################################################################################## +# EDA integration related settings +################################################################################## + +# This value determines the depth of the category tree, that is visible inside KiCad +# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels. +# Set to -1, to show all parts of Part-DB inside a single category in KiCad +EDA_KICAD_CATEGORY_DEPTH=0 + ################################################################################### # SAML Single sign on-settings ################################################################################### diff --git a/config/parameters.yaml b/config/parameters.yaml index 8c2bad17..596492eb 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -142,3 +142,4 @@ parameters: env(HISTORY_SAVE_REMOVED_DATA): 1 env(HISTORY_SAVE_NEW_DATA): 1 + env(EDA_KICAD_CATEGORY_DEPTH): 0 diff --git a/config/services.yaml b/config/services.yaml index 44831820..ccfe14a0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -316,6 +316,13 @@ services: $global_locale: '%partdb.locale%' $global_timezone: '%partdb.timezone%' + #################################################################################################################### + # EDA system + #################################################################################################################### + App\Services\EDA\KiCadHelper: + arguments: + $category_depth: '%env(int:EDA_KICAD_CATEGORY_DEPTH)%' + #################################################################################################################### # Symfony overrides #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index 7a5daa53..1796b7df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -128,6 +128,14 @@ then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAV * `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not contain sensitive information, but could confuse end-users. +### EDA related settings + +* `EDA_KICAD_CATEGORY_DEPTH`: A number, which determines how many levels of Part-DB categories should be shown inside KiCad. + All parts in the selected category and all subcategories are shown in KiCad. + For performance reason this value should not be too high. The default is 0, which means that only the top level categories are shown in KiCad. + All parts in the selected category and all subcategories are shown in KiCad. Set this to a higher value, if you want to show more categories in KiCad. + When you set this value to -1, all parts are shown inside a single category in KiCad. + ### SAML SSO settings The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to log in to diff --git a/docs/usage/eda_integration.md b/docs/usage/eda_integration.md index 55d0a770..3bbee4da 100644 --- a/docs/usage/eda_integration.md +++ b/docs/usage/eda_integration.md @@ -25,7 +25,8 @@ You require a user account in Part-DB, which has the permission to access Part-D To connect KiCad with Part-DB do following steps: 1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently KiCAD can only read Part-DB database, so a token with read only scope is enough. -2. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content: +2. Add some EDA metadata to parts, categories or footprints. Only parts with useable info will show up in KiCad. See below for more info. +3. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content: ``` { "meta": { @@ -41,11 +42,11 @@ To connect KiCad with Part-DB do following steps: } } ``` -3. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel. -4. Replace the `token` field value with the token you have generated in step 1. -5. Open KiCad and add this created file as library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries) +4. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel. +5. Replace the `token` field value with the token you have generated in step 1. +6. Open KiCad and add this created file as library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries) -If you then place a new part, the library dialog opens and you should be able to see the categories and parts from Part-DB. +If you then place a new part, the library dialog opens, and you should be able to see the categories and parts from Part-DB. ### How to associate footprints and symbols with parts @@ -55,4 +56,24 @@ You can define this on a per-part basis using the KiCad symbol and KiCad footpri For example to configure the values for an BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew. -If you type in a character, you will get an autocomplete list of all symbols and footprints available in the kicad standard library. You can also input your own value. \ No newline at end of file +If you type in a character, you will get an autocomplete list of all symbols and footprints available in the kicad standard library. You can also input your own value. + +### Parts and category visibility + +Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad, +you need to define at least a symbol, footprint, reference prefix or value on a part, category or footprint. + +You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad. + +*Please note that KiCad caches the library categories. So if you change something, which would change the visibile categories in KiCad, you have to reload EEschema to see the changes.* + +### Category depth in KiCad + +For performance reasons, only the most top level categories of Part-DB are shown as categories in KiCad. All parts in the subcategories are shown in the top level category. + +You can configure the depth of the categories shown in KiCad, via the `EDA_KICAD_CATEGORY_DEPTH` env option. The default value is 0, which meabs only the top level categories are shown. +To show more levels of categories, you can set this value to a higher number. + +If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories. + +You can view the "real" category path of a part in the part details dialog in KiCad. \ No newline at end of file diff --git a/src/Controller/KiCadApiController.php b/src/Controller/KiCadApiController.php index cd63e627..14548b7b 100644 --- a/src/Controller/KiCadApiController.php +++ b/src/Controller/KiCadApiController.php @@ -25,7 +25,7 @@ namespace App\Controller; use App\Entity\Parts\Category; use App\Entity\Parts\Part; -use App\Services\EDAIntegration\KiCADHelper; +use App\Services\EDA\KiCadHelper; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -36,7 +36,7 @@ use Symfony\Component\Routing\Annotation\Route; class KiCadApiController extends AbstractController { public function __construct( - private readonly KiCADHelper $kiCADHelper, + private readonly KiCadHelper $kiCADHelper, ) { } @@ -62,9 +62,13 @@ class KiCadApiController extends AbstractController } #[Route('/parts/category/{category}.json', name: 'kicad_api_category')] - public function categoryParts(Category $category): Response + public function categoryParts(?Category $category): Response { - $this->denyAccessUnlessGranted('read', $category); + if ($category) { + $this->denyAccessUnlessGranted('read', $category); + } else { + $this->denyAccessUnlessGranted('@categories.read'); + } $this->denyAccessUnlessGranted('@parts.read'); return $this->json($this->kiCADHelper->getCategoryParts($category)); diff --git a/src/Entity/EDA/EDACategoryInfo.php b/src/Entity/EDA/EDACategoryInfo.php index db56485a..61d99988 100644 --- a/src/Entity/EDA/EDACategoryInfo.php +++ b/src/Entity/EDA/EDACategoryInfo.php @@ -38,10 +38,10 @@ class EDACategoryInfo #[Groups(['full', 'category:read', 'category:write'])] private ?string $reference_prefix = null; - /** @var bool|null If this is true, then this part is invisible for the EDA software */ - #[Column(type: Types::BOOLEAN, nullable: true)] + /** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */ + #[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility #[Groups(['full', 'category:read', 'category:write'])] - private ?bool $invisible = null; + private ?bool $visibility = null; /** @var bool|null If this is set to true, then this part will be excluded from the BOM */ #[Column(type: Types::BOOLEAN, nullable: true)] @@ -74,14 +74,14 @@ class EDACategoryInfo return $this; } - public function getInvisible(): ?bool + public function getVisibility(): ?bool { - return $this->invisible; + return $this->visibility; } - public function setInvisible(?bool $invisible): EDACategoryInfo + public function setVisibility(?bool $visibility): EDACategoryInfo { - $this->invisible = $invisible; + $this->visibility = $visibility; return $this; } diff --git a/src/Entity/EDA/EDAPartInfo.php b/src/Entity/EDA/EDAPartInfo.php index e314d580..5742921a 100644 --- a/src/Entity/EDA/EDAPartInfo.php +++ b/src/Entity/EDA/EDAPartInfo.php @@ -43,10 +43,10 @@ class EDAPartInfo #[Groups(['full', 'eda_info:read', 'eda_info:write'])] private ?string $value = null; - /** @var bool|null If this is true, then this part is invisible for the EDA software */ - #[Column(type: Types::BOOLEAN, nullable: true)] + /** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */ + #[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility #[Groups(['full', 'eda_info:read', 'eda_info:write'])] - private ?bool $invisible = null; + private ?bool $visibility = null; /** @var bool|null If this is set to true, then this part will be excluded from the BOM */ #[Column(type: Types::BOOLEAN, nullable: true)] @@ -100,14 +100,14 @@ class EDAPartInfo return $this; } - public function getInvisible(): ?bool + public function getVisibility(): ?bool { - return $this->invisible; + return $this->visibility; } - public function setInvisible(?bool $invisible): EDAPartInfo + public function setVisibility(?bool $visibility): EDAPartInfo { - $this->invisible = $invisible; + $this->visibility = $visibility; return $this; } diff --git a/src/Form/Part/EDA/EDACategoryInfoType.php b/src/Form/Part/EDA/EDACategoryInfoType.php index 3e60913e..24fe6717 100644 --- a/src/Form/Part/EDA/EDACategoryInfoType.php +++ b/src/Form/Part/EDA/EDACategoryInfoType.php @@ -45,8 +45,9 @@ class EDACategoryInfoType extends AbstractType ] ] ) - ->add('invisible', TriStateCheckboxType::class, [ - 'label' => 'eda_info.invisible', + ->add('visibility', TriStateCheckboxType::class, [ + 'help' => 'eda_info.visibility.help', + 'label' => 'eda_info.visibility', ]) ->add('exclude_from_bom', TriStateCheckboxType::class, [ 'label' => 'eda_info.exclude_from_bom', diff --git a/src/Form/Part/EDA/EDAPartInfoType.php b/src/Form/Part/EDA/EDAPartInfoType.php index 2e75d9e8..e8cac681 100644 --- a/src/Form/Part/EDA/EDAPartInfoType.php +++ b/src/Form/Part/EDA/EDAPartInfoType.php @@ -50,8 +50,9 @@ class EDAPartInfoType extends AbstractType 'placeholder' => t('eda_info.value.placeholder'), ] ]) - ->add('invisible', TriStateCheckboxType::class, [ - 'label' => 'eda_info.invisible', + ->add('visibility', TriStateCheckboxType::class, [ + 'help' => 'eda_info.visibility.help', + 'label' => 'eda_info.visibility', ]) ->add('exclude_from_bom', TriStateCheckboxType::class, [ 'label' => 'eda_info.exclude_from_bom', diff --git a/src/Repository/AbstractPartsContainingRepository.php b/src/Repository/AbstractPartsContainingRepository.php index 3a389610..e3c7f610 100644 --- a/src/Repository/AbstractPartsContainingRepository.php +++ b/src/Repository/AbstractPartsContainingRepository.php @@ -64,6 +64,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo return $this->getPartsCountRecursiveWithDepthN($element, self::RECURSION_LIMIT); } + public function getPartsRecursive(AbstractPartsContainingDBElement $element): array + { + return $this->getPartsRecursiveWithDepthN($element, self::RECURSION_LIMIT); + } + /** * The implementation of the recursive function to get the parts count. * This function is used to limit the recursion depth (remaining_depth is decreased on each call). @@ -91,6 +96,23 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo return $count; } + protected function getPartsRecursiveWithDepthN(AbstractPartsContainingDBElement $element, int $remaining_depth): array + { + if ($remaining_depth <= 0) { + throw new \RuntimeException('Recursion limit reached!'); + } + + //Add direct parts + $parts = $this->getParts($element); + + //Then iterate over all children and add their parts + foreach ($element->getChildren() as $child) { + $parts = array_merge($parts, $this->getPartsRecursiveWithDepthN($child, $remaining_depth - 1)); + } + + return $parts; + } + protected function getPartsByField(object $element, array $order_by, string $field_name): array { if (!$element instanceof AbstractPartsContainingDBElement) { diff --git a/src/Services/EDAIntegration/KiCADHelper.php b/src/Services/EDA/KiCadHelper.php similarity index 57% rename from src/Services/EDAIntegration/KiCADHelper.php rename to src/Services/EDA/KiCadHelper.php index 5e8d21a6..f50e52cd 100644 --- a/src/Services/EDAIntegration/KiCADHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -21,19 +21,21 @@ declare(strict_types=1); -namespace App\Services\EDAIntegration; +namespace App\Services\EDA; use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class KiCADHelper +class KiCadHelper { public function __construct( @@ -43,6 +45,8 @@ class KiCADHelper private readonly ElementCacheTagGenerator $tagGenerator, private readonly UrlGeneratorInterface $urlGenerator, private readonly TranslatorInterface $translator, + /** The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */ + private readonly int $category_depth, ) { } @@ -55,23 +59,48 @@ class KiCADHelper */ public function getCategories(): array { - return $this->kicadCache->get('kicad_categories', function (ItemInterface $item) { + return $this->kicadCache->get('kicad_categories_' . $this->category_depth, function (ItemInterface $item) { //Invalidate the cache on category changes $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class); $item->tag($secure_class_name); + //If the category depth is smaller than 0, create only one dummy category + if ($this->category_depth < 0) { + return [ + [ + 'id' => '0', + 'name' => 'Part-DB', + ] + ]; + } + + //Otherwise just get the categories and filter them + $categories = $this->nodesListBuilder->typeToNodesList(Category::class); $repo = $this->em->getRepository(Category::class); $result = []; foreach ($categories as $category) { //Skip invisible categories - if ($category->getEdaInfo()->getInvisible() ?? false) { + if ($category->getEdaInfo()->getVisibility() === false) { + continue; + } + + //Skip categories with a depth greater than the configured one + if ($category->getLevel() > $this->category_depth) { continue; } - /** @var $category Category */ //Ensure that the category contains parts - if ($repo->getPartsCount($category) < 1) { + //For the last level, we need to use a recursive query, otherwise we can use a simple query + /** @var Category $category */ + $parts_count = $category->getLevel() >= $this->category_depth ? $repo->getPartsCountRecursive($category) : $repo->getPartsCount($category); + + if ($parts_count < 1) { + continue; + } + + //Check if the category should be visible + if (!$this->shouldCategoryBeVisible($category)) { continue; } @@ -89,25 +118,43 @@ class KiCADHelper /** * Returns an array of objects containing all parts for the given category in the format required by KiCAD. * The result is cached for performance and invalidated on category or part changes. - * @param Category $category + * @param Category|null $category * @return array */ - public function getCategoryParts(Category $category): array + public function getCategoryParts(?Category $category): array { - return $this->kicadCache->get('kicad_category_parts_'.$category->getID(), + return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth, function (ItemInterface $item) use ($category) { $item->tag([ $this->tagGenerator->getElementTypeCacheTag(Category::class), - $this->tagGenerator->getElementTypeCacheTag(Part::class) + $this->tagGenerator->getElementTypeCacheTag(Part::class), + //Visibility can change based on the footprint + $this->tagGenerator->getElementTypeCacheTag(Footprint::class) ]); - $category_repo = $this->em->getRepository(Category::class); - $parts = $category_repo->getParts($category); + if ($this->category_depth >= 0) { + //Ensure that the category is set + if (!$category) { + throw new NotFoundHttpException('Category must be set, if category_depth is greater than 1!'); + } + + $category_repo = $this->em->getRepository(Category::class); + if ($category->getLevel() >= $this->category_depth) { + //Get all parts for the category and its children + $parts = $category_repo->getPartsRecursive($category); + } else { + //Get only direct parts for the category (without children), as the category is not collapsed + $parts = $category_repo->getParts($category); + } + } else { + //Get all parts + $parts = $this->em->getRepository(Part::class)->findAll(); + } $result = []; foreach ($parts as $part) { //If the part is invisible, then skip it - if ($part->getEdaInfo()->getInvisible() ?? $part->getCategory()?->getEdaInfo()->getInvisible() ?? false) { + if (!$this->shouldPartBeVisible($part)) { continue; } @@ -186,6 +233,91 @@ class KiCADHelper return $result; } + /** + * Determine if the given part should be visible for the EDA. + * @param Category $category + * @return bool + */ + private function shouldCategoryBeVisible(Category $category): bool + { + $eda_info = $category->getEdaInfo(); + + //If the category visibility is explicitly set, then use it + if ($eda_info->getVisibility() !== null) { + return $eda_info->getVisibility(); + } + + //try to check if the fields were set + if ($eda_info->getKicadSymbol() !== null + || $eda_info->getReferencePrefix() !== null) { + return true; + } + + //Check if there is any part in this category, which should be visible + $category_repo = $this->em->getRepository(Category::class); + if ($category->getLevel() >= $this->category_depth) { + //Get all parts for the category and its children + $parts = $category_repo->getPartsRecursive($category); + } else { + //Get only direct parts for the category (without children), as the category is not collapsed + $parts = $category_repo->getParts($category); + } + + foreach ($parts as $part) { + if ($this->shouldPartBeVisible($part)) { + return true; + } + } + + //Otherwise the category should be not visible + return false; + } + + /** + * Determine if the given part should be visible for the EDA. + * @param Part $part + * @return bool + */ + private function shouldPartBeVisible(Part $part): bool + { + $eda_info = $part->getEdaInfo(); + $category = $part->getCategory(); + + //If the user set a visibility, then use it + if ($eda_info->getVisibility() !== null) { + return $part->getEdaInfo()->getVisibility(); + } + + //If the part has a category, then use the category visibility if possible + if ($category && $category->getEdaInfo()->getVisibility() !== null) { + return $category->getEdaInfo()->getVisibility(); + } + + //If both are null, then we try to determine the visibility based on if fields are set + if ($eda_info->getKicadSymbol() !== null + || $eda_info->getKicadFootprint() !== null + || $eda_info->getReferencePrefix() !== null + || $eda_info->getValue() !== null) { + return true; + } + + //Check also if the fields are set for the category (if it exists) + if ($category && ( + $category->getEdaInfo()->getKicadSymbol() !== null + || $category->getEdaInfo()->getReferencePrefix() !== null + )) { + return true; + } + + //And on the footprint + if ($part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null) { + return true; + } + + //Otherwise the part should be not visible + return false; + } + /** * Converts a boolean value to the format required by KiCAD. * @param bool $value diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index ddbf9b9b..5811640b 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -41,7 +41,7 @@