From 82bc2963be52478857ccac8ef3e483b34a4c109e Mon Sep 17 00:00:00 2001 From: wangzhengzhuo05 <175673456+wangzhengzhuo05@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:25:00 +0800 Subject: [PATCH 1/2] fix(eda): inherit KiCad reference prefix from ancestor categories --- src/Services/EDA/KiCadHelper.php | 32 +++++++++++- tests/Services/EDA/KiCadHelperTest.php | 72 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 85159c51d..010456877 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -211,7 +211,7 @@ public function getKiCADPart(Part $part): array ]; $result["fields"]["footprint"] = $this->createField($part->getEdaInfo()->getKicadFootprint() ?? $part->getFootprint()?->getEdaInfo()->getKicadFootprint() ?? ""); - $result["fields"]["reference"] = $this->createField($part->getEdaInfo()->getReferencePrefix() ?? $part->getCategory()?->getEdaInfo()->getReferencePrefix() ?? 'U', true); + $result["fields"]["reference"] = $this->createField($this->getReferencePrefix($part), true); $result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true); $result["fields"]["keywords"] = $this->createField($part->getTags()); @@ -355,6 +355,36 @@ public function getKiCADPart(Part $part): array return $result; } + /** + * Resolve the reference prefix for the given part. + * + * A part without its own reference prefix inherits the prefix of the closest category + * ancestor that defines one (a category tree can be several levels deep, see issue #1535). + * If neither the part nor any category ancestor defines a prefix, 'U' is used as fallback. + */ + private function getReferencePrefix(Part $part): string + { + $prefix = $part->getEdaInfo()->getReferencePrefix(); + if ($prefix !== null && $prefix !== '') { + return $prefix; + } + + //Walk the category tree upwards: the closest ancestor with a prefix wins. + $category = $part->getCategory(); + $depth = 0; + while ($category !== null && $depth < 20) { + $prefix = $category->getEdaInfo()->getReferencePrefix(); + if ($prefix !== null && $prefix !== '') { + return $prefix; + } + + $category = $category->getParent(); + ++$depth; + } + + return 'U'; + } + /** * Determine if the given part should be visible for the EDA. * @param Category $category diff --git a/tests/Services/EDA/KiCadHelperTest.php b/tests/Services/EDA/KiCadHelperTest.php index b043d5980..d38b6dfc0 100644 --- a/tests/Services/EDA/KiCadHelperTest.php +++ b/tests/Services/EDA/KiCadHelperTest.php @@ -633,6 +633,78 @@ public function testParameterWithEmptyNameIsSkipped(): void self::assertArrayNotHasKey('', $result['fields']); } + public function testReferencePrefixIsInheritedFromAncestorCategory(): void + { + $part = $this->em->find(Part::class, 1); + $part->getEdaInfo()->setReferencePrefix(null); + + $parent = (new Category())->setName('Connectors'); + $parent->getEdaInfo()->setReferencePrefix('J'); + + $child = (new Category())->setName('D-sub'); + $child->setParent($parent); + + $part->setCategory($child); + + $result = $this->helper->getKiCADPart($part); + + self::assertSame('J', $result['fields']['reference']['value']); + } + + public function testNearestCategoryWithPrefixWins(): void + { + $part = $this->em->find(Part::class, 1); + $part->getEdaInfo()->setReferencePrefix(null); + + $parent = (new Category())->setName('Connectors'); + $parent->getEdaInfo()->setReferencePrefix('J'); + + $child = (new Category())->setName('D-sub'); + $child->getEdaInfo()->setReferencePrefix('X'); + $child->setParent($parent); + + $part->setCategory($child); + + $result = $this->helper->getKiCADPart($part); + + self::assertSame('X', $result['fields']['reference']['value']); + } + + public function testPartReferencePrefixOverridesCategory(): void + { + $part = $this->em->find(Part::class, 1); + $part->getEdaInfo()->setReferencePrefix('C'); + + $parent = (new Category())->setName('Connectors'); + $parent->getEdaInfo()->setReferencePrefix('J'); + + $child = (new Category())->setName('D-sub'); + $child->setParent($parent); + + $part->setCategory($child); + + $result = $this->helper->getKiCADPart($part); + + self::assertSame('C', $result['fields']['reference']['value']); + } + + public function testReferencePrefixFallsBackToUWhenNothingIsSet(): void + { + $part = $this->em->find(Part::class, 1); + $part->getEdaInfo()->setReferencePrefix(null); + + $parent = (new Category())->setName('Connectors'); + + $child = (new Category())->setName('D-sub'); + $child->setParent($parent); + + $part->setCategory($child); + + $result = $this->helper->getKiCADPart($part); + + self::assertSame('U', $result['fields']['reference']['value']); + } + /** * Category 1 (from fixtures) has a KiCad symbol set, so its parts are visible to the EDA. * The listing must carry the fields, so KiCad does not have to request each part separately. From d04aa259ee6129f7ef3cbd8541de591d634d07e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 20 Sep 2026 01:33:40 +0200 Subject: [PATCH 2/2] Implemented caching mechanism for resolved category prefix --- src/Services/EDA/KiCadHelper.php | 66 +++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index dc968f2dc..6ba8be0ac 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -178,6 +178,9 @@ function (ItemInterface $item) use ($category) { } $result = []; + //Shared across all parts of this request, so the reference prefix of a category + //(and its ancestors) only has to be resolved once, no matter how many parts use it. + $categoryPrefixCache = []; foreach ($parts as $part) { //If the part is invisible, then skip it if (!$this->shouldPartBeVisible($part)) { @@ -193,14 +196,14 @@ function (ItemInterface $item) use ($category) { * This might increase the KiCAD API response size for a category, but overall perfomance boost is * massive */ - $result[] = $this->getKiCADPart($part); + $result[] = $this->getKiCADPart($part, $categoryPrefixCache); } return $result; }); } - public function getKiCADPart(Part $part): array + public function getKiCADPart(Part $part, array &$categoryPrefixCache = []): array { $result = [ 'id' => (string)$part->getId(), @@ -214,7 +217,7 @@ public function getKiCADPart(Part $part): array ]; $result["fields"]["footprint"] = $this->createField($part->getEdaInfo()->getKicadFootprint() ?? $part->getFootprint()?->getEdaInfo()->getKicadFootprint() ?? ""); - $result["fields"]["reference"] = $this->createField($this->getReferencePrefix($part), true); + $result["fields"]["reference"] = $this->createField($this->getReferencePrefix($part, $categoryPrefixCache), true); $result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true); $result["fields"]["keywords"] = $this->createField($part->getTags()); @@ -377,28 +380,71 @@ private function edaSettingsFingerprint(): string * A part without its own reference prefix inherits the prefix of the closest category * ancestor that defines one (a category tree can be several levels deep, see issue #1535). * If neither the part nor any category ancestor defines a prefix, 'U' is used as fallback. + * + * @param Part $part The part for which to resolve the reference prefix. + * @param array $cache Reference prefix cache (keyed by category ID) to avoid repeated + * ancestor lookups for the same category across parts of a request. */ - private function getReferencePrefix(Part $part): string + private function getReferencePrefix(Part $part, array &$cache = []): string { $prefix = $part->getEdaInfo()->getReferencePrefix(); if ($prefix !== null && $prefix !== '') { return $prefix; } - //Walk the category tree upwards: the closest ancestor with a prefix wins. $category = $part->getCategory(); + if ($category === null) { + return 'U'; + } + + return $this->resolveCategoryReferencePrefix($category, $cache); + } + + /** + * Resolves the effective reference prefix of a category: the prefix of the closest ancestor + * (including the category itself) that defines one, or 'U' as fallback. + * + * Every category visited while walking up the tree is memoized in $cache, so subsequent calls + * for the same category, or for any of its descendants seen earlier in the same request, are + * resolved in O(1) instead of re-walking the ancestor chain. + * + * @param array $cache Reference prefix cache (keyed by category ID), shared across calls. + */ + private function resolveCategoryReferencePrefix(Category $category, array &$cache): string + { + $visited = []; + $current = $category; $depth = 0; - while ($category !== null && $depth < 20) { - $prefix = $category->getEdaInfo()->getReferencePrefix(); + $result = 'U'; + + while ($current !== null && $depth < 20) { + $id = $current->getId(); + if ($id !== null && isset($cache[$id])) { + $result = $cache[$id]; + break; + } + + $visited[] = $current; + + $prefix = $current->getEdaInfo()->getReferencePrefix(); if ($prefix !== null && $prefix !== '') { - return $prefix; + $result = $prefix; + break; } - $category = $category->getParent(); + $current = $current->getParent(); ++$depth; } - return 'U'; + //Cache the resolved prefix for every category on the walked path, not just the starting one. + foreach ($visited as $visitedCategory) { + $id = $visitedCategory->getId(); + if ($id !== null) { + $cache[$id] = $result; + } + } + + return $result; } /**