@tscircuit/3d-viewer 0.0.598 → 0.0.600

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.
Files changed (2) hide show
  1. package/dist/index.js +1015 -28
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -26343,6 +26343,292 @@ var FootprintPlatedHole = ({
26343
26343
  }
26344
26344
  throw new Error("Shape not supported: " + hole.shape);
26345
26345
  };
26346
+ function LGA(p) {
26347
+ for (const k of [
26348
+ "bodyWidth",
26349
+ "bodyLength",
26350
+ "bodyHeight",
26351
+ "pitch",
26352
+ "landWidth",
26353
+ "landLength",
26354
+ "landThickness"
26355
+ ])
26356
+ if (!Number.isFinite(p[k]) || p[k] <= 0)
26357
+ throw new Error(`${k} must be finite and positive`);
26358
+ for (const k of ["landsPerSideX", "landsPerSideY"])
26359
+ if (!Number.isInteger(p[k]) || p[k] < 0)
26360
+ throw new Error(`${k} must be a nonnegative integer`);
26361
+ if (!Number.isFinite(p.edgeInset) || p.edgeInset < 0 || p.landThickness >= p.bodyHeight || p.landWidth >= p.pitch || p.landsPerSideX + p.landsPerSideY < 1)
26362
+ throw new Error("Invalid land layout or thickness");
26363
+ const layouts = [
26364
+ {
26365
+ count: p.landsPerSideX,
26366
+ across: p.bodyWidth,
26367
+ along: p.bodyLength,
26368
+ horizontal: true
26369
+ },
26370
+ {
26371
+ count: p.landsPerSideY,
26372
+ across: p.bodyLength,
26373
+ along: p.bodyWidth,
26374
+ horizontal: false
26375
+ }
26376
+ ];
26377
+ for (const l of layouts) {
26378
+ if (l.count === 0) continue;
26379
+ const row = (l.count - 1) * p.pitch + p.landWidth;
26380
+ if (row >= l.along - 2 * p.edgeInset || 2 * (p.edgeInset + p.landLength) >= l.across)
26381
+ throw new Error(
26382
+ "Lands must fit below the body without opposite-row overlap"
26383
+ );
26384
+ const other = l.horizontal ? p.landsPerSideY : p.landsPerSideX;
26385
+ if (other > 0 && row >= l.along - 2 * (p.edgeInset + p.landLength) && (other - 1) * p.pitch + p.landWidth >= l.across - 2 * (p.edgeInset + p.landLength))
26386
+ throw new Error("Adjacent land rows overlap at corners");
26387
+ }
26388
+ const r = Math.min(p.bodyWidth, p.bodyLength) * 0.035;
26389
+ return /* @__PURE__ */ jsxs(Fragment2, { children: [
26390
+ /* @__PURE__ */ jsx2(Colorize, { color: "#353535", children: /* @__PURE__ */ jsxs(Subtract, { children: [
26391
+ /* @__PURE__ */ jsx2(Translate, { z: (p.bodyHeight + p.landThickness / 2) / 2, children: /* @__PURE__ */ jsx2(
26392
+ Cuboid,
26393
+ {
26394
+ size: [
26395
+ p.bodyWidth,
26396
+ p.bodyLength,
26397
+ p.bodyHeight - p.landThickness / 2
26398
+ ]
26399
+ }
26400
+ ) }),
26401
+ /* @__PURE__ */ jsx2(
26402
+ Translate,
26403
+ {
26404
+ offset: [
26405
+ -p.bodyWidth / 2 + 2 * r,
26406
+ p.bodyLength / 2 - 2 * r,
26407
+ p.bodyHeight
26408
+ ],
26409
+ children: /* @__PURE__ */ jsx2(Cylinder, { radius: r, height: Math.min(0.04, p.bodyHeight / 10) })
26410
+ }
26411
+ )
26412
+ ] }) }),
26413
+ /* @__PURE__ */ jsx2(Colorize, { color: "#c5b987", children: layouts.flatMap(
26414
+ (l) => [-1, 1].flatMap(
26415
+ (sign) => Array.from({ length: l.count }, (_, i) => {
26416
+ const across = sign * (l.across / 2 - p.edgeInset - p.landLength / 2), along = ((l.count - 1) / 2 - i) * p.pitch;
26417
+ return /* @__PURE__ */ jsx2(
26418
+ Translate,
26419
+ {
26420
+ offset: [
26421
+ l.horizontal ? across : along,
26422
+ l.horizontal ? along : across,
26423
+ p.landThickness / 2
26424
+ ],
26425
+ children: /* @__PURE__ */ jsx2(
26426
+ Cuboid,
26427
+ {
26428
+ size: l.horizontal ? [p.landLength, p.landWidth, p.landThickness] : [p.landWidth, p.landLength, p.landThickness]
26429
+ }
26430
+ )
26431
+ },
26432
+ `${l.horizontal}-${sign}-${i}`
26433
+ );
26434
+ })
26435
+ )
26436
+ ) })
26437
+ ] });
26438
+ }
26439
+ function createDualRowGullWing(p) {
26440
+ const {
26441
+ pinCount,
26442
+ pitch,
26443
+ bodyWidth,
26444
+ bodyLength: bodyLength10,
26445
+ bodyHeight,
26446
+ standoff,
26447
+ leadSpan,
26448
+ leadWidth,
26449
+ leadThickness,
26450
+ contactLength,
26451
+ taperInset,
26452
+ exposedPadWidth = 0,
26453
+ exposedPadLength = 0
26454
+ } = p;
26455
+ for (const key of [
26456
+ "pitch",
26457
+ "bodyWidth",
26458
+ "bodyLength",
26459
+ "bodyHeight",
26460
+ "leadSpan",
26461
+ "leadWidth",
26462
+ "leadThickness",
26463
+ "contactLength"
26464
+ ])
26465
+ if (!Number.isFinite(p[key]) || p[key] <= 0)
26466
+ throw new Error(`${key} must be finite and positive`);
26467
+ for (const [name, value] of Object.entries({
26468
+ standoff,
26469
+ taperInset,
26470
+ exposedPadWidth,
26471
+ exposedPadLength
26472
+ }))
26473
+ if (!Number.isFinite(value) || value < 0)
26474
+ throw new Error(`${name} must be finite and nonnegative`);
26475
+ if (!Number.isInteger(pinCount) || pinCount < 4 || pinCount % 2)
26476
+ throw new Error("pinCount must be even and at least 4");
26477
+ const moldHeight = bodyHeight - standoff;
26478
+ const reach = (leadSpan - bodyWidth) / 2;
26479
+ const rowLength = (pinCount / 2 - 1) * pitch + leadWidth;
26480
+ if (moldHeight <= leadThickness * 2 || leadWidth >= pitch || rowLength >= bodyLength10 - 2 * taperInset || taperInset * 2 >= Math.min(bodyWidth, bodyLength10) || contactLength >= reach)
26481
+ throw new Error("Invalid mold, lead spacing, or terminal reach");
26482
+ if (exposedPadWidth === 0 !== (exposedPadLength === 0) || exposedPadWidth >= bodyWidth - 2 * taperInset || exposedPadLength >= bodyLength10 - 2 * taperInset)
26483
+ throw new Error("Exposed pad must fit inside the bottom mold face");
26484
+ const midZ = standoff + moldHeight / 2;
26485
+ const slice = Math.min(2e-3, moldHeight / 100);
26486
+ const body = /* @__PURE__ */ jsxs(Hull, { children: [
26487
+ /* @__PURE__ */ jsx2(Translate, { z: standoff + slice / 2, children: /* @__PURE__ */ jsx2(
26488
+ Cuboid,
26489
+ {
26490
+ size: [
26491
+ bodyWidth - 2 * taperInset,
26492
+ bodyLength10 - 2 * taperInset,
26493
+ slice
26494
+ ]
26495
+ }
26496
+ ) }),
26497
+ /* @__PURE__ */ jsx2(Translate, { z: midZ, children: /* @__PURE__ */ jsx2(Cuboid, { size: [bodyWidth, bodyLength10, slice] }) }),
26498
+ /* @__PURE__ */ jsx2(Translate, { z: bodyHeight - slice / 2, children: /* @__PURE__ */ jsx2(
26499
+ Cuboid,
26500
+ {
26501
+ size: [
26502
+ bodyWidth - 2 * taperInset,
26503
+ bodyLength10 - 2 * taperInset,
26504
+ slice
26505
+ ]
26506
+ }
26507
+ ) })
26508
+ ] });
26509
+ const dimpleRadius = Math.min(bodyWidth, bodyLength10) * 0.045;
26510
+ const dimpleDepth = Math.min(0.04, moldHeight / 10);
26511
+ const lead = (side, y) => {
26512
+ const outer = leadSpan / 2;
26513
+ const root = bodyWidth / 2 - Math.min(bodyWidth / 8, 0.15);
26514
+ const bendStart = outer - contactLength;
26515
+ const bendEnd = bodyWidth / 2 + Math.min(0.08, (reach - contactLength) / 4);
26516
+ const points = [
26517
+ [outer - slice / 2, leadThickness / 2],
26518
+ [bendStart, leadThickness / 2]
26519
+ ];
26520
+ for (let i = 1; i <= 12; i++) {
26521
+ const t = i / 12;
26522
+ points.push([
26523
+ bendStart + (bendEnd - bendStart) * t,
26524
+ leadThickness / 2 + (midZ - leadThickness / 2) * t * t * (3 - 2 * t)
26525
+ ]);
26526
+ }
26527
+ points.push([root, midZ]);
26528
+ const section = ([x, z22]) => /* @__PURE__ */ jsx2(Translate, { offset: { x: side * x, y, z: z22 }, children: /* @__PURE__ */ jsx2(Cuboid, { size: [slice, leadWidth, leadThickness] }) });
26529
+ return /* @__PURE__ */ jsx2(Union, { children: points.slice(1).map((point, i) => /* @__PURE__ */ jsxs(Hull, { children: [
26530
+ section(points[i]),
26531
+ section(point)
26532
+ ] }, i)) });
26533
+ };
26534
+ return /* @__PURE__ */ jsxs(Fragment2, { children: [
26535
+ /* @__PURE__ */ jsx2(Colorize, { color: "#353535", children: /* @__PURE__ */ jsxs(Subtract, { children: [
26536
+ body,
26537
+ /* @__PURE__ */ jsx2(
26538
+ Translate,
26539
+ {
26540
+ offset: {
26541
+ x: -bodyWidth / 2 + taperInset + dimpleRadius * 2,
26542
+ y: bodyLength10 / 2 - taperInset - dimpleRadius * 2,
26543
+ z: bodyHeight
26544
+ },
26545
+ children: /* @__PURE__ */ jsx2(Cylinder, { radius: dimpleRadius, height: dimpleDepth * 2 })
26546
+ }
26547
+ )
26548
+ ] }) }),
26549
+ /* @__PURE__ */ jsx2(Colorize, { color: "#bfc1c4", children: Array.from(
26550
+ { length: pinCount },
26551
+ (_, i) => lead(
26552
+ i < pinCount / 2 ? -1 : 1,
26553
+ ((pinCount / 2 - 1) / 2 - i % (pinCount / 2)) * pitch
26554
+ )
26555
+ ) }),
26556
+ exposedPadWidth > 0 && /* @__PURE__ */ jsx2(Colorize, { color: "#bfc1c4", children: /* @__PURE__ */ jsx2(Translate, { z: (standoff + leadThickness) / 2, children: /* @__PURE__ */ jsx2(
26557
+ Cuboid,
26558
+ {
26559
+ size: [
26560
+ exposedPadWidth,
26561
+ exposedPadLength,
26562
+ standoff + leadThickness
26563
+ ]
26564
+ }
26565
+ ) }) })
26566
+ ] });
26567
+ }
26568
+ var SSOP = (props) => createDualRowGullWing(props);
26569
+ function renderFootprinterBodyModel(p) {
26570
+ if (p.bodywidth === void 0 && p.bodyheight === void 0 && p.bodythickness === void 0)
26571
+ return null;
26572
+ if (p.fn === "ssop") {
26573
+ const bodyWidth = p.bodywidth ?? p.w * 0.55;
26574
+ const bodyLength10 = p.bodyheight ?? (p.num_pins / 2 - 1) * p.p + p.pw + 0.4;
26575
+ const thickness = p.bodythickness ?? 1;
26576
+ const leadWidth = Math.min(p.pw, p.p * 0.5);
26577
+ const rowLength = (p.num_pins / 2 - 1) * p.p + leadWidth;
26578
+ if (bodyLength10 <= rowLength)
26579
+ throw new Error("SSOP bodyheight must contain the lead row");
26580
+ const leadSpan = Math.max(p.w + 0.2 + p.pl / 2, bodyWidth + p.p);
26581
+ const standoff = Math.min(0.1, thickness / 10);
26582
+ return /* @__PURE__ */ jsx2(
26583
+ SSOP,
26584
+ {
26585
+ pinCount: p.num_pins,
26586
+ pitch: p.p,
26587
+ bodyWidth,
26588
+ bodyLength: bodyLength10,
26589
+ bodyHeight: thickness + standoff,
26590
+ standoff,
26591
+ leadSpan,
26592
+ leadWidth,
26593
+ leadThickness: Math.min(0.15, thickness / 4),
26594
+ contactLength: Math.min(p.pl / 2, (leadSpan - bodyWidth) / 4),
26595
+ taperInset: Math.min(0.12, (bodyLength10 - rowLength) / 4, bodyWidth / 8)
26596
+ }
26597
+ );
26598
+ }
26599
+ if (p.fn === "lga") {
26600
+ const bodyWidth = p.bodywidth ?? p.w;
26601
+ const bodyLength10 = p.bodyheight ?? p.h;
26602
+ const thickness = p.bodythickness ?? 1;
26603
+ const grid2 = p.grid;
26604
+ const landWidth = Math.min(p.pw, p.p / 2);
26605
+ const roomX = bodyWidth - ((grid2.y - 1) * p.p + landWidth);
26606
+ const roomY = bodyLength10 - ((grid2.x - 1) * p.p + landWidth);
26607
+ const landLength = Math.min(
26608
+ p.pl / 2,
26609
+ bodyWidth / 8,
26610
+ bodyLength10 / 8,
26611
+ roomX / 4,
26612
+ roomY / 4
26613
+ );
26614
+ return /* @__PURE__ */ jsx2(
26615
+ LGA,
26616
+ {
26617
+ bodyWidth,
26618
+ bodyLength: bodyLength10,
26619
+ bodyHeight: thickness,
26620
+ landsPerSideX: grid2.x,
26621
+ landsPerSideY: grid2.y,
26622
+ pitch: p.p,
26623
+ landWidth,
26624
+ landLength,
26625
+ edgeInset: 0,
26626
+ landThickness: Math.min(0.015, thickness / 10)
26627
+ }
26628
+ );
26629
+ }
26630
+ return null;
26631
+ }
26346
26632
  var ChipBody = ({
26347
26633
  center,
26348
26634
  width: width10,
@@ -26591,7 +26877,7 @@ var SmdChipLead = (props) => {
26591
26877
  const polygon2 = getExpandedStroke(points, thickness);
26592
26878
  return /* @__PURE__ */ jsx2(Colorize, { color: "#fff", children: /* @__PURE__ */ jsx2(Translate, { offset: { z: 0, y: 0, x: 0, ...props.position }, children: /* @__PURE__ */ jsx2(Rotate, { rotation: ["90deg", 0, rotation ?? 0], children: /* @__PURE__ */ jsx2(Translate, { offset: { x: 0, y: 0, z: -width10 / 2 }, children: /* @__PURE__ */ jsx2(ExtrudeLinear, { height: width10, children: /* @__PURE__ */ jsx2(Polygon, { points: polygon2.map((p) => [p.x, p.y]) }) }) }) }) }) });
26593
26879
  };
26594
- var Tssop = ({
26880
+ var LegacyTssop = ({
26595
26881
  pinCount,
26596
26882
  leadLength,
26597
26883
  leadWidth,
@@ -26669,7 +26955,8 @@ var Tssop = ({
26669
26955
  )
26670
26956
  ] });
26671
26957
  };
26672
- var MSOP = ({
26958
+ var Tssop = (props) => "leadSpan" in props ? createDualRowGullWing(props) : LegacyTssop(props);
26959
+ var LegacyMSOP = ({
26673
26960
  pinCount,
26674
26961
  padContactLength = 0.4,
26675
26962
  leadWidth = 0.2,
@@ -26727,6 +27014,7 @@ var MSOP = ({
26727
27014
  )
26728
27015
  ] });
26729
27016
  };
27017
+ var MSOP = (props) => "leadSpan" in props ? createDualRowGullWing(props) : LegacyMSOP(props);
26730
27018
  var fullLength = 1;
26731
27019
  var width = 0.5;
26732
27020
  var height = 0.5;
@@ -27105,6 +27393,103 @@ var PinRow = ({
27105
27393
  );
27106
27394
  }) });
27107
27395
  };
27396
+ function createPhysicalQfn(p) {
27397
+ const {
27398
+ num_pins,
27399
+ bodyWidth,
27400
+ bodyLength: bodyLength10,
27401
+ bodyHeight,
27402
+ standoff,
27403
+ terminalSpanX,
27404
+ terminalSpanY,
27405
+ terminalThickness,
27406
+ padWidth,
27407
+ padLength,
27408
+ pitch,
27409
+ exposedPadWidth,
27410
+ exposedPadLength
27411
+ } = p;
27412
+ for (const key of [
27413
+ "bodyWidth",
27414
+ "bodyLength",
27415
+ "bodyHeight",
27416
+ "terminalSpanX",
27417
+ "terminalSpanY",
27418
+ "terminalThickness",
27419
+ "padWidth",
27420
+ "padLength",
27421
+ "pitch",
27422
+ "exposedPadWidth",
27423
+ "exposedPadLength"
27424
+ ])
27425
+ if (!Number.isFinite(p[key]) || p[key] <= 0)
27426
+ throw new Error(`${key} must be finite and positive`);
27427
+ if (!Number.isInteger(num_pins) || num_pins < 8 || num_pins % 4)
27428
+ throw new Error("num_pins must be divisible by four and at least eight");
27429
+ if (!Number.isFinite(standoff) || standoff < 0)
27430
+ throw new Error("standoff must be finite and nonnegative");
27431
+ const rowLength = (num_pins / 4 - 1) * pitch + padWidth;
27432
+ if (terminalThickness <= standoff || terminalThickness >= bodyHeight || padWidth >= pitch || rowLength >= Math.min(bodyWidth, bodyLength10))
27433
+ throw new Error("Invalid mold, terminal thickness or spacing");
27434
+ if (terminalSpanX < bodyWidth || terminalSpanY < bodyLength10 || terminalSpanX / 2 - padLength >= bodyWidth / 2 || terminalSpanY / 2 - padLength >= bodyLength10 / 2 || exposedPadWidth >= Math.min(bodyWidth, terminalSpanX - 2 * padLength) || exposedPadLength >= Math.min(bodyLength10, terminalSpanY - 2 * padLength) || rowLength >= Math.min(terminalSpanX, terminalSpanY) - 2 * padLength)
27435
+ throw new Error(
27436
+ "Terminals must enter the mold and clear the exposed pad and corners"
27437
+ );
27438
+ const body = /* @__PURE__ */ jsx2(Translate, { z: (bodyHeight + standoff) / 2, children: /* @__PURE__ */ jsx2(Cuboid, { size: [bodyWidth, bodyLength10, bodyHeight - standoff] }) });
27439
+ const radius = Math.min(bodyWidth, bodyLength10) * 0.035;
27440
+ return /* @__PURE__ */ jsxs(Fragment2, { children: [
27441
+ /* @__PURE__ */ jsx2(Colorize, { color: "#353535", children: /* @__PURE__ */ jsxs(Subtract, { children: [
27442
+ body,
27443
+ /* @__PURE__ */ jsx2(
27444
+ Translate,
27445
+ {
27446
+ offset: {
27447
+ x: -bodyWidth / 2 + radius * 2,
27448
+ y: bodyLength10 / 2 - radius * 2,
27449
+ z: bodyHeight
27450
+ },
27451
+ children: /* @__PURE__ */ jsx2(
27452
+ Cylinder,
27453
+ {
27454
+ radius,
27455
+ height: Math.min(0.06, (bodyHeight - standoff) / 5)
27456
+ }
27457
+ )
27458
+ }
27459
+ )
27460
+ ] }) }),
27461
+ /* @__PURE__ */ jsx2(Colorize, { color: "#bfc1c4", children: Array.from({ length: num_pins }, (_, i) => {
27462
+ const side = Math.floor(i / (num_pins / 4));
27463
+ const along = ((num_pins / 4 - 1) / 2 - i % (num_pins / 4)) * pitch;
27464
+ const horizontal = side % 2 === 0;
27465
+ const sign = side < 2 ? -1 : 1;
27466
+ const across = ((horizontal ? terminalSpanX : terminalSpanY) - padLength) / 2 * sign;
27467
+ return /* @__PURE__ */ jsx2(
27468
+ Translate,
27469
+ {
27470
+ offset: {
27471
+ x: horizontal ? across : along,
27472
+ y: horizontal ? along : across,
27473
+ z: terminalThickness / 2
27474
+ },
27475
+ children: /* @__PURE__ */ jsx2(
27476
+ Cuboid,
27477
+ {
27478
+ size: horizontal ? [padLength, padWidth, terminalThickness] : [padWidth, padLength, terminalThickness]
27479
+ }
27480
+ )
27481
+ },
27482
+ i
27483
+ );
27484
+ }) }),
27485
+ /* @__PURE__ */ jsx2(Colorize, { color: "#bfc1c4", children: /* @__PURE__ */ jsx2(Translate, { z: terminalThickness / 2, children: /* @__PURE__ */ jsx2(
27486
+ Cuboid,
27487
+ {
27488
+ size: [exposedPadWidth, exposedPadLength, terminalThickness]
27489
+ }
27490
+ ) }) })
27491
+ ] });
27492
+ }
27108
27493
  var getQuadCoords2 = (params) => {
27109
27494
  const SIDES_CCW2 = ["left", "bottom", "right", "top"];
27110
27495
  const { pin_count, pn, w, h: h2, p, pl, legsoutside } = params;
@@ -27188,7 +27573,7 @@ var getQuadPinMap2 = ({
27188
27573
  }
27189
27574
  return pin_map;
27190
27575
  };
27191
- var QFN = ({
27576
+ var LegacyQFN = ({
27192
27577
  num_pins = 16,
27193
27578
  bodyWidth = 9,
27194
27579
  bodyLength: bodyLength10 = 9,
@@ -27258,6 +27643,7 @@ var QFN = ({
27258
27643
  )
27259
27644
  ] });
27260
27645
  };
27646
+ var QFN = (props) => "bodyHeight" in props ? createPhysicalQfn(props) : LegacyQFN(props);
27261
27647
  var qfn_default = QFN;
27262
27648
  var SOT235 = () => {
27263
27649
  const fullWidth = 2.8;
@@ -28002,7 +28388,7 @@ var PushButtonLeg = (props) => {
28002
28388
  }
28003
28389
  ) });
28004
28390
  };
28005
- var SOIC = ({
28391
+ var LegacySOIC = ({
28006
28392
  pinCount,
28007
28393
  leadLength,
28008
28394
  leadWidth,
@@ -28062,6 +28448,7 @@ var SOIC = ({
28062
28448
  )
28063
28449
  ] });
28064
28450
  };
28451
+ var SOIC = (props) => "leadSpan" in props ? createDualRowGullWing(props) : LegacySOIC(props);
28065
28452
  var VSSOP = ({
28066
28453
  pinCount,
28067
28454
  pitch,
@@ -29110,7 +29497,140 @@ var SOT323 = () => {
29110
29497
  )
29111
29498
  ] });
29112
29499
  };
29113
- var LQFP = ({
29500
+ function createPhysicalLqfp(p) {
29501
+ const {
29502
+ pinCount,
29503
+ pitch,
29504
+ bodyWidth,
29505
+ bodyLength: bodyLength10,
29506
+ bodyHeight,
29507
+ standoff,
29508
+ leadSpanX,
29509
+ leadSpanY,
29510
+ leadWidth,
29511
+ leadThickness,
29512
+ contactLength,
29513
+ taperInset
29514
+ } = p;
29515
+ for (const key of [
29516
+ "pitch",
29517
+ "bodyWidth",
29518
+ "bodyLength",
29519
+ "bodyHeight",
29520
+ "leadSpanX",
29521
+ "leadSpanY",
29522
+ "leadWidth",
29523
+ "leadThickness",
29524
+ "contactLength"
29525
+ ])
29526
+ if (!Number.isFinite(p[key]) || p[key] <= 0)
29527
+ throw new Error(`${key} must be finite and positive`);
29528
+ for (const [name, value] of Object.entries({
29529
+ standoff,
29530
+ taperInset
29531
+ }))
29532
+ if (!Number.isFinite(value) || value < 0)
29533
+ throw new Error(`${name} must be finite and nonnegative`);
29534
+ if (!Number.isInteger(pinCount) || pinCount < 8 || pinCount % 4)
29535
+ throw new Error("pinCount must be divisible by four and at least eight");
29536
+ const moldHeight = bodyHeight - standoff;
29537
+ const reach = Math.min(leadSpanX - bodyWidth, leadSpanY - bodyLength10) / 2;
29538
+ const rowLength = (pinCount / 4 - 1) * pitch + leadWidth;
29539
+ if (moldHeight <= leadThickness * 2 || leadWidth >= pitch || rowLength >= Math.min(bodyWidth, bodyLength10) - 2 * taperInset || taperInset * 2 >= Math.min(bodyWidth, bodyLength10) || contactLength >= reach)
29540
+ throw new Error("Invalid mold, lead spacing, or terminal reach");
29541
+ const midZ = standoff + moldHeight / 2;
29542
+ const slice = Math.min(2e-3, moldHeight / 100);
29543
+ const body = /* @__PURE__ */ jsxs(Hull, { children: [
29544
+ /* @__PURE__ */ jsx2(Translate, { z: standoff + slice / 2, children: /* @__PURE__ */ jsx2(
29545
+ Cuboid,
29546
+ {
29547
+ size: [
29548
+ bodyWidth - 2 * taperInset,
29549
+ bodyLength10 - 2 * taperInset,
29550
+ slice
29551
+ ]
29552
+ }
29553
+ ) }),
29554
+ /* @__PURE__ */ jsx2(Translate, { z: midZ, children: /* @__PURE__ */ jsx2(Cuboid, { size: [bodyWidth, bodyLength10, slice] }) }),
29555
+ /* @__PURE__ */ jsx2(Translate, { z: bodyHeight - slice / 2, children: /* @__PURE__ */ jsx2(
29556
+ Cuboid,
29557
+ {
29558
+ size: [
29559
+ bodyWidth - 2 * taperInset,
29560
+ bodyLength10 - 2 * taperInset,
29561
+ slice
29562
+ ]
29563
+ }
29564
+ ) })
29565
+ ] });
29566
+ const dimpleRadius = Math.min(bodyWidth, bodyLength10) * 0.045;
29567
+ const dimpleDepth = Math.min(0.04, moldHeight / 10);
29568
+ const lead = (side, along) => {
29569
+ const horizontal = side % 2 === 0;
29570
+ const sign = side < 2 ? -1 : 1;
29571
+ const width10 = horizontal ? bodyWidth : bodyLength10;
29572
+ const outer = (horizontal ? leadSpanX : leadSpanY) / 2;
29573
+ const root = width10 / 2 - Math.min(width10 / 8, 0.15);
29574
+ const bendStart = outer - contactLength;
29575
+ const bendEnd = width10 / 2 + Math.min(0.08, (reach - contactLength) / 4);
29576
+ const points = [
29577
+ [outer - slice / 2, leadThickness / 2],
29578
+ [bendStart, leadThickness / 2]
29579
+ ];
29580
+ for (let i = 1; i <= 12; i++) {
29581
+ const t = i / 12;
29582
+ points.push([
29583
+ bendStart + (bendEnd - bendStart) * t,
29584
+ leadThickness / 2 + (midZ - leadThickness / 2) * t * t * (3 - 2 * t)
29585
+ ]);
29586
+ }
29587
+ points.push([root, midZ]);
29588
+ const section = ([x, z22]) => /* @__PURE__ */ jsx2(
29589
+ Translate,
29590
+ {
29591
+ offset: {
29592
+ x: horizontal ? sign * x : along,
29593
+ y: horizontal ? along : sign * x,
29594
+ z: z22
29595
+ },
29596
+ children: /* @__PURE__ */ jsx2(
29597
+ Cuboid,
29598
+ {
29599
+ size: horizontal ? [slice, leadWidth, leadThickness] : [leadWidth, slice, leadThickness]
29600
+ }
29601
+ )
29602
+ }
29603
+ );
29604
+ return /* @__PURE__ */ jsx2(Union, { children: points.slice(1).map((point, i) => /* @__PURE__ */ jsxs(Hull, { children: [
29605
+ section(points[i]),
29606
+ section(point)
29607
+ ] }, i)) });
29608
+ };
29609
+ return /* @__PURE__ */ jsxs(Fragment2, { children: [
29610
+ /* @__PURE__ */ jsx2(Colorize, { color: "#353535", children: /* @__PURE__ */ jsxs(Subtract, { children: [
29611
+ body,
29612
+ /* @__PURE__ */ jsx2(
29613
+ Translate,
29614
+ {
29615
+ offset: {
29616
+ x: -bodyWidth / 2 + taperInset + dimpleRadius * 2,
29617
+ y: bodyLength10 / 2 - taperInset - dimpleRadius * 2,
29618
+ z: bodyHeight
29619
+ },
29620
+ children: /* @__PURE__ */ jsx2(Cylinder, { radius: dimpleRadius, height: dimpleDepth * 2 })
29621
+ }
29622
+ )
29623
+ ] }) }),
29624
+ /* @__PURE__ */ jsx2(Colorize, { color: "#bfc1c4", children: Array.from(
29625
+ { length: pinCount },
29626
+ (_, i) => lead(
29627
+ Math.floor(i / (pinCount / 4)),
29628
+ ((pinCount / 4 - 1) / 2 - i % (pinCount / 4)) * pitch
29629
+ )
29630
+ ) })
29631
+ ] });
29632
+ }
29633
+ var LegacyLQFP = ({
29114
29634
  pinCount,
29115
29635
  pitch,
29116
29636
  leadWidth,
@@ -29220,6 +29740,7 @@ var LQFP = ({
29220
29740
  )
29221
29741
  ] });
29222
29742
  };
29743
+ var LQFP = (props) => "leadSpanX" in props ? createPhysicalLqfp(props) : LegacyLQFP(props);
29223
29744
  var SOT723 = () => {
29224
29745
  const bodyWidth = 0.85;
29225
29746
  const bodyLength10 = 1.2;
@@ -29292,8 +29813,41 @@ var DFN = ({
29292
29813
  padWidth = bodyLength10 / 5.3 * 0.6,
29293
29814
  padLength = bodyLength10 / 5.3 * 1,
29294
29815
  pitch = 0.5,
29295
- thermalPadThickness = 0.2
29816
+ thermalPadThickness = 0.2,
29817
+ bodyStyle = "chamfered",
29818
+ standoff = 0,
29819
+ terminalInset = 0,
29820
+ terminalThickness = thermalPadThickness,
29821
+ pin1TerminalChamfer = 0,
29822
+ pin1MarkWidth = 0
29296
29823
  }) => {
29824
+ if (bodyStyle === "rectangular") {
29825
+ if (!Number.isInteger(num_pins) || num_pins < 2 || num_pins % 2 !== 0)
29826
+ throw new Error("DFN requires an even pin count of at least two");
29827
+ for (const value of [
29828
+ bodyWidth,
29829
+ bodyLength10,
29830
+ bodyThickness,
29831
+ padWidth,
29832
+ padLength,
29833
+ pitch,
29834
+ terminalThickness
29835
+ ])
29836
+ if (!Number.isFinite(value) || value <= 0)
29837
+ throw new Error("DFN dimensions must be finite and positive");
29838
+ for (const value of [
29839
+ standoff,
29840
+ terminalInset,
29841
+ pin1TerminalChamfer,
29842
+ pin1MarkWidth
29843
+ ])
29844
+ if (!Number.isFinite(value) || value < 0)
29845
+ throw new Error("DFN offsets must be finite and nonnegative");
29846
+ if (terminalThickness <= standoff || terminalThickness > standoff + bodyThickness || terminalInset + padLength >= bodyWidth / 2 || (num_pins / 2 - 1) * pitch + padWidth > bodyLength10 || num_pins > 2 && padWidth >= pitch || pin1TerminalChamfer >= Math.min(padLength, padWidth / 2) || pin1MarkWidth >= bodyWidth / 4)
29847
+ throw new Error(
29848
+ "DFN dimensions leave disconnected or overlapping terminals"
29849
+ );
29850
+ }
29297
29851
  const pinPositions = [];
29298
29852
  const pinsPerSide = Math.floor(num_pins / 2);
29299
29853
  const pinSpan = pitch * (pinsPerSide - 1);
@@ -29303,10 +29857,88 @@ var DFN = ({
29303
29857
  const y = pinSpan / 2 - indexOnSide * pitch;
29304
29858
  const padSizeX = padLength;
29305
29859
  const padSizeY = padWidth;
29306
- const x = side === "left" ? -bodyWidth / 2 + padSizeX / 2 : bodyWidth / 2 - padSizeX / 2;
29860
+ const x = side === "left" ? -bodyWidth / 2 + padSizeX / 2 + terminalInset : bodyWidth / 2 - padSizeX / 2 - terminalInset;
29307
29861
  const pinNumber = i + 1;
29308
29862
  pinPositions.push({ pinNumber, x, y, padSizeX, padSizeY });
29309
29863
  }
29864
+ if (bodyStyle === "rectangular") {
29865
+ const top = standoff + bodyThickness;
29866
+ const inkThickness = Math.min(bodyThickness, bodyLength10) / 100;
29867
+ const stripe = /* @__PURE__ */ jsx2(
29868
+ Cuboid,
29869
+ {
29870
+ size: [pin1MarkWidth, bodyLength10 * 0.8, inkThickness],
29871
+ center: [
29872
+ -bodyWidth / 2 + pin1MarkWidth * 1.5,
29873
+ 0,
29874
+ top - inkThickness / 2
29875
+ ]
29876
+ }
29877
+ );
29878
+ const body = /* @__PURE__ */ jsx2(
29879
+ Cuboid,
29880
+ {
29881
+ size: [bodyWidth, bodyLength10, bodyThickness],
29882
+ center: [0, 0, standoff + bodyThickness / 2]
29883
+ }
29884
+ );
29885
+ return /* @__PURE__ */ jsxs(Fragment2, { children: [
29886
+ /* @__PURE__ */ jsx2(Colorize, { color: "#25272a", children: pin1MarkWidth > 0 ? /* @__PURE__ */ jsxs(Subtract, { children: [
29887
+ body,
29888
+ stripe
29889
+ ] }) : body }),
29890
+ /* @__PURE__ */ jsx2(Colorize, { color: "#c7c9cd", children: pinPositions.map(
29891
+ (p) => p.pinNumber === 1 && pin1TerminalChamfer > 0 ? /* @__PURE__ */ jsxs(Hull, { children: [
29892
+ /* @__PURE__ */ jsx2(
29893
+ Cuboid,
29894
+ {
29895
+ size: [
29896
+ padLength - pin1TerminalChamfer,
29897
+ padWidth,
29898
+ terminalThickness
29899
+ ],
29900
+ center: [
29901
+ p.x + pin1TerminalChamfer / 2,
29902
+ p.y,
29903
+ terminalThickness / 2
29904
+ ]
29905
+ }
29906
+ ),
29907
+ /* @__PURE__ */ jsx2(
29908
+ Cuboid,
29909
+ {
29910
+ size: [
29911
+ padLength,
29912
+ padWidth - 2 * pin1TerminalChamfer,
29913
+ terminalThickness
29914
+ ],
29915
+ center: [p.x, p.y, terminalThickness / 2]
29916
+ }
29917
+ )
29918
+ ] }, p.pinNumber) : /* @__PURE__ */ jsx2(
29919
+ Cuboid,
29920
+ {
29921
+ size: [p.padSizeX, p.padSizeY, terminalThickness],
29922
+ center: [p.x, p.y, terminalThickness / 2]
29923
+ },
29924
+ p.pinNumber
29925
+ )
29926
+ ) }),
29927
+ thermalPadSize && /* @__PURE__ */ jsx2(
29928
+ Cuboid,
29929
+ {
29930
+ color: "#c7c9cd",
29931
+ size: [
29932
+ thermalPadSize.width,
29933
+ thermalPadSize.length,
29934
+ thermalPadThickness
29935
+ ],
29936
+ center: [0, 0, thermalPadThickness / 2]
29937
+ }
29938
+ ),
29939
+ pin1MarkWidth > 0 && /* @__PURE__ */ jsx2(Colorize, { color: "#969997", children: stripe })
29940
+ ] });
29941
+ }
29310
29942
  return /* @__PURE__ */ jsxs(Fragment2, { children: [
29311
29943
  /* @__PURE__ */ jsx2(
29312
29944
  ChipBody,
@@ -29483,36 +30115,50 @@ var MicroMELF = ({
29483
30115
  var MINIMELF = ({
29484
30116
  bodyLength: bodyLength10 = 3.5,
29485
30117
  bodyDiameter = 1.5,
29486
- color = "#3a3a3aff",
29487
- contactColor = "#c6c6c6"
30118
+ color = "#202020",
30119
+ contactColor = "#c6c6c6",
30120
+ bandColor = "#f2f2f2"
29488
30121
  }) => {
29489
- const padLength = 0.5;
30122
+ const contactLength = 0.4;
30123
+ const glassLength = bodyLength10 - 2 * contactLength + 0.04;
30124
+ const contactOffset = (bodyLength10 - contactLength) / 2;
30125
+ const contactRadius = bodyDiameter / 2 + 0.02;
30126
+ const bandLength = 0.14;
30127
+ const bandOffset = -bodyLength10 / 2 + contactLength + 0.35;
29490
30128
  return /* @__PURE__ */ jsxs(Fragment2, { children: [
29491
30129
  /* @__PURE__ */ jsx2(Colorize, { color, children: /* @__PURE__ */ jsx2(Rotate, { rotation: [0, "90deg", 0], children: /* @__PURE__ */ jsx2(
29492
30130
  RoundedCylinder,
29493
30131
  {
29494
- height: bodyLength10,
30132
+ height: glassLength,
29495
30133
  radius: bodyDiameter / 2,
29496
- roundRadius: 0.3,
30134
+ roundRadius: 0.05,
29497
30135
  center: [-bodyDiameter / 2, 0, 0]
29498
30136
  }
29499
30137
  ) }) }),
30138
+ /* @__PURE__ */ jsx2(Colorize, { color: bandColor, children: /* @__PURE__ */ jsx2(Rotate, { rotation: [0, "90deg", 0], children: /* @__PURE__ */ jsx2(
30139
+ Cylinder,
30140
+ {
30141
+ height: bandLength,
30142
+ radius: bodyDiameter / 2 + 2e-3,
30143
+ center: [-bodyDiameter / 2, 0, bandOffset]
30144
+ }
30145
+ ) }) }),
29500
30146
  /* @__PURE__ */ jsx2(Colorize, { color: contactColor, children: /* @__PURE__ */ jsx2(Rotate, { rotation: [0, "90deg", 0], children: /* @__PURE__ */ jsx2(
29501
30147
  RoundedCylinder,
29502
30148
  {
29503
- height: padLength,
29504
- radius: bodyDiameter / 2,
29505
- roundRadius: 0.2,
29506
- center: [-bodyDiameter / 2, 0, -bodyLength10 / 2]
30149
+ height: contactLength,
30150
+ radius: contactRadius,
30151
+ roundRadius: 0.04,
30152
+ center: [-bodyDiameter / 2, 0, -contactOffset]
29507
30153
  }
29508
30154
  ) }) }),
29509
30155
  /* @__PURE__ */ jsx2(Colorize, { color: contactColor, children: /* @__PURE__ */ jsx2(Rotate, { rotation: [0, "90deg", 0], children: /* @__PURE__ */ jsx2(
29510
30156
  RoundedCylinder,
29511
30157
  {
29512
- height: padLength,
29513
- radius: bodyDiameter / 2,
29514
- roundRadius: 0.2,
29515
- center: [-bodyDiameter / 2, 0, bodyLength10 / 2]
30158
+ height: contactLength,
30159
+ radius: contactRadius,
30160
+ roundRadius: 0.04,
30161
+ center: [-bodyDiameter / 2, 0, contactOffset]
29516
30162
  }
29517
30163
  ) }) })
29518
30164
  ] });
@@ -31311,6 +31957,103 @@ var JSTXH2_5mm = ({
31311
31957
  })()
31312
31958
  ] });
31313
31959
  };
31960
+ function isJstShFootprint(footprint) {
31961
+ const p = fp.string(footprint).json();
31962
+ if (p.fn !== "jst") return false;
31963
+ return Boolean(
31964
+ p.sh || p.smd && Math.abs(mm(p.p) - 1) < 0.01 && Math.abs(mm(p.mpx ?? 0) - (p.num_pins - 1 + 2.6)) < 0.15 && Math.abs(mm(p.mpy ?? 0) - 2.525) < 0.15
31965
+ );
31966
+ }
31967
+ var JstSh = ({ footprint = "jst6_sh" }) => {
31968
+ if (!isJstShFootprint(footprint))
31969
+ throw new Error(
31970
+ "JstSh requires a named SH or matching 1mm SH top-entry footprint"
31971
+ );
31972
+ const canonical = footprint.replace(/_pin1location\([^)]*\)/g, "");
31973
+ const params = fp.string(canonical).json();
31974
+ const pads = fp.string(canonical).circuitJson().filter((e) => e.type === "pcb_smtpad" && e.shape === "rect");
31975
+ const target = fp.string(footprint).circuitJson().filter(
31976
+ (e) => e.type === "pcb_smtpad" && (e.shape === "rect" || e.shape === "rotated_rect")
31977
+ );
31978
+ const n = params.num_pins;
31979
+ if (!Number.isInteger(n) || n < 2 || n > 20 || pads.length !== n + 2)
31980
+ throw new Error("JstSh requires 2\u201320 contacts and two hold-down pads");
31981
+ const signals = pads.slice(0, n), mounts = pads.slice(n);
31982
+ const angle = Math.atan2(target[1].y - target[0].y, target[1].x - target[0].x) - Math.atan2(pads[1].y - pads[0].y, pads[1].x - pads[0].x);
31983
+ const x = signals.reduce((sum, p) => sum + p.x, 0) / n, rowY = signals[0].y;
31984
+ const direction = mounts[0].y > rowY ? 1 : -1;
31985
+ const y = rowY + direction * 1.8, width10 = n + 2, depth = 2.9, height10 = 4.2;
31986
+ const pinY = y - direction * 0.25;
31987
+ return /* @__PURE__ */ jsxs(Rotate, { rotation: [0, 0, angle], children: [
31988
+ /* @__PURE__ */ jsx2(Colorize, { color: "#e4e1d6", children: /* @__PURE__ */ jsxs(Subtract, { children: [
31989
+ /* @__PURE__ */ jsx2(
31990
+ Cuboid,
31991
+ {
31992
+ size: [width10, depth, height10],
31993
+ center: [x, y, height10 / 2 + 0.05]
31994
+ }
31995
+ ),
31996
+ /* @__PURE__ */ jsx2(
31997
+ Cuboid,
31998
+ {
31999
+ size: [width10 - 1.3, 1.8, height10],
32000
+ center: [x, y, height10 / 2 + 0.8]
32001
+ }
32002
+ ),
32003
+ mounts.map((pad2, i) => /* @__PURE__ */ jsx2(
32004
+ Cuboid,
32005
+ {
32006
+ size: [0.7, pad2.height * 0.8, 1.5],
32007
+ center: [pad2.x, pad2.y, 0.75]
32008
+ },
32009
+ `recess-${i}`
32010
+ )),
32011
+ [-1, 1].map((side) => /* @__PURE__ */ jsx2(
32012
+ Cuboid,
32013
+ {
32014
+ size: [0.55, 0.6, 1],
32015
+ center: [
32016
+ x + side * (width10 / 2 - 0.9),
32017
+ y + direction * depth / 2,
32018
+ 4
32019
+ ]
32020
+ },
32021
+ side
32022
+ ))
32023
+ ] }) }),
32024
+ /* @__PURE__ */ jsxs(Colorize, { color: "#c6c7c9", children: [
32025
+ signals.map((pad2, i) => {
32026
+ const tailY = pad2.y - direction * pad2.height * 0.4;
32027
+ return /* @__PURE__ */ jsxs(Union, { children: [
32028
+ /* @__PURE__ */ jsx2(
32029
+ Cuboid,
32030
+ {
32031
+ size: [0.2, Math.abs(pinY - tailY) + 0.1, 0.2],
32032
+ center: [pad2.x, (pinY + tailY) / 2, 0.1]
32033
+ }
32034
+ ),
32035
+ /* @__PURE__ */ jsx2(Cuboid, { size: [0.2, 0.2, 3.45], center: [pad2.x, pinY, 1.725] })
32036
+ ] }, i);
32037
+ }),
32038
+ mounts.map((pad2, i) => /* @__PURE__ */ jsxs(Union, { children: [
32039
+ /* @__PURE__ */ jsx2(
32040
+ Cuboid,
32041
+ {
32042
+ size: [0.7, pad2.height * 0.75, 0.2],
32043
+ center: [pad2.x, pad2.y, 0.1]
32044
+ }
32045
+ ),
32046
+ /* @__PURE__ */ jsx2(
32047
+ Cuboid,
32048
+ {
32049
+ size: [0.2, pad2.height * 0.6, 1.35],
32050
+ center: [pad2.x, pad2.y, 0.675]
32051
+ }
32052
+ )
32053
+ ] }, `mount-${i}`))
32054
+ ] })
32055
+ ] });
32056
+ };
31314
32057
  var getTerminalPoints = ([centerX, centerY], width10, length64, chamfer, isPinOne) => {
31315
32058
  const xDirection = Math.sign(centerX);
31316
32059
  const yDirection = Math.sign(centerY);
@@ -32486,6 +33229,81 @@ var SmdPushButton = ({
32486
33229
  )
32487
33230
  ] });
32488
33231
  };
33232
+ var SmdSlideSwitch = ({
33233
+ footprint = "smdslideswitch7",
33234
+ position = 0
33235
+ }) => {
33236
+ const canonical = footprint.replace(/_pin1location\([^)]*\)/g, "");
33237
+ const params = fp.string(canonical).json();
33238
+ const elements = fp.string(canonical).circuitJson();
33239
+ const pads = elements.filter(
33240
+ (e) => e.type === "pcb_smtpad" && e.shape === "rect"
33241
+ );
33242
+ const target = fp.string(footprint).circuitJson().filter(
33243
+ (e) => e.type === "pcb_smtpad" && (e.shape === "rect" || e.shape === "rotated_rect")
33244
+ );
33245
+ if (pads.length < 7 || !Number.isFinite(params.mpx) || params.mpx < 3)
33246
+ throw new Error("SmdSlideSwitch needs signal pads and four mounting pads");
33247
+ const angle = Math.atan2(target[1].y - target[0].y, target[1].x - target[0].x) - Math.atan2(pads[1].y - pads[0].y, pads[1].x - pads[0].x);
33248
+ const width10 = params.mpx - 0.5, depth = 2.7, height10 = params.mpx > 6 ? 1.5 : 1.4, y = params.mounty;
33249
+ return /* @__PURE__ */ jsxs(Rotate, { rotation: [0, 0, angle], children: [
33250
+ /* @__PURE__ */ jsx2(Colorize, { color: "#aeb4ba", children: /* @__PURE__ */ jsxs(Subtract, { children: [
33251
+ /* @__PURE__ */ jsx2(Cuboid, { size: [width10, depth, height10], center: [0, y, height10 / 2] }),
33252
+ /* @__PURE__ */ jsx2(
33253
+ Cuboid,
33254
+ {
33255
+ size: [width10 - 0.3, depth - 0.3, height10],
33256
+ center: [0, y, height10 / 2 - 0.15]
33257
+ }
33258
+ ),
33259
+ /* @__PURE__ */ jsx2(Cuboid, { size: [2.7, 0.5, 0.9], center: [0, y - depth / 2, 0.65] })
33260
+ ] }) }),
33261
+ /* @__PURE__ */ jsxs(Colorize, { color: "#28292c", children: [
33262
+ /* @__PURE__ */ jsx2(
33263
+ Cuboid,
33264
+ {
33265
+ size: [width10 - 0.32, depth - 0.32, 0.35],
33266
+ center: [0, y, 0.175]
33267
+ }
33268
+ ),
33269
+ /* @__PURE__ */ jsx2(
33270
+ Cuboid,
33271
+ {
33272
+ size: [1, 2.25, 0.65],
33273
+ center: [position === 0 ? -0.5 : 0.5, y - 1.5, 0.65]
33274
+ }
33275
+ ),
33276
+ elements.filter((e) => e.type === "pcb_hole").map(
33277
+ (hole, i) => "hole_diameter" in hole && "x" in hole ? /* @__PURE__ */ jsx2(
33278
+ Cylinder,
33279
+ {
33280
+ radius: hole.hole_diameter * 0.42,
33281
+ height: 0.6,
33282
+ center: [hole.x, hole.y, -0.2]
33283
+ },
33284
+ i
33285
+ ) : null
33286
+ )
33287
+ ] }),
33288
+ /* @__PURE__ */ jsx2(Colorize, { color: "#c6c7c9", children: pads.map((pad2, i) => {
33289
+ const signal = i < pads.length - 4;
33290
+ const outerY = pad2.y + pad2.height * 0.4;
33291
+ const innerY = Math.min(pad2.y - pad2.height * 0.4, y + depth / 2 - 0.2);
33292
+ return /* @__PURE__ */ jsx2(
33293
+ Cuboid,
33294
+ {
33295
+ size: [
33296
+ pad2.width * 0.8,
33297
+ signal ? outerY - innerY : pad2.height * 0.8,
33298
+ 0.18
33299
+ ],
33300
+ center: [pad2.x, signal ? (outerY + innerY) / 2 : pad2.y, 0.09]
33301
+ },
33302
+ i
33303
+ );
33304
+ }) })
33305
+ ] });
33306
+ };
32489
33307
  var SOT563 = ({ fullWidth = 1.94, fullLength: fullLength10 = 1.6 }) => {
32490
33308
  const bodyWidth = 1.2;
32491
33309
  const bodyLength10 = 1.6;
@@ -32566,6 +33384,154 @@ var BGA = ({
32566
33384
  })
32567
33385
  ] });
32568
33386
  };
33387
+ var Capsule = ({
33388
+ width: width10,
33389
+ height: height10,
33390
+ depth
33391
+ }) => /* @__PURE__ */ jsx2(Hull, { children: [-1, 1].map((side) => /* @__PURE__ */ jsx2(Translate, { offset: [side * (width10 - height10) / 2, 0, 0], children: /* @__PURE__ */ jsx2(Rotate, { rotation: [Math.PI / 2, 0, 0], children: /* @__PURE__ */ jsx2(Cylinder, { radius: height10 / 2, height: depth }) }) }, side)) });
33392
+ var UsbCMidmount = ({
33393
+ footprint = "usbcmidmount16"
33394
+ }) => {
33395
+ const canonical = footprint.replace(/_pin1location\([^)]*\)/g, "");
33396
+ const params = fp.string(canonical).json();
33397
+ const elements = fp.string(canonical).circuitJson();
33398
+ const pads = elements.filter(
33399
+ (e) => e.type === "pcb_smtpad" && e.shape === "rect"
33400
+ );
33401
+ const slots = elements.filter((e) => e.type === "pcb_plated_hole");
33402
+ const holes = elements.filter((e) => e.type === "pcb_hole");
33403
+ const target = fp.string(footprint).circuitJson().find((e) => e.type === "pcb_plated_hole");
33404
+ const first = slots[0];
33405
+ const angle = first && target && "x" in target ? Math.atan2(target.y, target.x) - Math.atan2(first.y, first.x) : 0;
33406
+ const width10 = 8.94, height10 = 3.2, wall = 0.22;
33407
+ const back = params.rowy + 0.23, front = -params.bodybottom;
33408
+ const depth = back - front, centerY = (back + front) / 2, centerZ = 1.65;
33409
+ if (!Number.isFinite(depth) || depth < 3 || slots.length !== 4)
33410
+ throw new Error(
33411
+ "UsbCMidmount requires four mounting slots and a positive body depth"
33412
+ );
33413
+ const tabBottom = -0.9, tabTop = centerZ + 0.1;
33414
+ return /* @__PURE__ */ jsxs(Rotate, { rotation: [0, 0, angle], children: [
33415
+ /* @__PURE__ */ jsx2(Colorize, { color: "#b8bdc4", children: /* @__PURE__ */ jsx2(Translate, { offset: [0, centerY, centerZ], children: /* @__PURE__ */ jsxs(Subtract, { children: [
33416
+ /* @__PURE__ */ jsx2(Capsule, { width: width10, height: height10, depth }),
33417
+ /* @__PURE__ */ jsx2(
33418
+ Capsule,
33419
+ {
33420
+ width: width10 - wall * 2,
33421
+ height: height10 - wall * 2,
33422
+ depth: depth + 0.2
33423
+ }
33424
+ )
33425
+ ] }) }) }),
33426
+ /* @__PURE__ */ jsxs(Colorize, { color: "#242529", children: [
33427
+ /* @__PURE__ */ jsx2(
33428
+ Cuboid,
33429
+ {
33430
+ size: [6.65, depth - 1.1, 0.7],
33431
+ center: [0, centerY + 0.3, centerZ]
33432
+ }
33433
+ ),
33434
+ /* @__PURE__ */ jsx2(Cuboid, { size: [7.6, 1, 2.45], center: [0, back - 0.65, centerZ] }),
33435
+ holes.map(
33436
+ (hole, i) => "x" in hole && "hole_diameter" in hole ? /* @__PURE__ */ jsx2(
33437
+ Cylinder,
33438
+ {
33439
+ radius: hole.hole_diameter * 0.42,
33440
+ height: 0.8,
33441
+ center: [hole.x, hole.y, -0.3]
33442
+ },
33443
+ i
33444
+ ) : null
33445
+ )
33446
+ ] }),
33447
+ /* @__PURE__ */ jsxs(Colorize, { color: "#d3ad57", children: [
33448
+ [-1, 1].flatMap(
33449
+ (side) => [-2.75, -2.25, -1.25, -0.25, 0.25, 1.25, 2.25, 2.75].map((x) => /* @__PURE__ */ jsx2(
33450
+ Cuboid,
33451
+ {
33452
+ size: [0.24, depth - 2, 0.04],
33453
+ center: [x, centerY + 0.1, centerZ + side * 0.365]
33454
+ },
33455
+ `${side}-${x}`
33456
+ ))
33457
+ ),
33458
+ pads.map((pad2, i) => /* @__PURE__ */ jsx2(
33459
+ Cuboid,
33460
+ {
33461
+ size: [
33462
+ Math.min(pad2.width * 0.7, 0.35),
33463
+ pad2.height * 0.8 + 0.35,
33464
+ 0.15
33465
+ ],
33466
+ center: [pad2.x, pad2.y - 0.175, 0.075]
33467
+ },
33468
+ i
33469
+ ))
33470
+ ] }),
33471
+ /* @__PURE__ */ jsx2(Colorize, { color: "#b8bdc4", children: slots.map((slot, i) => /* @__PURE__ */ jsx2(
33472
+ Cuboid,
33473
+ {
33474
+ size: [
33475
+ 0.4,
33476
+ "hole_height" in slot ? Math.max(0.3, slot.hole_height - 0.2) : 1,
33477
+ tabTop - tabBottom
33478
+ ],
33479
+ center: [slot.x, slot.y, (tabTop + tabBottom) / 2]
33480
+ },
33481
+ i
33482
+ )) })
33483
+ ] });
33484
+ };
33485
+ var getPads = (footprint) => fp.string(footprint).circuitJson().filter(
33486
+ (element) => element.type === "pcb_smtpad" && (element.shape === "rect" || element.shape === "rotated_rect")
33487
+ ).map((pad2) => ({ x: pad2.x, y: pad2.y, pin: pad2.port_hints?.[0] }));
33488
+ var SOT143 = ({ footprint = "sot143" }) => {
33489
+ const pads = getPads(footprint);
33490
+ if (pads.length !== 4) throw new Error("SOT143 requires four signal pads");
33491
+ const canonical = getPads("sot143");
33492
+ const options = fp.string(footprint).json();
33493
+ const origin = canonical[1];
33494
+ const target = pads.find((pad2) => pad2.pin === "2");
33495
+ const angle = options.pin1location ? Math.round(
33496
+ (Math.atan2(target.y, target.x) - Math.atan2(origin.y, origin.x)) / (Math.PI / 2)
33497
+ ) * (Math.PI / 2) : 0;
33498
+ const local = pads.map((pad2) => ({
33499
+ ...pad2,
33500
+ x: pad2.x * Math.cos(angle) + pad2.y * Math.sin(angle),
33501
+ y: -pad2.x * Math.sin(angle) + pad2.y * Math.cos(angle)
33502
+ }));
33503
+ return /* @__PURE__ */ jsxs(Rotate, { rotation: [0, 0, angle], children: [
33504
+ /* @__PURE__ */ jsx2(
33505
+ ChipBody,
33506
+ {
33507
+ width: 2.9,
33508
+ length: 1.3,
33509
+ height: 0.95,
33510
+ heightAboveSurface: 0.05,
33511
+ center: { x: 0, y: 0, z: 0 },
33512
+ includeNotch: false,
33513
+ taperRatio: 0.06
33514
+ }
33515
+ ),
33516
+ local.map((pad2) => {
33517
+ const side = pad2.y < 0 ? -1 : 1;
33518
+ const leadWidth = pad2.pin === "1" ? 0.83 : 0.43;
33519
+ return /* @__PURE__ */ jsx2(
33520
+ SmdChipLead,
33521
+ {
33522
+ width: leadWidth,
33523
+ thickness: 0.12,
33524
+ height: 0.45,
33525
+ padContactLength: 0.3,
33526
+ bodyDistance: 0.6,
33527
+ rotation: side < 0 ? Math.PI / 2 : -Math.PI / 2,
33528
+ position: { x: pad2.x, y: side * 1.15, z: 0.06 }
33529
+ },
33530
+ pad2.pin
33531
+ );
33532
+ })
33533
+ ] });
33534
+ };
32569
33535
  var DEFAULT_ASPECT_RATIO = 16 / 9;
32570
33536
  var DEFAULT_DIAGONAL = 40;
32571
33537
  var EPSILON = 1e-6;
@@ -33138,6 +34104,8 @@ var Footprinter3d = ({ footprint }) => {
33138
34104
  const parsed = mm(value);
33139
34105
  return Number.isFinite(parsed) ? parsed : fallback;
33140
34106
  };
34107
+ const bodyModel = renderFootprinterBodyModel(fpJson);
34108
+ if (bodyModel) return bodyModel;
33141
34109
  switch (fpJson.fn) {
33142
34110
  case "crystal":
33143
34111
  return /* @__PURE__ */ jsx2(
@@ -33340,6 +34308,12 @@ var Footprinter3d = ({ footprint }) => {
33340
34308
  }
33341
34309
  case "sot457":
33342
34310
  return /* @__PURE__ */ jsx2(SOT457, {});
34311
+ case "smdslideswitch":
34312
+ return /* @__PURE__ */ jsx2(SmdSlideSwitch, { footprint: normalizedFootprint });
34313
+ case "usbcmidmount":
34314
+ return /* @__PURE__ */ jsx2(UsbCMidmount, { footprint: normalizedFootprint });
34315
+ case "sot143":
34316
+ return /* @__PURE__ */ jsx2(SOT143, { footprint: normalizedFootprint });
33343
34317
  case "sot223":
33344
34318
  return /* @__PURE__ */ jsx2(SOT223, {});
33345
34319
  case "sot23w":
@@ -33366,6 +34340,9 @@ var Footprinter3d = ({ footprint }) => {
33366
34340
  }
33367
34341
  );
33368
34342
  case "jst":
34343
+ if (isJstShFootprint(normalizedFootprint)) {
34344
+ return /* @__PURE__ */ jsx2(JstSh, { footprint: normalizedFootprint });
34345
+ }
33369
34346
  if (fpJson.zh) {
33370
34347
  return /* @__PURE__ */ jsx2(JSTZH1_5mm, { numPins: fpJson.num_pins });
33371
34348
  }
@@ -33806,11 +34783,8 @@ function renderNode(node, colorCtx, renderCtx) {
33806
34783
  }));
33807
34784
  }
33808
34785
  if (type === Rotate) {
33809
- const rot = Array.isArray(props?.rotation) ? [
33810
- degToRad(props.rotation[0]),
33811
- degToRad(props.rotation[1]),
33812
- degToRad(props.rotation[2])
33813
- ] : [
34786
+ const angles = props?.angles ?? props?.rotation;
34787
+ const rot = Array.isArray(angles) ? [degToRad(angles[0]), degToRad(angles[1]), degToRad(angles[2])] : [
33814
34788
  degToRad(props?.x ?? 0),
33815
34789
  degToRad(props?.y ?? 0),
33816
34790
  degToRad(props?.z ?? 0)
@@ -35765,7 +36739,7 @@ import * as THREE23 from "three";
35765
36739
  // package.json
35766
36740
  var package_default = {
35767
36741
  name: "@tscircuit/3d-viewer",
35768
- version: "0.0.597",
36742
+ version: "0.0.599",
35769
36743
  repository: {
35770
36744
  type: "git",
35771
36745
  url: "https://github.com/tscircuit/3d-viewer"
@@ -35816,6 +36790,7 @@ var package_default = {
35816
36790
  "@biomejs/biome": "^2.1.4",
35817
36791
  "@chromatic-com/storybook": "^1.9.0",
35818
36792
  "@jscad/modeling": "^2.12.5",
36793
+ "@napi-rs/canvas": "^1.0.9",
35819
36794
  "@storybook/blocks": "9.0.0-alpha.17",
35820
36795
  "@storybook/react-vite": "^9.1.5",
35821
36796
  "@tscircuit/alphabet": "^0.0.25",
@@ -35830,7 +36805,7 @@ var package_default = {
35830
36805
  "bun-match-svg": "^0.0.9",
35831
36806
  "bun-types": "1.2.1",
35832
36807
  debug: "^4.4.0",
35833
- "jscad-electronics": "^0.0.159",
36808
+ "jscad-electronics": "^0.0.178",
35834
36809
  "jscad-planner": "^0.0.13",
35835
36810
  jsdom: "^26.0.0",
35836
36811
  "manifold-3d": "^3.2.1",
@@ -40856,10 +41831,12 @@ var createCombinedTexture = ({
40856
41831
  };
40857
41832
  var createMaskedCopperMask = ({
40858
41833
  textures,
41834
+ soldermaskTexture,
40859
41835
  boardData,
40860
41836
  traceTextureResolution
40861
41837
  }) => {
40862
- if (!textures.some((texture) => texture?.image)) return null;
41838
+ if (!soldermaskTexture?.image || !textures.some((texture) => texture?.image))
41839
+ return null;
40863
41840
  const bounds = calculateOutlineBounds(boardData);
40864
41841
  const width10 = Math.floor(bounds.width * traceTextureResolution);
40865
41842
  const height10 = Math.floor(bounds.height * traceTextureResolution);
@@ -40873,6 +41850,15 @@ var createMaskedCopperMask = ({
40873
41850
  if (!texture?.image) continue;
40874
41851
  ctx.drawImage(texture.image, 0, 0, width10, height10);
40875
41852
  }
41853
+ ctx.globalCompositeOperation = "destination-in";
41854
+ ctx.drawImage(
41855
+ soldermaskTexture.image,
41856
+ 0,
41857
+ 0,
41858
+ width10,
41859
+ height10
41860
+ );
41861
+ ctx.globalCompositeOperation = "source-over";
40876
41862
  const maskTexture = new THREE39.CanvasTexture(canvas);
40877
41863
  maskTexture.colorSpace = THREE39.NoColorSpace;
40878
41864
  maskTexture.generateMipmaps = false;
@@ -40999,6 +41985,7 @@ function createCombinedBoardTextures({
40999
41985
  });
41000
41986
  const maskedCopperTexture = showMask && showCopper ? createMaskedCopperMask({
41001
41987
  textures: [traceTexture, maskedCopperPourTexture],
41988
+ soldermaskTexture,
41002
41989
  boardData,
41003
41990
  traceTextureResolution
41004
41991
  }) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.598",
3
+ "version": "0.0.600",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/tscircuit/3d-viewer"
@@ -51,6 +51,7 @@
51
51
  "@biomejs/biome": "^2.1.4",
52
52
  "@chromatic-com/storybook": "^1.9.0",
53
53
  "@jscad/modeling": "^2.12.5",
54
+ "@napi-rs/canvas": "^1.0.9",
54
55
  "@storybook/blocks": "9.0.0-alpha.17",
55
56
  "@storybook/react-vite": "^9.1.5",
56
57
  "@tscircuit/alphabet": "^0.0.25",
@@ -65,7 +66,7 @@
65
66
  "bun-match-svg": "^0.0.9",
66
67
  "bun-types": "1.2.1",
67
68
  "debug": "^4.4.0",
68
- "jscad-electronics": "^0.0.159",
69
+ "jscad-electronics": "^0.0.178",
69
70
  "jscad-planner": "^0.0.13",
70
71
  "jsdom": "^26.0.0",
71
72
  "manifold-3d": "^3.2.1",