@tscircuit/3d-viewer 0.0.233 → 0.0.235

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 +236 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13980,7 +13980,7 @@ var require_browser = __commonJS({
13980
13980
  });
13981
13981
 
13982
13982
  // src/CadViewer.tsx
13983
- import { useState as useState9, useCallback as useCallback4, useRef as useRef7, useEffect as useEffect7 } from "react";
13983
+ import { useState as useState10, useRef as useRef8, useEffect as useEffect8 } from "react";
13984
13984
 
13985
13985
  // src/hooks/use-convert-children-to-soup.ts
13986
13986
  import { Circuit } from "@tscircuit/core";
@@ -16985,7 +16985,7 @@ import { Canvas, useFrame as useFrame2 } from "@react-three/fiber";
16985
16985
  // package.json
16986
16986
  var package_default = {
16987
16987
  name: "@tscircuit/3d-viewer",
16988
- version: "0.0.232",
16988
+ version: "0.0.234",
16989
16989
  main: "./dist/index.js",
16990
16990
  module: "./dist/index.js",
16991
16991
  type: "module",
@@ -17313,7 +17313,7 @@ var colors = {
17313
17313
  var MANIFOLD_Z_OFFSET = 1e-3;
17314
17314
  var SMOOTH_CIRCLE_SEGMENTS = 32;
17315
17315
  var DEFAULT_SMT_PAD_THICKNESS = 0.035;
17316
- var TRACE_TEXTURE_RESOLUTION = 200;
17316
+ var TRACE_TEXTURE_RESOLUTION = 50;
17317
17317
  var boardMaterialColors = {
17318
17318
  fr1: colors.fr1Copper,
17319
17319
  fr4: colors.fr4Green
@@ -18980,12 +18980,12 @@ var CadViewerJscad = forwardRef2(
18980
18980
 
18981
18981
  // src/CadViewerManifold.tsx
18982
18982
  import { useEffect as useEffect6, useState as useState8, useMemo as useMemo7 } from "react";
18983
- import { su as su12 } from "@tscircuit/soup-util";
18983
+ import { su as su13 } from "@tscircuit/soup-util";
18984
18984
  import ManifoldModule from "manifold-3d";
18985
18985
 
18986
18986
  // src/hooks/useManifoldBoardBuilder.ts
18987
18987
  import { useState as useState7, useEffect as useEffect5, useMemo as useMemo6, useRef as useRef6 } from "react";
18988
- import { su as su11 } from "@tscircuit/soup-util";
18988
+ import { su as su12 } from "@tscircuit/soup-util";
18989
18989
  import * as THREE10 from "three";
18990
18990
 
18991
18991
  // src/utils/manifold-mesh-to-three-geometry.ts
@@ -19537,6 +19537,105 @@ function createManifoldBoard(Manifold, CrossSection, boardData, pcbThickness, ma
19537
19537
  return boardOp;
19538
19538
  }
19539
19539
 
19540
+ // src/utils/manifold/process-cutouts.ts
19541
+ import { su as su11 } from "@tscircuit/soup-util";
19542
+ var arePointsClockwise3 = (points) => {
19543
+ let area = 0;
19544
+ for (let i = 0; i < points.length; i++) {
19545
+ const j = (i + 1) % points.length;
19546
+ if (points[i] && points[j]) {
19547
+ area += points[i][0] * points[j][1];
19548
+ area -= points[j][0] * points[i][1];
19549
+ }
19550
+ }
19551
+ const signedArea = area / 2;
19552
+ return signedArea <= 0;
19553
+ };
19554
+ function processCutoutsForManifold(Manifold, CrossSection, circuitJson, pcbThickness, manifoldInstancesForCleanup) {
19555
+ const cutoutOps = [];
19556
+ const pcbCutouts = su11(circuitJson).pcb_cutout.list();
19557
+ for (const cutout of pcbCutouts) {
19558
+ let cutoutOp;
19559
+ const cutoutHeight = pcbThickness * 1.5;
19560
+ switch (cutout.shape) {
19561
+ case "rect":
19562
+ cutoutOp = Manifold.cube(
19563
+ [cutout.width, cutout.height, cutoutHeight],
19564
+ true
19565
+ // centered
19566
+ );
19567
+ manifoldInstancesForCleanup.push(cutoutOp);
19568
+ if (cutout.rotation) {
19569
+ const rotationRadians = cutout.rotation * Math.PI / 180;
19570
+ const rotatedOp = cutoutOp.rotate([0, 0, cutout.rotation]);
19571
+ manifoldInstancesForCleanup.push(rotatedOp);
19572
+ cutoutOp = rotatedOp;
19573
+ }
19574
+ cutoutOp = cutoutOp.translate([
19575
+ cutout.center.x,
19576
+ cutout.center.y,
19577
+ 0
19578
+ // Centered vertically by Manifold.cube, so Z is 0 for board plane
19579
+ ]);
19580
+ manifoldInstancesForCleanup.push(cutoutOp);
19581
+ break;
19582
+ case "circle":
19583
+ cutoutOp = Manifold.cylinder(
19584
+ cutoutHeight,
19585
+ cutout.radius,
19586
+ -1,
19587
+ // default for radiusHigh
19588
+ SMOOTH_CIRCLE_SEGMENTS,
19589
+ true
19590
+ // centered
19591
+ );
19592
+ manifoldInstancesForCleanup.push(cutoutOp);
19593
+ cutoutOp = cutoutOp.translate([cutout.center.x, cutout.center.y, 0]);
19594
+ manifoldInstancesForCleanup.push(cutoutOp);
19595
+ break;
19596
+ case "polygon":
19597
+ if (cutout.points.length < 3) {
19598
+ console.warn(
19599
+ `PCB Cutout [${cutout.pcb_cutout_id}] polygon has fewer than 3 points, skipping.`
19600
+ );
19601
+ continue;
19602
+ }
19603
+ let pointsVec2 = cutout.points.map((p) => [
19604
+ p.x,
19605
+ p.y
19606
+ ]);
19607
+ if (arePointsClockwise3(pointsVec2)) {
19608
+ pointsVec2 = pointsVec2.reverse();
19609
+ }
19610
+ const crossSection = CrossSection.ofPolygons([pointsVec2]);
19611
+ manifoldInstancesForCleanup.push(crossSection);
19612
+ cutoutOp = Manifold.extrude(
19613
+ crossSection,
19614
+ cutoutHeight,
19615
+ 0,
19616
+ // nDivisions
19617
+ 0,
19618
+ // twistDegrees
19619
+ [1, 1],
19620
+ // scaleTop
19621
+ true
19622
+ // center extrusion
19623
+ );
19624
+ manifoldInstancesForCleanup.push(cutoutOp);
19625
+ break;
19626
+ default:
19627
+ console.warn(
19628
+ `Unsupported cutout shape: ${cutout.shape} for cutout ${cutout.pcb_cutout_id}`
19629
+ );
19630
+ continue;
19631
+ }
19632
+ if (cutoutOp) {
19633
+ cutoutOps.push(cutoutOp);
19634
+ }
19635
+ }
19636
+ return { cutoutOps };
19637
+ }
19638
+
19540
19639
  // src/hooks/useManifoldBoardBuilder.ts
19541
19640
  var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
19542
19641
  const [geoms, setGeoms] = useState7(null);
@@ -19547,7 +19646,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
19547
19646
  const manifoldInstancesForCleanup = useRef6([]);
19548
19647
  const boardData = useMemo6(() => {
19549
19648
  if (!circuitJson) return null;
19550
- const boards = su11(circuitJson).pcb_board.list();
19649
+ const boards = su12(circuitJson).pcb_board.list();
19551
19650
  if (boards.length === 0) {
19552
19651
  return null;
19553
19652
  }
@@ -19558,7 +19657,7 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
19558
19657
  setGeoms(null);
19559
19658
  setTextures(null);
19560
19659
  setPcbThickness(null);
19561
- if (circuitJson && su11(circuitJson).pcb_board.list().length === 0) {
19660
+ if (circuitJson && su12(circuitJson).pcb_board.list().length === 0) {
19562
19661
  setError("No pcb_board found in circuitJson.");
19563
19662
  }
19564
19663
  setIsLoading(false);
@@ -19610,9 +19709,23 @@ var useManifoldBoardBuilder = (manifoldJSModule, circuitJson) => {
19610
19709
  if (allBoardDrills.length > 0) {
19611
19710
  const unionedDrills = Manifold.union(allBoardDrills);
19612
19711
  manifoldInstancesForCleanup.current.push(unionedDrills);
19613
- const nextBoard = currentBoardOp.subtract(unionedDrills);
19614
- manifoldInstancesForCleanup.current.push(nextBoard);
19615
- currentBoardOp = nextBoard;
19712
+ const nextBoardAfterDrills = currentBoardOp.subtract(unionedDrills);
19713
+ manifoldInstancesForCleanup.current.push(nextBoardAfterDrills);
19714
+ currentBoardOp = nextBoardAfterDrills;
19715
+ }
19716
+ const { cutoutOps } = processCutoutsForManifold(
19717
+ Manifold,
19718
+ CrossSection,
19719
+ circuitJson,
19720
+ currentPcbThickness,
19721
+ manifoldInstancesForCleanup.current
19722
+ );
19723
+ if (cutoutOps.length > 0) {
19724
+ const unionedCutouts = Manifold.union(cutoutOps);
19725
+ manifoldInstancesForCleanup.current.push(unionedCutouts);
19726
+ const nextBoardAfterCutouts = currentBoardOp.subtract(unionedCutouts);
19727
+ manifoldInstancesForCleanup.current.push(nextBoardAfterCutouts);
19728
+ currentBoardOp = nextBoardAfterCutouts;
19616
19729
  }
19617
19730
  boardManifold = currentBoardOp;
19618
19731
  if (boardManifold) {
@@ -19825,7 +19938,7 @@ var CadViewerManifold = ({
19825
19938
  [textures, boardData, pcbThickness]
19826
19939
  );
19827
19940
  const cadComponents = useMemo7(
19828
- () => circuitJson ? su12(circuitJson).cad_component.list() : [],
19941
+ () => circuitJson ? su13(circuitJson).cad_component.list() : [],
19829
19942
  [circuitJson]
19830
19943
  );
19831
19944
  const initialCameraPosition = useMemo7(() => {
@@ -19905,29 +20018,76 @@ var CadViewerManifold = ({
19905
20018
  };
19906
20019
  var CadViewerManifold_default = CadViewerManifold;
19907
20020
 
19908
- // src/CadViewer.tsx
19909
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
19910
- var CadViewer = (props) => {
19911
- const [engine, setEngine] = useState9("jscad");
20021
+ // src/hooks/useContextMenu.ts
20022
+ import { useState as useState9, useCallback as useCallback4, useRef as useRef7, useEffect as useEffect7 } from "react";
20023
+ var useContextMenu = ({ containerRef }) => {
19912
20024
  const [menuVisible, setMenuVisible] = useState9(false);
19913
20025
  const [menuPos, setMenuPos] = useState9({
19914
20026
  x: 0,
19915
20027
  y: 0
19916
20028
  });
19917
- const containerRef = useRef7(null);
19918
20029
  const menuRef = useRef7(null);
19919
- const handleContextMenu = useCallback4((e) => {
19920
- e.preventDefault();
19921
- setMenuPos({ x: e.clientX, y: e.clientY });
19922
- setMenuVisible(true);
20030
+ const interactionOriginPosRef = useRef7(null);
20031
+ const handleContextMenu = useCallback4(
20032
+ (e) => {
20033
+ e.preventDefault();
20034
+ const eventX = typeof e.clientX === "number" ? e.clientX : 0;
20035
+ const eventY = typeof e.clientY === "number" ? e.clientY : 0;
20036
+ if (!interactionOriginPosRef.current) {
20037
+ return;
20038
+ }
20039
+ const { x: originX, y: originY } = interactionOriginPosRef.current;
20040
+ const dx = Math.abs(eventX - originX);
20041
+ const dy = Math.abs(eventY - originY);
20042
+ const swipeThreshold = 10;
20043
+ if (dx > swipeThreshold || dy > swipeThreshold) {
20044
+ interactionOriginPosRef.current = null;
20045
+ return;
20046
+ }
20047
+ setMenuPos({ x: eventX, y: eventY });
20048
+ setMenuVisible(true);
20049
+ interactionOriginPosRef.current = null;
20050
+ },
20051
+ [setMenuPos, setMenuVisible]
20052
+ );
20053
+ const handleTouchStart = useCallback4((e) => {
20054
+ if (e.touches.length === 1) {
20055
+ const touch = e.touches[0];
20056
+ if (touch) {
20057
+ interactionOriginPosRef.current = { x: touch.clientX, y: touch.clientY };
20058
+ } else {
20059
+ interactionOriginPosRef.current = null;
20060
+ }
20061
+ } else {
20062
+ interactionOriginPosRef.current = null;
20063
+ }
20064
+ }, []);
20065
+ const handleTouchMove = useCallback4((e) => {
20066
+ if (!interactionOriginPosRef.current || e.touches.length !== 1) {
20067
+ return;
20068
+ }
20069
+ const touch = e.touches[0];
20070
+ if (touch) {
20071
+ const dx = Math.abs(touch.clientX - interactionOriginPosRef.current.x);
20072
+ const dy = Math.abs(touch.clientY - interactionOriginPosRef.current.y);
20073
+ const swipeThreshold = 10;
20074
+ if (dx > swipeThreshold || dy > swipeThreshold) {
20075
+ interactionOriginPosRef.current = null;
20076
+ }
20077
+ } else {
20078
+ interactionOriginPosRef.current = null;
20079
+ }
20080
+ }, []);
20081
+ const handleTouchEnd = useCallback4(() => {
20082
+ setTimeout(() => {
20083
+ if (interactionOriginPosRef.current) {
20084
+ interactionOriginPosRef.current = null;
20085
+ }
20086
+ }, 0);
19923
20087
  }, []);
19924
- const handleMenuClick = (newEngine) => {
19925
- setEngine(newEngine);
19926
- setMenuVisible(false);
19927
- };
19928
20088
  const handleClickAway = useCallback4((e) => {
19929
20089
  const target = e.target;
19930
- if (!menuRef.current || !menuRef.current.contains(target)) {
20090
+ if (menuRef.current && !menuRef.current.contains(target)) {
19931
20091
  setMenuVisible(false);
19932
20092
  }
19933
20093
  }, []);
@@ -19937,13 +20097,52 @@ var CadViewer = (props) => {
19937
20097
  return () => document.removeEventListener("mousedown", handleClickAway);
19938
20098
  }
19939
20099
  }, [menuVisible, handleClickAway]);
19940
- useEffect7(() => {
20100
+ const contextMenuEventHandlers = {
20101
+ onMouseDown: (e) => {
20102
+ if (e.button === 2) {
20103
+ interactionOriginPosRef.current = { x: e.clientX, y: e.clientY };
20104
+ } else {
20105
+ interactionOriginPosRef.current = null;
20106
+ }
20107
+ },
20108
+ onContextMenu: handleContextMenu,
20109
+ onTouchStart: handleTouchStart,
20110
+ onTouchMove: handleTouchMove,
20111
+ onTouchEnd: handleTouchEnd
20112
+ };
20113
+ return {
20114
+ menuVisible,
20115
+ menuPos,
20116
+ menuRef,
20117
+ contextMenuEventHandlers,
20118
+ setMenuVisible
20119
+ // Expose setMenuVisible for direct control if needed
20120
+ };
20121
+ };
20122
+
20123
+ // src/CadViewer.tsx
20124
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
20125
+ var CadViewer = (props) => {
20126
+ const [engine, setEngine] = useState10("jscad");
20127
+ const containerRef = useRef8(null);
20128
+ const {
20129
+ menuVisible,
20130
+ menuPos,
20131
+ menuRef,
20132
+ contextMenuEventHandlers,
20133
+ setMenuVisible
20134
+ } = useContextMenu({ containerRef });
20135
+ const handleMenuClick = (newEngine) => {
20136
+ setEngine(newEngine);
20137
+ setMenuVisible(false);
20138
+ };
20139
+ useEffect8(() => {
19941
20140
  const stored = window.localStorage.getItem("cadViewerEngine");
19942
20141
  if (stored === "jscad" || stored === "manifold") {
19943
20142
  setEngine(stored);
19944
20143
  }
19945
20144
  }, []);
19946
- useEffect7(() => {
20145
+ useEffect8(() => {
19947
20146
  window.localStorage.setItem("cadViewerEngine", engine);
19948
20147
  }, [engine]);
19949
20148
  const viewerKey = props.circuitJson ? JSON.stringify(props.circuitJson) : void 0;
@@ -19952,7 +20151,7 @@ var CadViewer = (props) => {
19952
20151
  {
19953
20152
  ref: containerRef,
19954
20153
  style: { width: "100%", height: "100%", position: "relative" },
19955
- onContextMenu: handleContextMenu,
20154
+ ...contextMenuEventHandlers,
19956
20155
  children: [
19957
20156
  engine === "jscad" ? /* @__PURE__ */ jsx12(CadViewerJscad, { ...props }) : /* @__PURE__ */ jsx12(CadViewerManifold_default, { ...props }),
19958
20157
  /* @__PURE__ */ jsxs9(
@@ -19970,11 +20169,6 @@ var CadViewer = (props) => {
19970
20169
  opacity: 0.7,
19971
20170
  userSelect: "none"
19972
20171
  },
19973
- onClick: () => {
19974
- if ("ontouchstart" in window) {
19975
- setEngine(engine === "jscad" ? "manifold" : "jscad");
19976
- }
19977
- },
19978
20172
  children: [
19979
20173
  "Engine: ",
19980
20174
  /* @__PURE__ */ jsx12("b", { children: engine === "jscad" ? "JSCAD" : "Manifold" })
@@ -20047,7 +20241,7 @@ var CadViewer = (props) => {
20047
20241
 
20048
20242
  // src/convert-circuit-json-to-3d-svg.ts
20049
20243
  var import_debug = __toESM(require_browser(), 1);
20050
- import { su as su13 } from "@tscircuit/soup-util";
20244
+ import { su as su14 } from "@tscircuit/soup-util";
20051
20245
  import * as THREE16 from "three";
20052
20246
  import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
20053
20247
 
@@ -20262,7 +20456,7 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
20262
20456
  const pointLight = new THREE16.PointLight(16777215, Math.PI / 4);
20263
20457
  pointLight.position.set(-10, -10, 10);
20264
20458
  scene.add(pointLight);
20265
- const components = su13(circuitJson).cad_component.list();
20459
+ const components = su14(circuitJson).cad_component.list();
20266
20460
  for (const component of components) {
20267
20461
  await renderComponent(component, scene);
20268
20462
  }
@@ -20308,7 +20502,7 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
20308
20502
  }
20309
20503
 
20310
20504
  // src/hooks/exporter/gltf.ts
20311
- import { useEffect as useEffect8, useState as useState10, useMemo as useMemo8, useCallback as useCallback5 } from "react";
20505
+ import { useEffect as useEffect9, useState as useState11, useMemo as useMemo8, useCallback as useCallback6 } from "react";
20312
20506
  function useSaveGltfAs(options = {}) {
20313
20507
  const parse = useParser(options);
20314
20508
  const link = useMemo8(() => document.createElement("a"), []);
@@ -20321,7 +20515,7 @@ function useSaveGltfAs(options = {}) {
20321
20515
  link.dispatchEvent(new MouseEvent("click"));
20322
20516
  URL.revokeObjectURL(url);
20323
20517
  };
20324
- useEffect8(
20518
+ useEffect9(
20325
20519
  () => () => {
20326
20520
  link.remove();
20327
20521
  instance = null;
@@ -20329,20 +20523,20 @@ function useSaveGltfAs(options = {}) {
20329
20523
  []
20330
20524
  );
20331
20525
  let instance;
20332
- const ref = useCallback5((obj3D) => {
20526
+ const ref = useCallback6((obj3D) => {
20333
20527
  instance = obj3D;
20334
20528
  }, []);
20335
20529
  return [ref, saveAs];
20336
20530
  }
20337
20531
  function useExportGltfUrl(options = {}) {
20338
20532
  const parse = useParser(options);
20339
- const [url, setUrl] = useState10();
20340
- const [error, setError] = useState10();
20341
- const ref = useCallback5(
20533
+ const [url, setUrl] = useState11();
20534
+ const [error, setError] = useState11();
20535
+ const ref = useCallback6(
20342
20536
  (instance) => parse(instance).then(setUrl).catch(setError),
20343
20537
  []
20344
20538
  );
20345
- useEffect8(() => () => URL.revokeObjectURL(url), [url]);
20539
+ useEffect9(() => () => URL.revokeObjectURL(url), [url]);
20346
20540
  return [ref, url, error];
20347
20541
  }
20348
20542
  function useParser(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.233",
3
+ "version": "0.0.235",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",