Skip to content

Commit 68afcef

Browse files
committed
Fix EasyEDA footprint model references
Rewrite EasyEDA footprint model paths after STEP/WRL export, strip stale KiCad model blocks when no 3D artifact is produced, and add service-worker regressions plus docs updates.
1 parent de626cd commit 68afcef

6 files changed

Lines changed: 372 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ This section summarizes the current `Development` branch compared with `main` as
3838
### Fixed
3939

4040
- Improved provider-aware page detection for JLCPCB, LCSC, Mouser, and Farnell product pages.
41+
- Reconciled EasyEDA footprint model references with exported STEP/WRL artifacts and removed stale model blocks when no 3D artifact is exported.
4142
- Preserved SamacSys KiCad library relationships by rewriting symbol footprint properties and footprint model paths in library mode.
4243
- Handled SamacSys ZIP structures where `KiCad/` and `3D/` folders appear either at the archive root or under a part-specific parent folder.
4344
- Reworked SamacSys ZIP `401` handling into clearer sign-in-required errors and retry behavior.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ Downloads/
116116
`-- <component>-datasheet.pdf
117117
```
118118

119+
When a footprint and matching 3D model are exported together, the footprint model reference is rewritten to the generated model path. If no 3D model is exported, stale model references are removed from the generated footprint.
120+
119121
## Supported sources and outputs
120122

121123
| Source flow | Pages | Symbol | Footprint | 3D model | Datasheet | Notes |

docs/architecture-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This file records short implementation notes that supplement, but do not replace
3232
- Farnell, element14, and Newark do not have their own backend adapter files because they intentionally reuse the shared SamacSys distributor backend.
3333
- Symbol library append behavior depends on `chrome.storage.local`, not on local filesystem reads.
3434
- Library-mode download paths remain relative to Downloads and are resolved from extension settings, not absolute filesystem paths.
35+
- EasyEDA footprints are written after 3D export attempts so their KiCad `(model ...)` path can match the artifact that was actually downloaded, or be stripped when no model artifact was produced.
3536
- SamacSys distributor support is still Chrome-first, but Firefox can opt into a user-managed relay through the advanced Firefox settings menu.
3637
- Chrome direct SamacSys ZIP export now retries once with configured upstream auth after a `401`, but preview requests still use the normal direct browser session without preemptive auth headers.
3738
- Firefox relay mode forwards matching `componentsearchengine.com` cookies so authenticated SamacSys ZIP downloads can reuse the browser session instead of teaching the relay to log in.

src/sources/easyeda_adapter.js

Lines changed: 201 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./easyeda_common.js";
1616
import {
1717
createExportContext,
18+
getLibraryName,
1819
resolveExportOptions,
1920
writeBinaryArtifact,
2021
writeSymbolArtifact,
@@ -27,6 +28,175 @@ function buildSafeFilename(name, extension, fallback) {
2728
return `${sanitizeFilenamePart(name, fallback)}.${extension}`;
2829
}
2930

31+
function isKicadModelBlockStart(text, index) {
32+
const token = "(model";
33+
const nextChar = text[index + token.length] || "";
34+
return (
35+
text.startsWith(token, index) &&
36+
(!nextChar || /\s|\)/.test(nextChar))
37+
);
38+
}
39+
40+
function findNextKicadModelBlock(text, startIndex = 0) {
41+
let inString = false;
42+
let escaped = false;
43+
44+
for (let index = startIndex; index < text.length; index += 1) {
45+
const char = text[index];
46+
if (inString) {
47+
if (escaped) {
48+
escaped = false;
49+
} else if (char === "\\") {
50+
escaped = true;
51+
} else if (char === "\"") {
52+
inString = false;
53+
}
54+
continue;
55+
}
56+
if (char === "\"") {
57+
inString = true;
58+
continue;
59+
}
60+
if (!isKicadModelBlockStart(text, index)) {
61+
continue;
62+
}
63+
64+
let depth = 0;
65+
let blockInString = false;
66+
let blockEscaped = false;
67+
for (let blockEnd = index; blockEnd < text.length; blockEnd += 1) {
68+
const blockChar = text[blockEnd];
69+
if (blockInString) {
70+
if (blockEscaped) {
71+
blockEscaped = false;
72+
} else if (blockChar === "\\") {
73+
blockEscaped = true;
74+
} else if (blockChar === "\"") {
75+
blockInString = false;
76+
}
77+
continue;
78+
}
79+
if (blockChar === "\"") {
80+
blockInString = true;
81+
continue;
82+
}
83+
if (blockChar === "(") {
84+
depth += 1;
85+
} else if (blockChar === ")") {
86+
depth -= 1;
87+
if (depth === 0) {
88+
return { start: index, end: blockEnd + 1 };
89+
}
90+
}
91+
}
92+
93+
return null;
94+
}
95+
96+
return null;
97+
}
98+
99+
function stripKicadFootprintModels(footprintText) {
100+
const text = String(footprintText || "");
101+
let result = "";
102+
let cursor = 0;
103+
let modelBlock = findNextKicadModelBlock(text, cursor);
104+
105+
while (modelBlock) {
106+
let removalStart = modelBlock.start;
107+
while (removalStart > cursor && /[ \t]/.test(text[removalStart - 1])) {
108+
removalStart -= 1;
109+
}
110+
if (removalStart > cursor && text[removalStart - 1] === "\n") {
111+
removalStart -= 1;
112+
if (removalStart > cursor && text[removalStart - 1] === "\r") {
113+
removalStart -= 1;
114+
}
115+
}
116+
117+
result += text.slice(cursor, removalStart);
118+
cursor = modelBlock.end;
119+
modelBlock = findNextKicadModelBlock(text, cursor);
120+
}
121+
122+
result += text.slice(cursor);
123+
return result.replace(/(?:[ \t]*\r?\n){3,}/g, "\n\n");
124+
}
125+
126+
function findKicadModelPathRange(modelBlockText) {
127+
if (!modelBlockText.startsWith("(model")) {
128+
return null;
129+
}
130+
131+
let cursor = "(model".length;
132+
while (cursor < modelBlockText.length && /\s/.test(modelBlockText[cursor])) {
133+
cursor += 1;
134+
}
135+
136+
if (modelBlockText[cursor] === "\"") {
137+
const pathStart = cursor + 1;
138+
let escaped = false;
139+
for (cursor = pathStart; cursor < modelBlockText.length; cursor += 1) {
140+
const char = modelBlockText[cursor];
141+
if (escaped) {
142+
escaped = false;
143+
} else if (char === "\\") {
144+
escaped = true;
145+
} else if (char === "\"") {
146+
return { start: pathStart, end: cursor };
147+
}
148+
}
149+
return null;
150+
}
151+
152+
const pathStart = cursor;
153+
while (
154+
cursor < modelBlockText.length &&
155+
!/[\s)]/.test(modelBlockText[cursor])
156+
) {
157+
cursor += 1;
158+
}
159+
160+
if (cursor === pathStart) {
161+
return null;
162+
}
163+
164+
return { start: pathStart, end: cursor };
165+
}
166+
167+
function rewriteFirstKicadFootprintModelPath(footprintText, modelPath) {
168+
if (!modelPath) {
169+
return stripKicadFootprintModels(footprintText);
170+
}
171+
172+
const text = String(footprintText || "");
173+
const modelBlock = findNextKicadModelBlock(text);
174+
if (!modelBlock) {
175+
return text;
176+
}
177+
178+
const modelBlockText = text.slice(modelBlock.start, modelBlock.end);
179+
const pathRange = findKicadModelPathRange(modelBlockText);
180+
if (!pathRange) {
181+
return text;
182+
}
183+
184+
const rewrittenBlock = `${modelBlockText.slice(0, pathRange.start)}${modelPath}${modelBlockText.slice(pathRange.end)}`;
185+
return `${text.slice(0, modelBlock.start)}${rewrittenBlock}${text.slice(modelBlock.end)}`;
186+
}
187+
188+
function resolveEasyedaFootprintModelPath(exportContext, modelFilename) {
189+
if (!modelFilename) {
190+
return "";
191+
}
192+
if (exportContext.settings.downloadIndividually) {
193+
return "${KIPRJMOD}/" + modelFilename;
194+
}
195+
196+
const libraryName = getLibraryName(exportContext.libraryPaths);
197+
return `../${libraryName}.3dshapes/${modelFilename}`;
198+
}
199+
30200
function createEasyedaAdapter(deps) {
31201
const {
32202
chromeApi,
@@ -55,6 +225,11 @@ function createEasyedaAdapter(deps) {
55225
symbol: resolvedOptions.symbol,
56226
footprint: resolvedOptions.footprint
57227
});
228+
const modelInfo = find3dModelInfo(cadData.packageDetail);
229+
const safeModelName = modelInfo
230+
? sanitizeFilenamePart(modelInfo.name, modelInfo.uuid || "model")
231+
: "";
232+
let footprintModelFilename = "";
58233

59234
if (kicadFiles.symbol) {
60235
const symbolFilename = buildSafeFilename(
@@ -72,28 +247,8 @@ function createEasyedaAdapter(deps) {
72247
});
73248
}
74249

75-
if (kicadFiles.footprint) {
76-
const footprintFilename = buildSafeFilename(
77-
kicadFiles.footprint.name,
78-
"kicad_mod",
79-
"footprint"
80-
);
81-
downloadCount += await writeTextArtifact({
82-
downloads,
83-
exportContext,
84-
content: kicadFiles.footprint.content,
85-
individualFilename: footprintFilename,
86-
libraryPath: `${exportContext.libraryPaths.footprintDir}/${footprintFilename}`
87-
});
88-
}
89-
90250
if (resolvedOptions.model3d) {
91-
const modelInfo = find3dModelInfo(cadData.packageDetail);
92251
if (modelInfo) {
93-
const safeModelName = sanitizeFilenamePart(
94-
modelInfo.name,
95-
modelInfo.uuid || "model"
96-
);
97252
const stepResponse = await fetchImpl(
98253
EASYEDA_MODEL_STEP_ENDPOINT.replace("{uuid}", modelInfo.uuid)
99254
);
@@ -106,6 +261,7 @@ function createEasyedaAdapter(deps) {
106261
individualFilename: `${safeModelName}.step`,
107262
libraryPath: `${exportContext.libraryPaths.modelDir}/${safeModelName}.step`
108263
});
264+
footprintModelFilename = `${safeModelName}.step`;
109265
} else {
110266
console.warn("3D STEP download failed:", stepResponse.status);
111267
warnings.push("3D STEP model download failed.");
@@ -124,6 +280,7 @@ function createEasyedaAdapter(deps) {
124280
individualFilename: `${safeModelName}.wrl`,
125281
libraryPath: `${exportContext.libraryPaths.modelDir}/${safeModelName}.wrl`
126282
});
283+
footprintModelFilename = `${safeModelName}.wrl`;
127284
} else {
128285
console.warn("3D OBJ download failed:", objResponse.status);
129286
warnings.push("3D WRL model download failed.");
@@ -133,6 +290,30 @@ function createEasyedaAdapter(deps) {
133290
}
134291
}
135292

293+
if (kicadFiles.footprint) {
294+
const footprintFilename = buildSafeFilename(
295+
kicadFiles.footprint.name,
296+
"kicad_mod",
297+
"footprint"
298+
);
299+
const modelPath = resolveEasyedaFootprintModelPath(
300+
exportContext,
301+
footprintModelFilename
302+
);
303+
const footprintContent = rewriteFirstKicadFootprintModelPath(
304+
kicadFiles.footprint.content,
305+
modelPath
306+
);
307+
308+
downloadCount += await writeTextArtifact({
309+
downloads,
310+
exportContext,
311+
content: footprintContent,
312+
individualFilename: footprintFilename,
313+
libraryPath: `${exportContext.libraryPaths.footprintDir}/${footprintFilename}`
314+
});
315+
}
316+
136317
if (resolvedOptions.datasheet) {
137318
if (!datasheetInfo.url) {
138319
warnings.push("Datasheet not available for this part.");

systemDesign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ The test suite remains the primary regression net for:
255255
- loose-file downloads when `downloadIndividually` is `true`
256256
- KiCad-style library structure when `downloadIndividually` is `false`
257257
- Library-mode symbol exports merge into a stored symbol library keyed by the resolved library root.
258+
- Footprint model references are reconciled after 3D export attempts finish:
259+
- loose-file footprints reference `${KIPRJMOD}/<modelFilename>` when a model was exported
260+
- library-mode footprints reference `../<libraryName>.3dshapes/<modelFilename>` when a model was exported
261+
- stale footprint `(model ...)` blocks are removed when no selected 3D model artifact is exported
258262
- Library-mode datasheet exports are written under `<libraryRoot>/datasheets/`.
259263

260264
### 5.4 Export SamacSys distributor parts
@@ -339,6 +343,7 @@ The test suite remains the primary regression net for:
339343
- SamacSys distributor loose-file symbol output keeps the extracted `.kicad_sym` filename from the ZIP.
340344
- Footprint output uses the extracted or generated `.kicad_mod` filename.
341345
- SamacSys distributor footprint library-mode downloads rewrite the model path into the library `.3dshapes` directory.
346+
- EasyEDA footprint downloads rewrite the first KiCad `(model ...)` path to the exported STEP or WRL artifact, preferring WRL when both are produced, and remove stale model blocks when no model artifact is exported.
342347
- EasyEDA datasheet output uses a sanitized base name plus `-datasheet` and the detected extension. In library mode it is saved under `<libraryRoot>/datasheets/`.
343348
- The library root name defaults to `easyECADDownloader` and can be changed to another Downloads-relative folder for library mode.
344349

0 commit comments

Comments
 (0)