@jlcpcb/cli 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +23 -5
- package/dist/assets/search.html +32 -31
- package/dist/index.js +7053 -6459
- package/package.json +1 -1
- package/src/app/App.tsx +1 -1
- package/src/app/__fixtures__/interactive-install-scenarios.tsx +149 -0
- package/src/app/__fixtures__/navigation-scenarios.tsx +103 -0
- package/src/app/__fixtures__/terminal.tsx +57 -0
- package/src/app/components/DetailView.tsx +8 -5
- package/src/app/components/ListView.tsx +1 -1
- package/src/app/components/list-view-format.test.ts +2 -0
- package/src/app/navigation/NavigationContext.tsx +1 -1
- package/src/app/navigation/types.ts +8 -1
- package/src/app/navigation.test.ts +24 -0
- package/src/app/screens/EasyEDAInfoScreen.tsx +7 -9
- package/src/app/screens/InfoScreen.tsx +37 -19
- package/src/app/screens/InstallScreen.tsx +3 -5
- package/src/app/screens/InstalledScreen.tsx +3 -3
- package/src/app/screens/LibrarySetupScreen.tsx +12 -5
- package/src/app/screens/SearchScreen.tsx +22 -18
- package/src/app/state/AppStateContext.tsx +0 -23
- package/src/commands/__fixtures__/failure-scenarios.ts +67 -0
- package/src/commands/agent-json.test.ts +194 -0
- package/src/commands/easyeda.ts +67 -14
- package/src/commands/failures.test.ts +105 -0
- package/src/commands/info.ts +6 -9
- package/src/commands/install.ts +43 -12
- package/src/commands/interactive-install.test.ts +29 -0
- package/src/commands/library.ts +14 -23
- package/src/commands/search.ts +36 -4
- package/src/commands/validate.ts +114 -53
- package/src/index.ts +38 -6
- package/src/utils/agent-output.ts +35 -0
- package/src/utils/search-result-output.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @jlcpcb/cli
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Remove the 50 mm symbol tolerance and report blocking pin validation errors with explicit scope in JSON, text, and HTML; preserve failures for intentional templates and incomplete source metadata.
|
|
8
|
+
- Bundle corrected browser pad/drill rendering, complete symbol curves and fills, and visible property placement for off-origin symbols.
|
|
9
|
+
- Apply corrected footprint geometry and conservative standard-package substitution during installation and regeneration.
|
|
10
|
+
- Fail footprint validation on manufacturing differences or unverified geometry, and check batch fixture identities against live part/package metadata.
|
|
11
|
+
|
|
12
|
+
- Preserve destination, force, and 3D options throughout LCSC and community interactive installation.
|
|
13
|
+
- Keep search constraints and filtered results when toggling filters or returning from component details.
|
|
14
|
+
- Handle Escape once, discard abandoned setup work, and return from completed setup and installation to component details.
|
|
15
|
+
- Exit unsuccessfully for regeneration failures and report missing requested validation references as failures.
|
|
16
|
+
|
|
17
|
+
## 0.4.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- Improve agent-facing CLI and MCP contracts.
|
|
22
|
+
- Add clean JSON output modes for CLI search, info, install, EasyEDA, library, and validation commands.
|
|
23
|
+
- Add direct non-interactive install flows with `--yes` while keeping `--force` scoped to reinstall behavior.
|
|
24
|
+
- Use discriminated component result IDs for LCSC and EasyEDA community parts.
|
|
25
|
+
- Return structured MCP errors and explicit partial-success statuses.
|
|
26
|
+
- Harden EasyEDA HTTP fallback execution and validate component IDs before URL construction.
|
|
27
|
+
- Avoid artifact writes during no-op installs when a symbol already exists.
|
|
28
|
+
|
|
3
29
|
## 0.3.2
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -72,10 +72,8 @@ jlc info C8734 --json
|
|
|
72
72
|
| Key | Action |
|
|
73
73
|
|-----|--------|
|
|
74
74
|
| `Enter` | Install component |
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `M` | Open 3D model |
|
|
78
|
-
| `D` | Open datasheet URL |
|
|
75
|
+
| `D` | Delete installed component |
|
|
76
|
+
| `O` | Open datasheet URL |
|
|
79
77
|
| `R` | Regenerate symbol/footprint |
|
|
80
78
|
| `Esc` | Go back |
|
|
81
79
|
|
|
@@ -103,12 +101,16 @@ jlc install C8734 --force # Regenerate if exists
|
|
|
103
101
|
| `-p, --project <path>` | Install to project-local library |
|
|
104
102
|
| `--with-3d` | Include 3D model (STEP) |
|
|
105
103
|
| `-f, --force` | Force reinstall/regenerate |
|
|
104
|
+
| `-y, --yes` | Install directly without the TUI |
|
|
105
|
+
| `--json` | Install directly and emit JSON |
|
|
106
|
+
|
|
107
|
+
Destination, force, and 3D options apply throughout interactive selection and confirmation as well as direct installation. A project-local install checks and updates that project's library rather than a global copy.
|
|
106
108
|
|
|
107
109
|
**Installation Result:**
|
|
108
110
|
|
|
109
111
|
After installation, you'll see:
|
|
110
112
|
- **Symbol reference**: e.g., `JLC-MCP-ICs:STM32F103C8T6`
|
|
111
|
-
- **Footprint reference**:
|
|
113
|
+
- **Footprint reference**: a KiCad standard passive reference or a generated `JLC-MCP:...` reference
|
|
112
114
|
- Validation info (pin/pad counts, power pins detected)
|
|
113
115
|
|
|
114
116
|
### `jlc library`
|
|
@@ -140,6 +142,22 @@ Shows all installed components with:
|
|
|
140
142
|
|
|
141
143
|
Navigate with `↑` `↓` and press `Enter` to view component details.
|
|
142
144
|
|
|
145
|
+
### Regeneration and validation exit status
|
|
146
|
+
|
|
147
|
+
`jlc library regenerate --project ./my-kicad-project` reports each component failure and exits nonzero if any regeneration fails.
|
|
148
|
+
|
|
149
|
+
`jlc validate C8734 --json` also exits nonzero when a requested symbol or footprint reference is unavailable. `--symbol-only` and `--footprint-only` disable the other comparison; a missing requested reference is never treated as a successful comparison.
|
|
150
|
+
|
|
151
|
+
Footprint validation checks copper boundaries, layers, plating, slot dimensions/orientation/offset, mechanical holes, and vias. Unknown manufacturing metadata fails as unverified rather than passing on pad count alone. Batch fixtures also verify the live manufacturer part number and package identity. The footprint limits are 0.05 mm for position and 0.02 mm for full dimensions.
|
|
152
|
+
|
|
153
|
+
Symbol validation checks pin identity, names, relative connection layout, direction, and declared electrical type. It ignores only global translation, with a maximum 0.05 mm position tolerance and 1° direction tolerance. Missing/duplicate identities, unsupported metadata, and meaningful differences are blocking errors—not warning-only passes.
|
|
154
|
+
|
|
155
|
+
JSON and HTML reports explicitly identify this as **pin validation**: body graphics, text, pin lengths, and pin styles are not compared. Standardized passive symbols can intentionally differ from their source and still fail this comparison. The live fixture catalog also retains failures for empty RP2040 pin electrical metadata and duplicate Mini-USB pin identities; fixture expectations never bypass CLI failures.
|
|
156
|
+
|
|
157
|
+
Generate a side-by-side report with `jlc validate C42417612 --symbol-only --html validation.html`. Browser previews preserve capsules, custom copper, offsets, arcs, fills, and visible labels, but use browser fonts rather than KiCad's stroke fonts.
|
|
158
|
+
|
|
159
|
+
Generation preserves source geometry or returns an error for ambiguous drills and unsupported cutout semantics. Standard substitution is limited to compatible unpolarized two-terminal R/C/L packages; it intentionally uses KiCad's nominal land pattern. Existing installations need explicit regeneration, for example `jlc install C8734 --force`, to receive these corrections.
|
|
160
|
+
|
|
143
161
|
## Keyboard Navigation
|
|
144
162
|
|
|
145
163
|
Global keyboard shortcuts across all screens:
|
package/dist/assets/search.html
CHANGED
|
@@ -452,76 +452,77 @@
|
|
|
452
452
|
</div>
|
|
453
453
|
|
|
454
454
|
<script>
|
|
455
|
-
function
|
|
456
|
-
<g transform="scale(${
|
|
457
|
-
${
|
|
455
|
+
function Bt(t){let e=[],n=0;while(n<t.length){let i=t[n];if(/\s/.test(i)){n++;continue}if(i==="("||i===")"){e.push(i),n++;continue}if(i==='"'){let s="";n++;while(n<t.length)if(t[n]==="\\"&&n+1<t.length)s+=t[n+1],n+=2;else if(t[n]==='"'){n++;break}else s+=t[n],n++;e.push(`"${s}"`);continue}let o="";while(n<t.length&&!/[\s()]/.test(t[n]))o+=t[n],n++;if(o)e.push(o)}return e}function gt(t){if(t.length===0)return{expr:[],remaining:[]};let e=t[0];if(e==="("){let i=[],o=t.slice(1);while(o.length>0&&o[0]!==")"){let{expr:s,remaining:a}=gt(o);i.push(s),o=a}if(o[0]===")")o=o.slice(1);return{expr:i,remaining:o}}if(e===")")return{expr:[],remaining:t.slice(1)};let n=e;if(n.startsWith('"')&&n.endsWith('"'))n=n.slice(1,-1);return{expr:n,remaining:t.slice(1)}}function nt(t){let e=Bt(t),{expr:n}=gt(e);return n}function k(t){return Array.isArray(t)}function b(t){return typeof t==="string"}function F(t){if(k(t)&&t.length>0&&b(t[0]))return t[0];return}function y(t,e){if(!k(t))return;for(let n of t)if(k(n)&&F(n)===e)return n;return}function L(t,e){if(!k(t))return[];let n=[];for(let i of t)if(k(i)&&F(i)===e)n.push(i);return n}function T(t,e){let n=y(t,e);if(n&&n.length>=2&&b(n[1]))return n[1];return}function Y(t,e){let n=T(t,e);if(n!==void 0){let i=parseFloat(n);if(!isNaN(i))return i}return}function C(t,e){let n=y(t,e);if(n&&n.length>=3){let i=parseFloat(b(n[1])?n[1]:""),o=parseFloat(b(n[2])?n[2]:"");if(!isNaN(i)&&!isNaN(o))return{x:i,y:o}}return}function V(t,e){let n=y(t,e);if(n&&n.length>=3){let i=parseFloat(b(n[1])?n[1]:""),o=parseFloat(b(n[2])?n[2]:"");if(!isNaN(i)&&!isNaN(o)){let s={x:i,y:o};if(n.length>=4&&b(n[3])){let a=parseFloat(n[3]);if(!isNaN(a))s.rotation=a}return s}}return}function et(t){let e=y(t,"size");if(e&&e.length>=3){let n=parseFloat(b(e[1])?e[1]:""),i=parseFloat(b(e[2])?e[2]:"");if(!isNaN(n)&&!isNaN(i))return{width:n,height:i}}return}function j(t){let e=y(t,"stroke");if(!e)return;let n=Y(e,"width")??0.254,i=T(e,"type")??"default";return{width:n,type:i}}function it(t){let e=y(t,"fill");if(!e)return;let n=T(e,"type");if(n)return n;if(e.length>=2&&b(e[1]))return e[1];return}function K(t){let e=y(t,"pts");if(!e)return[];let n=[];for(let i of e)if(k(i)&&F(i)==="xy"&&i.length>=3){let o=parseFloat(b(i[1])?i[1]:""),s=parseFloat(b(i[2])?i[2]:"");if(!isNaN(o)&&!isNaN(s))n.push({x:o,y:s})}return n}function D(t){let e=T(t,"layer");if(e)return[e];let n=y(t,"layers");if(!n)return[];let i=[];for(let o=1;o<n.length;o++){let s=n[o];if(b(s))i.push(s)}return i}var B={background:"#FFFFF8",body:"#840000",bodyFill:"#FFFFC4",pin:"#840000",pinText:"#008484",text:"#000000"},x={background:"#000000",fCu:"#840000",bCu:"#008400",in1Cu:"#C2C200",in2Cu:"#C200C2",fSilkS:"#008484",bSilkS:"#840084",fMask:"#840084",bMask:"#848400",fPaste:"#840000",bPaste:"#00C2C2",fFab:"#848484",bFab:"#000084",fCrtYd:"#C2C2C2",bCrtYd:"#848484",edgeCuts:"#C2C200",dwgsUser:"#C2C2C2",cmtsUser:"#848484",margin:"#C200C2",padFront:"#840000",padBack:"#008400",padThruHole:"#C2C200",drill:"#848484"},U=10;function q(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function ot(t){if(!t||t.trim()==="")return X("No symbol data");try{let e=nt(t);if(k(e)&&F(e)==="kicad_symbol_lib"){let c=L(e,"symbol");if(c.length===0)return X("No symbol found in library");e=c[0]}if(!k(e)||F(e)!=="symbol")return X("Invalid symbol format");let n=[],i={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0},o=L(e,"symbol"),s=y(e,"pin_names"),a=y(e,"pin_numbers");for(let c of L(e,"property"))n.push(O(c,i,!0));for(let c of o){for(let l of L(c,"rectangle")){let d=Ft(l,i);if(d)n.push(d)}for(let l of L(c,"polyline")){let d=Pt(l,i);if(d)n.push(d)}for(let l of L(c,"circle")){let d=Tt(l,i);if(d)n.push(d)}for(let l of L(c,"arc")){let d=It(l,i);if(d)n.push(d)}for(let l of L(c,"text"))n.push(O(l,i,!0));for(let l of L(c,"pin")){let d=Ht(l,i,s,a);if(d)n.push(d)}}let f=5;if(!isFinite(i.minX))i.minX=-20,i.maxX=20,i.minY=-20,i.maxY=20;let u=(i.maxX-i.minX+f*2)*U,r=(i.maxY-i.minY+f*2)*U;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${(i.minX-f)*U} ${(-i.maxY-f)*U} ${u} ${r}`}" width="100%" height="100%" style="background-color: ${B.background}">
|
|
456
|
+
<g transform="scale(${U}, ${-U})">
|
|
457
|
+
${n.join(`
|
|
458
458
|
`)}
|
|
459
459
|
</g>
|
|
460
|
-
</svg>`}catch(
|
|
461
|
-
`)}function
|
|
460
|
+
</svg>`}catch(e){return X(`Preview error: ${e}`)}}function Q(t){let e=it(t);return e==="background"?B.bodyFill:e==="outline"||e==="solid"?B.body:"none"}function Ft(t,e){let n=C(t,"start"),i=C(t,"end");if(!n||!i)return"";let o=j(t)?.width||0.254;return v(e,n.x,n.y,o/2),v(e,i.x,i.y,o/2),`<rect x="${Math.min(n.x,i.x)}" y="${Math.min(n.y,i.y)}" width="${Math.abs(i.x-n.x)}" height="${Math.abs(i.y-n.y)}" fill="${Q(t)}" stroke="${B.body}" stroke-width="${o}" stroke-linejoin="round"/>`}function Pt(t,e){let n=K(t);if(n.length<2)return"";let i=j(t)?.width||0.254;for(let o of n)v(e,o.x,o.y,i/2);return`<polyline points="${n.map((o)=>`${o.x},${o.y}`).join(" ")}" fill="${Q(t)}" stroke="${B.body}" stroke-width="${i}" stroke-linecap="round" stroke-linejoin="round"/>`}function Tt(t,e){let n=C(t,"center"),i=Y(t,"radius");if(!n||i===void 0)return"";let o=j(t)?.width||0.254;return v(e,n.x,n.y,i+o/2),`<circle cx="${n.x}" cy="${n.y}" r="${i}" fill="${Q(t)}" stroke="${B.body}" stroke-width="${o}"/>`}function It(t,e){let n=C(t,"start"),i=C(t,"mid"),o=C(t,"end");if(!n||!i||!o)return"";let s=j(t)?.width||0.254;return`<path d="${yt(n,i,o,e,s)}" fill="${Q(t)}" stroke="${B.body}" stroke-width="${s}" stroke-linecap="round"/>`}function A(t,e){let n=-(e.rotation??0)*Math.PI/180;return{x:e.x+t.x*Math.cos(n)-t.y*Math.sin(n),y:e.y+t.x*Math.sin(n)+t.y*Math.cos(n)}}function yt(t,e,n,i,o){let s=e.x-t.x,a=e.y-t.y,f=n.x-t.x,u=n.y-t.y,r=2*(s*u-a*f);for(let h of[t,e,n])v(i,h.x,h.y,o/2);if(Math.abs(r)<=Number.EPSILON*Math.max(s*s+a*a,f*f+u*u)*16)return`M ${t.x} ${t.y} L ${e.x} ${e.y} L ${n.x} ${n.y}`;let m=s*s+a*a,c=f*f+u*u,l=(m*u-c*a)/r,d=(s*c-f*m)/r,g={x:t.x+l,y:t.y+d},w=Math.hypot(l,d),p=Math.PI*2,S=(h)=>(h%p+p)%p,M=Math.atan2(-d,-l),I=Math.atan2(n.y-g.y,n.x-g.x),P=r>0,E=S(P?I-M:M-I);for(let h=0;h<p;h+=Math.PI/2)if(S(P?h-M:M-h)<=E+0.000000000001)v(i,g.x+w*Math.cos(h),g.y+w*Math.sin(h),o/2);return`M ${t.x} ${t.y} A ${w} ${w} 0 ${E>Math.PI?1:0} ${P?1:0} ${n.x} ${n.y}`}function z(t){if(!t)return!1;let e=y(t,"hide");return t.includes("hide")||!!e&&(e.length===1||e[1]==="yes")}function Ht(t,e,n,i){if(z(t))return"";let o=V(t,"at");if(!o)return"";let s=Y(t,"length")??2.54,a=((o.rotation??0)%360+360)%360,f=a*Math.PI/180,u={x:Math.cos(f),y:Math.sin(f)},r={x:o.x+s*u.x,y:o.y+s*u.y},m=b(t[2])?t[2]:"line";if(!["line","inverted","clock","inverted_clock"].includes(m))throw Error(`Unsupported pin style: ${m}`);let c=m==="inverted"||m==="inverted_clock",l=m==="clock"||m==="inverted_clock",d=0.3,g=c?{x:r.x-2*d*u.x,y:r.y-2*d*u.y}:r;v(e,o.x,o.y,0.127),v(e,r.x,r.y,0.127);let w=[`<line x1="${o.x}" y1="${o.y}" x2="${g.x}" y2="${g.y}" stroke="${B.pin}" stroke-width="0.254"/>`];if(c){let E={x:r.x-d*u.x,y:r.y-d*u.y};v(e,E.x,E.y,d+0.127),w.push(`<circle cx="${E.x}" cy="${E.y}" r="${d}" fill="none" stroke="${B.pin}" stroke-width="0.254"/>`)}if(l){let E=[{x:r.x-u.y*0.6,y:r.y+u.x*0.6},{x:r.x+u.x*0.6,y:r.y+u.y*0.6},{x:r.x+u.y*0.6,y:r.y-u.x*0.6}];for(let h of E)v(e,h.x,h.y,0.127);w.push(`<polyline points="${E.map((h)=>`${h.x},${h.y}`).join(" ")}" fill="none" stroke="${B.pin}" stroke-width="0.254" stroke-linejoin="round"/>`)}let p=y(t,"name"),S=y(t,"number"),M=n?Y(n,"offset")??0.508:0.508,I=M===0?{x:(o.x+r.x)/2-u.y*0.8,y:(o.y+r.y)/2+u.x*0.8}:{x:r.x+u.x*M,y:r.y+u.y*M},P=a===90||a===270?90:0;if(p&&p[1]!=="~"&&!z(n)){let E=M===0?[]:[["justify",a===180||a===270?"right":"left"]],h=y(p,"effects")??["effects",["font",["size","1.27","1.27"]]];w.push(O(["text",p[1],["at",`${I.x}`,`${I.y}`,`${P}`],[...h,...E]],e,!0,B.pinText))}if(S&&!z(i)){let E=M===0?-1:1,h={x:(o.x+r.x)/2-u.y*0.8*E,y:(o.y+r.y)/2+u.x*0.8*E};w.push(O(["text",S[1],["at",`${h.x}`,`${h.y}`,`${P}`],y(S,"effects")??["effects",["font",["size","1.27","1.27"]]]],e,!0,B.pinText))}return w.join(`
|
|
461
|
+
`)}function rt(t){if(!t||t.trim()==="")return X("No footprint data");try{let e=nt(t);if(!k(e)||F(e)!=="footprint")return X("Invalid footprint format");let n=[],i={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0};for(let r of L(e,"pad")){let m=Nt(r,i);if(m)n.push(m)}let o=new Map;for(let r of L(e,"property"))if(b(r[1])&&b(r[2]))o.set(r[1].toUpperCase(),r[2]);for(let r of e){if(!k(r))continue;let m=F(r)??"";if(m==="fp_text"||m==="property")n.push(O(r,i,!1,void 0,o));else if(m.startsWith("fp_")||m.startsWith("gr_"))n.push(vt(r,i));else if(m==="zone")n.push(Yt(r,i))}let s=1;if(!isFinite(i.minX))i.minX=-5,i.maxX=5,i.minY=-5,i.maxY=5;let a=i.maxX-i.minX+s*2,f=i.maxY-i.minY+s*2;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${i.minX-s} ${i.minY-s} ${a} ${f}`}" width="100%" height="100%" style="background-color: ${x.background}">
|
|
462
462
|
<g>
|
|
463
|
-
${
|
|
463
|
+
${n.join(`
|
|
464
464
|
`)}
|
|
465
465
|
</g>
|
|
466
|
-
</svg>`}catch(
|
|
467
|
-
`)}function
|
|
466
|
+
</svg>`}catch(e){return X(`Preview error: ${e}`)}}function xt(t){if(t.includes("F.Cu"))return x.fCu;if(t.includes("B.Cu"))return x.bCu;if(/In\d+\.Cu/.test(t))return x.in1Cu;if(t.includes("F.SilkS"))return x.fSilkS;if(t.includes("B.SilkS"))return x.bSilkS;if(t.includes("F.Mask"))return x.fMask;if(t.includes("B.Mask"))return x.bMask;if(t.includes("F.Paste"))return x.fPaste;if(t.includes("B.Paste"))return x.bPaste;if(t.includes("F.Fab"))return x.fFab;if(t.includes("B.Fab"))return x.bFab;if(t.includes("F.CrtYd"))return x.fCrtYd;if(t.includes("B.CrtYd"))return x.bCrtYd;if(t.includes("Edge.Cuts"))return x.edgeCuts;if(t.includes("Dwgs.User"))return x.dwgsUser;if(t.includes("Cmts.User"))return x.cmtsUser;if(t.includes("Margin"))return x.margin;return x.fSilkS}function Nt(t,e){let n=b(t[1])?t[1]:"",i=b(t[2])?t[2]:"",o=b(t[3])?t[3]:"",s=V(t,"at"),a=et(t);if(!s||!a)throw Error(`Invalid pad geometry: ${n}`);if(!["rect","roundrect","circle","oval","custom"].includes(o)||y(t,"chamfer")||y(t,"rect_delta"))throw Error(`Unsupported pad geometry: ${o}`);let f=D(t),u=i==="thru_hole"||i==="np_thru_hole"?x.padThruHole:f.includes("B.Cu")&&!f.includes("F.Cu")?x.padBack:x.padFront,r=y(t,"drill"),m=r?C(r,"offset")??{x:0,y:0}:{x:0,y:0},c={...A(m,s),rotation:s.rotation},l={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0},d=[],g=o==="custom"?T(y(t,"options")??[],"anchor")??"rect":o;if(!["rect","roundrect","circle","oval"].includes(g))throw Error(`Unsupported custom anchor: ${g}`);let w=g==="oval"?Math.min(a.width,a.height)/2:g==="roundrect"?Math.min(a.width,a.height)*(Y(t,"roundrect_rratio")??0.25):0;if(g==="circle"){let p=a.width/2;v(l,c.x,c.y,p),d.push(`<circle cx="${c.x}" cy="${c.y}" r="${p}" fill="${u}"/>`)}else d.push(ht(a.width,a.height,w,c,u,l));if(o==="custom"){let p=y(t,"primitives");for(let S of p??[])if(k(S))d.push(vt(S,l,c,u))}if(v(e,l.minX,l.minY),v(e,l.maxX,l.maxY),r){let p=r[1]==="oval",S=Number(p?r[2]:r[1]),M=Number(p?r[3]:r[1]);if(!(S>0&&M>0))throw Error(`Invalid drill geometry: ${n}`);d.push(ht(S,M,Math.min(S,M)/2,s,x.drill,e))}if(n){let p=(o==="custom"?Math.min(l.maxX-l.minX,l.maxY-l.minY):Math.min(a.width,a.height))*0.5,S=n.length*p*0.7;v(e,c.x-S/2,c.y-p*0.75),v(e,c.x+S/2,c.y+p*0.75),d.push(`<text x="${c.x}" y="${c.y}" fill="#FFFFFF" font-size="${p}" textLength="${S}" lengthAdjust="spacingAndGlyphs" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${q(n)}</text>`)}return`<g data-pad="${q(n)}">${d.join(`
|
|
467
|
+
`)}</g>`}function ht(t,e,n,i,o,s){let a=Math.min(Math.max(n,0),Math.min(t,e)/2),f=(i.rotation??0)*Math.PI/180,u=Math.abs(Math.cos(f)),r=Math.abs(Math.sin(f)),m=u*(t/2-a)+r*(e/2-a)+a,c=r*(t/2-a)+u*(e/2-a)+a;return v(s,i.x-m,i.y-c),v(s,i.x+m,i.y+c),`<rect x="${-t/2}" y="${-e/2}" width="${t}" height="${e}" rx="${a}" fill="${o}" transform="translate(${i.x} ${i.y}) rotate(${-(i.rotation??0)})"/>`}function vt(t,e,n={x:0,y:0},i){let o=F(t)??"",s=o.replace(/^(fp|gr)_/,""),a=i??xt(D(t)[0]??"F.SilkS"),f=j(t)?.width??Y(t,"width")??(i?0:0.15),u=it(t),r=u==="solid"||u==="yes"?a:"none",m=`stroke="${a}" stroke-width="${f}" stroke-linecap="round" stroke-linejoin="round"`;if(s==="line"||s==="arc"||s==="rect"){let c=C(t,"start"),l=C(t,"end");if(!c||!l)throw Error(`Invalid ${o} geometry`);let d=A(c,n),g=A(l,n);if(s==="arc"){let w=C(t,"mid");if(!w)throw Error(`Unsupported ${o} without midpoint`);return`<path d="${yt(d,A(w,n),g,e,f)}" fill="none" ${m}/>`}if(s==="rect"){let w=[c,{x:l.x,y:c.y},l,{x:c.x,y:l.y}].map((p)=>A(p,n));for(let p of w)v(e,p.x,p.y,f/2);return`<polygon points="${w.map((p)=>`${p.x},${p.y}`).join(" ")}" fill="${r}" ${m}/>`}return v(e,d.x,d.y,f/2),v(e,g.x,g.y,f/2),`<line x1="${d.x}" y1="${d.y}" x2="${g.x}" y2="${g.y}" ${m}/>`}if(s==="circle"){let c=C(t,"center"),l=C(t,"end");if(!c||!l)throw Error(`Invalid ${o} geometry`);let d=Math.hypot(l.x-c.x,l.y-c.y),g=A(c,n);return v(e,g.x,g.y,d+f/2),`<circle cx="${g.x}" cy="${g.y}" r="${d}" fill="${r}" ${m}/>`}if(s==="poly"){let c=K(t).map((l)=>A(l,n));if(c.length<3)throw Error(`Invalid ${o} geometry`);for(let l of c)v(e,l.x,l.y,f/2);return`<polygon points="${c.map((l)=>`${l.x},${l.y}`).join(" ")}" fill="${r}" ${m}/>`}throw Error(`Unsupported footprint geometry: ${o}`)}function Yt(t,e){let n=y(t,"keepout");if(!n)throw Error("Unsupported footprint copper zone (not a rule area)");let i=n.filter(k).filter((f)=>f[1]==="not_allowed").map((f)=>F(f)),o=L(t,"polygon");if(o.length===0)throw Error("Invalid rule area: missing polygon");let s=x.margin,a=[];for(let f of o){let u=K(f);if(u.length<3)throw Error("Invalid rule-area polygon");for(let r of u)v(e,r.x,r.y,0.075);a.push(`<polygon points="${u.map((r)=>`${r.x},${r.y}`).join(" ")}" fill="${s}" fill-opacity="0.12" stroke="${s}" stroke-width="0.15" stroke-dasharray="0.4 0.2"/>`)}return`<g data-rule-area="true"><title>${q(`Rule area (${D(t).join(", ")}): ${i.join(", ")} prohibited; outline only, not a copper subtraction`)}</title>${a.join("")}</g>`}function O(t,e,n,i,o){let s=y(t,"effects");if(z(t)||z(s))return"";let a=V(t,"at"),f=t[F(t)==="text"?1:2];if(!a||!b(f)||!f)return"";let u=f.replace(/\$\{([^}]+)\}/g,(H,N)=>o?.get(N.toUpperCase())??H),r=s?y(s,"font"):void 0,m=r?et(r):void 0,c=m?.height??1.27,l=(m?.width??c)*0.7,d=s?y(s,"justify"):void 0,g=d?.includes("left")?"start":d?.includes("right")?"end":"middle",w=d?.includes("mirror")?-1:1,p=u.split(`
|
|
468
|
+
`),S=c*1.5,M=c+(p.length-1)*S,I=d?.includes("top")?0:d?.includes("bottom")?-M:-M/2,P=n?a.rotation??0:-(a.rotation??0),E=P*Math.PI/180,h=[];for(let H=0;H<p.length;H++){let N=p[H].length*l,ft=g==="start"?0:g==="end"?-N:-N/2,tt=I+c/2+H*S;for(let Lt of[ft,ft+N])for(let Ct of[tt-c*0.75,tt+c*0.75]){let pt=Lt*w,mt=Ct*(n?-1:1);v(e,a.x+pt*Math.cos(E)-mt*Math.sin(E),a.y+pt*Math.sin(E)+mt*Math.cos(E))}h.push(`<tspan x="0" y="${tt}"${N>0?` textLength="${N}" lengthAdjust="spacingAndGlyphs"`:""}>${q(p[H])}</tspan>`)}let Mt=i??(n?B.text:xt(D(t)[0]??"F.SilkS")),kt=`${r?.includes("bold")||T(r??[],"bold")==="yes"?' font-weight="bold"':""}${r?.includes("italic")||T(r??[],"italic")==="yes"?' font-style="italic"':""}`;return`<text fill="${Mt}" font-size="${c}" font-family="sans-serif" text-anchor="${g}" dominant-baseline="central"${kt} transform="translate(${a.x} ${a.y}) rotate(${P}) scale(${w} ${n?-1:1})">${h.join("")}</text>`}function v(t,e,n,i=0){t.minX=Math.min(t.minX,e-i),t.maxX=Math.max(t.maxX,e+i),t.minY=Math.min(t.minY,n-i),t.maxY=Math.max(t.maxY,n+i)}function X(t){return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" width="100%" height="100%">
|
|
468
469
|
<rect width="200" height="100" fill="#FFF0F0"/>
|
|
469
|
-
<text x="100" y="50" fill="#CC0000" font-size="12" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${
|
|
470
|
-
</svg>`}var
|
|
471
|
-
<div class="card" data-uuid="${
|
|
470
|
+
<text x="100" y="50" fill="#CC0000" font-size="12" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${q(t)}</text>
|
|
471
|
+
</svg>`}var ut=1,G="",St="user",st=!1,ct=null,W=document.getElementById("search-input"),$t=document.getElementById("source-select"),At=document.getElementById("search-btn"),dt=document.getElementById("results-grid"),at=document.getElementById("pagination"),Xt=document.getElementById("loading"),Z=document.getElementById("preview-modal"),J=document.getElementById("modal-content"),jt=document.getElementById("modal-close");function wt(){let e=new URLSearchParams(window.location.search).get("q");if(e)W.value=e,G=e,_();W.addEventListener("input",Ut),W.addEventListener("keydown",(n)=>{if(n.key==="Enter")n.preventDefault(),_()}),At.addEventListener("click",_),$t.addEventListener("change",()=>{if(St=$t.value,G)_()}),jt.addEventListener("click",lt),Z.addEventListener("click",(n)=>{if(n.target===Z)lt()}),document.addEventListener("keydown",(n)=>{if(n.key==="Escape")lt()})}function Ut(){if(ct)clearTimeout(ct);ct=window.setTimeout(()=>{let t=W.value.trim();if(t&&t!==G)G=t,ut=1,_()},300)}async function _(){let t=W.value.trim();if(!t||st)return;G=t,st=!0,bt(!0);try{let e=new URLSearchParams({q:t,source:St,page:String(ut),limit:"20"}),n=await fetch(`/api/search?${e}`);if(!n.ok)throw Error("Search failed");let i=await n.json();_t(i.results),zt(i.pagination)}catch(e){console.error("Search error:",e),dt.innerHTML='<div class="error">Search failed. Please try again.</div>'}finally{st=!1,bt(!1)}}function _t(t){if(t.length===0){dt.innerHTML='<div class="no-results">No components found. Try a different search term.</div>';return}dt.innerHTML=t.map((e)=>`
|
|
472
|
+
<div class="card" data-uuid="${e.uuid}">
|
|
472
473
|
<div class="card-images">
|
|
473
|
-
<div class="image-container symbol-container" data-uuid="${
|
|
474
|
+
<div class="image-container symbol-container" data-uuid="${e.uuid}">
|
|
474
475
|
<div class="symbol-placeholder">Loading...</div>
|
|
475
476
|
<div class="image-label">Symbol</div>
|
|
476
477
|
</div>
|
|
477
|
-
<div class="image-container footprint-container" data-uuid="${
|
|
478
|
+
<div class="image-container footprint-container" data-uuid="${e.uuid}">
|
|
478
479
|
<div class="footprint-placeholder">Loading...</div>
|
|
479
480
|
<div class="image-label">Footprint</div>
|
|
480
481
|
</div>
|
|
481
482
|
</div>
|
|
482
483
|
<div class="card-info">
|
|
483
|
-
<div class="card-title" title="${
|
|
484
|
-
<div class="card-package">Package: ${
|
|
485
|
-
<div class="card-owner">By: ${
|
|
484
|
+
<div class="card-title" title="${R(e.title)}">${R(e.title)}</div>
|
|
485
|
+
<div class="card-package">Package: ${R(e.package||"Unknown")}</div>
|
|
486
|
+
<div class="card-owner">By: ${R(e.owner.nickname||e.owner.username)}</div>
|
|
486
487
|
</div>
|
|
487
488
|
<div class="card-uuid">
|
|
488
|
-
<input type="text" value="${
|
|
489
|
-
<button class="copy-btn" data-uuid="${
|
|
489
|
+
<input type="text" value="${e.uuid}" readonly />
|
|
490
|
+
<button class="copy-btn" data-uuid="${e.uuid}" title="Copy UUID">
|
|
490
491
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
|
491
492
|
</button>
|
|
492
493
|
</div>
|
|
493
494
|
</div>
|
|
494
|
-
`).join(""),document.querySelectorAll(".copy-btn").forEach((
|
|
495
|
+
`).join(""),document.querySelectorAll(".copy-btn").forEach((e)=>{e.addEventListener("click",(n)=>{n.stopPropagation();let i=e.dataset.uuid;if(i)Et(i,e)})}),document.querySelectorAll(".image-container").forEach((e)=>{e.addEventListener("click",async()=>{let n=e.dataset.uuid;if(n)Dt(n)})}),t.forEach((e)=>Rt(e.uuid))}async function Rt(t){let e=document.querySelector(`.symbol-container[data-uuid="${t}"]`),n=document.querySelector(`.footprint-container[data-uuid="${t}"]`);if(!e&&!n)return;try{let i=await fetch(`/api/component/${t}`);if(!i.ok){if(e)e.innerHTML='<div class="no-preview">Error</div><div class="image-label">Symbol</div>';if(n)n.innerHTML='<div class="no-preview">Error</div><div class="image-label">Footprint</div>';return}let o=await i.json();if(e)if(o.symbolSexpr){let s=ot(o.symbolSexpr);e.innerHTML=s+'<div class="image-label">Symbol</div>'}else e.innerHTML='<div class="no-preview">No preview</div><div class="image-label">Symbol</div>';if(n)if(o.footprintSexpr){let s=rt(o.footprintSexpr);n.innerHTML=s+'<div class="image-label">Footprint</div>'}else n.innerHTML='<div class="no-preview">No preview</div><div class="image-label">Footprint</div>'}catch{if(e)e.innerHTML='<div class="no-preview">Error</div><div class="image-label">Symbol</div>';if(n)n.innerHTML='<div class="no-preview">Error</div><div class="image-label">Footprint</div>'}}async function Dt(t){Z.classList.remove("hidden"),J.innerHTML='<div class="modal-loading">Loading...</div>';try{let e=await fetch(`/api/component/${t}`);if(!e.ok)throw Error("Failed to fetch component");let n=await e.json(),i=n.symbolSexpr?ot(n.symbolSexpr):"",o=n.footprintSexpr?rt(n.footprintSexpr):"";J.innerHTML=`
|
|
495
496
|
<div class="modal-header">
|
|
496
|
-
<h2>${
|
|
497
|
+
<h2>${R(n.title)}</h2>
|
|
497
498
|
<div class="modal-uuid">
|
|
498
|
-
<code>${
|
|
499
|
-
<button class="copy-btn modal-copy" data-uuid="${
|
|
499
|
+
<code>${n.uuid}</code>
|
|
500
|
+
<button class="copy-btn modal-copy" data-uuid="${n.uuid}">Copy UUID</button>
|
|
500
501
|
</div>
|
|
501
502
|
</div>
|
|
502
503
|
<div class="modal-previews">
|
|
503
504
|
<div class="modal-preview">
|
|
504
505
|
<h3>Symbol (KiCad)</h3>
|
|
505
|
-
<div class="preview-svg">${
|
|
506
|
+
<div class="preview-svg">${i||'<div class="no-preview">No symbol preview</div>'}</div>
|
|
506
507
|
</div>
|
|
507
508
|
<div class="modal-preview">
|
|
508
509
|
<h3>Footprint (KiCad)</h3>
|
|
509
|
-
<div class="preview-svg">${
|
|
510
|
+
<div class="preview-svg">${o||'<div class="no-preview">No footprint preview</div>'}</div>
|
|
510
511
|
</div>
|
|
511
512
|
</div>
|
|
512
513
|
<div class="modal-info">
|
|
513
|
-
<p><strong>Description:</strong> ${
|
|
514
|
-
${
|
|
514
|
+
<p><strong>Description:</strong> ${R(n.description||"N/A")}</p>
|
|
515
|
+
${n.model3d?"<p><strong>3D Model:</strong> Available</p>":""}
|
|
515
516
|
</div>
|
|
516
|
-
`;let
|
|
517
|
-
<button class="page-btn" ${
|
|
517
|
+
`;let s=J.querySelector(".modal-copy");if(s)s.addEventListener("click",()=>{Et(n.uuid,s)})}catch(e){console.error("Modal error:",e),J.innerHTML='<div class="modal-error">Failed to load component details</div>'}}function lt(){Z.classList.add("hidden")}function zt(t){if(t.totalPages<=1){at.innerHTML="";return}at.innerHTML=`
|
|
518
|
+
<button class="page-btn" ${t.hasPrev?"":"disabled"} data-page="${t.page-1}">
|
|
518
519
|
← Prev
|
|
519
520
|
</button>
|
|
520
|
-
<span class="page-info">Page ${
|
|
521
|
-
<button class="page-btn" ${
|
|
521
|
+
<span class="page-info">Page ${t.page} of ${t.totalPages}</span>
|
|
522
|
+
<button class="page-btn" ${t.hasNext?"":"disabled"} data-page="${t.page+1}">
|
|
522
523
|
Next →
|
|
523
524
|
</button>
|
|
524
|
-
`,
|
|
525
|
+
`,at.querySelectorAll(".page-btn").forEach((e)=>{e.addEventListener("click",()=>{let n=parseInt(e.dataset.page||"1",10);if(n>0)ut=n,_(),window.scrollTo(0,0)})})}async function Et(t,e){try{await navigator.clipboard.writeText(t),e.classList.add("copied"),setTimeout(()=>e.classList.remove("copied"),1500)}catch{let n=document.createElement("input");n.value=t,document.body.appendChild(n),n.select(),document.execCommand("copy"),document.body.removeChild(n),e.classList.add("copied"),setTimeout(()=>e.classList.remove("copied"),1500)}}function bt(t){Xt.classList.toggle("hidden",!t)}function R(t){let e=document.createElement("div");return e.textContent=t,e.innerHTML}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",wt);else wt();
|
|
525
526
|
|
|
526
527
|
</script>
|
|
527
528
|
</body>
|