@tscircuit/3d-viewer 0.0.438 → 0.0.440

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 +439 -57
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -14228,7 +14228,7 @@ var require_browser = __commonJS({
14228
14228
  });
14229
14229
 
14230
14230
  // src/CadViewer.tsx
14231
- import { useState as useState34, useCallback as useCallback22, useRef as useRef24, useEffect as useEffect40, useMemo as useMemo26 } from "react";
14231
+ import { useState as useState36, useCallback as useCallback22, useRef as useRef26, useEffect as useEffect42, useMemo as useMemo28 } from "react";
14232
14232
  import * as THREE29 from "three";
14233
14233
 
14234
14234
  // src/CadViewerJscad.tsx
@@ -16957,7 +16957,8 @@ var pcb_cutout_rect = pcb_cutout_base.extend({
16957
16957
  center: point,
16958
16958
  width: length,
16959
16959
  height: length,
16960
- rotation: rotation.optional()
16960
+ rotation: rotation.optional(),
16961
+ corner_radius: length.optional()
16961
16962
  });
16962
16963
  expectTypesMatch(true);
16963
16964
  var pcb_cutout_circle = pcb_cutout_base.extend({
@@ -28287,7 +28288,7 @@ import * as THREE15 from "three";
28287
28288
  // package.json
28288
28289
  var package_default = {
28289
28290
  name: "@tscircuit/3d-viewer",
28290
- version: "0.0.437",
28291
+ version: "0.0.439",
28291
28292
  main: "./dist/index.js",
28292
28293
  module: "./dist/index.js",
28293
28294
  type: "module",
@@ -28348,7 +28349,7 @@ var package_default = {
28348
28349
  "@vitejs/plugin-react": "^4.3.4",
28349
28350
  "bun-match-svg": "^0.0.9",
28350
28351
  "bun-types": "1.2.1",
28351
- "circuit-json": "0.0.316",
28352
+ "circuit-json": "0.0.317",
28352
28353
  "circuit-to-svg": "^0.0.179",
28353
28354
  debug: "^4.4.0",
28354
28355
  "jscad-electronics": "^0.0.89",
@@ -30986,10 +30987,33 @@ var BoardGeomBuilder = class {
30986
30987
  const cutoutHeight = this.ctx.pcbThickness * 1.5;
30987
30988
  switch (cutout.shape) {
30988
30989
  case "rect":
30989
- cutoutGeom = (0, import_primitives10.cuboid)({
30990
- center: [cutout.center.x, cutout.center.y, 0],
30991
- size: [cutout.width, cutout.height, cutoutHeight]
30992
- });
30990
+ const rectCornerRadius = clampRectBorderRadius(
30991
+ cutout.width,
30992
+ cutout.height,
30993
+ extractRectBorderRadius(cutout)
30994
+ );
30995
+ if (rectCornerRadius > 0) {
30996
+ const rect2d = (0, import_primitives10.roundedRectangle)({
30997
+ size: [cutout.width, cutout.height],
30998
+ roundRadius: rectCornerRadius,
30999
+ segments: PAD_ROUNDED_SEGMENTS
31000
+ });
31001
+ cutoutGeom = (0, import_extrusions8.extrudeLinear)({ height: cutoutHeight }, rect2d);
31002
+ cutoutGeom = (0, import_transforms8.translate)([0, 0, -cutoutHeight / 2], cutoutGeom);
31003
+ cutoutGeom = (0, import_transforms8.translate)(
31004
+ [cutout.center.x, cutout.center.y, 0],
31005
+ cutoutGeom
31006
+ );
31007
+ } else {
31008
+ const baseCutoutGeom = (0, import_primitives10.cuboid)({
31009
+ center: [0, 0, 0],
31010
+ size: [cutout.width, cutout.height, cutoutHeight]
31011
+ });
31012
+ cutoutGeom = (0, import_transforms8.translate)(
31013
+ [cutout.center.x, cutout.center.y, 0],
31014
+ baseCutoutGeom
31015
+ );
31016
+ }
30993
31017
  if (cutout.rotation) {
30994
31018
  const rotationRadians = cutout.rotation * Math.PI / 180;
30995
31019
  cutoutGeom = (0, import_transforms8.rotateZ)(rotationRadians, cutoutGeom);
@@ -33492,15 +33516,25 @@ function processCutoutsForManifold(Manifold, CrossSection, circuitJson, pcbThick
33492
33516
  let cutoutOp;
33493
33517
  const cutoutHeight = pcbThickness * 1.5;
33494
33518
  switch (cutout.shape) {
33495
- case "rect":
33496
- cutoutOp = Manifold.cube(
33497
- [cutout.width, cutout.height, cutoutHeight],
33498
- true
33499
- // centered
33500
- );
33519
+ case "rect": {
33520
+ const rectCornerRadius = extractRectBorderRadius(cutout);
33521
+ if (typeof rectCornerRadius === "number" && rectCornerRadius > 0) {
33522
+ cutoutOp = createRoundedRectPrism({
33523
+ Manifold,
33524
+ width: cutout.width,
33525
+ height: cutout.height,
33526
+ thickness: cutoutHeight,
33527
+ borderRadius: rectCornerRadius
33528
+ });
33529
+ } else {
33530
+ cutoutOp = Manifold.cube(
33531
+ [cutout.width, cutout.height, cutoutHeight],
33532
+ true
33533
+ // centered
33534
+ );
33535
+ }
33501
33536
  manifoldInstancesForCleanup.push(cutoutOp);
33502
33537
  if (cutout.rotation) {
33503
- const rotationRadians = cutout.rotation * Math.PI / 180;
33504
33538
  const rotatedOp = cutoutOp.rotate([0, 0, cutout.rotation]);
33505
33539
  manifoldInstancesForCleanup.push(rotatedOp);
33506
33540
  cutoutOp = rotatedOp;
@@ -33513,6 +33547,7 @@ function processCutoutsForManifold(Manifold, CrossSection, circuitJson, pcbThick
33513
33547
  ]);
33514
33548
  manifoldInstancesForCleanup.push(cutoutOp);
33515
33549
  break;
33550
+ }
33516
33551
  case "circle":
33517
33552
  cutoutOp = Manifold.cylinder(
33518
33553
  cutoutHeight,
@@ -34468,8 +34503,110 @@ var useGlobalDownloadGltf = () => {
34468
34503
  }, []);
34469
34504
  };
34470
34505
 
34506
+ // src/hooks/useRegisteredHotkey.ts
34507
+ import { useEffect as useEffect25, useMemo as useMemo21, useRef as useRef11, useState as useState18 } from "react";
34508
+ var hotkeyRegistry = /* @__PURE__ */ new Map();
34509
+ var subscribers = /* @__PURE__ */ new Set();
34510
+ var isListenerAttached = false;
34511
+ var matchesKey = (eventKey, targetKey) => {
34512
+ if (!eventKey || !targetKey) return false;
34513
+ return eventKey.toLowerCase() === targetKey.toLowerCase();
34514
+ };
34515
+ var matchesModifiers = (event, modifiers) => {
34516
+ if (!modifiers || modifiers.length === 0) {
34517
+ return !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
34518
+ }
34519
+ const hasCtrl = modifiers.includes("Ctrl");
34520
+ const hasCmd = modifiers.includes("Cmd");
34521
+ const hasShift = modifiers.includes("Shift");
34522
+ const hasAlt = modifiers.includes("Alt");
34523
+ if (hasCtrl && !event.ctrlKey) return false;
34524
+ if (hasCmd && !event.metaKey) return false;
34525
+ if (hasShift && !event.shiftKey) return false;
34526
+ if (hasAlt && !event.altKey) return false;
34527
+ return true;
34528
+ };
34529
+ var isEditableTarget = (target) => {
34530
+ if (!target || typeof target !== "object") return false;
34531
+ const element = target;
34532
+ const tagName = element.tagName;
34533
+ const editableTags = ["INPUT", "TEXTAREA", "SELECT"];
34534
+ if (editableTags.includes(tagName)) {
34535
+ return true;
34536
+ }
34537
+ return Boolean(element.getAttribute?.("contenteditable"));
34538
+ };
34539
+ var handleKeydown = (event) => {
34540
+ if (isEditableTarget(event.target)) {
34541
+ return;
34542
+ }
34543
+ hotkeyRegistry.forEach((entry) => {
34544
+ if (matchesKey(event.key, entry.key) && matchesModifiers(event, entry.modifiers)) {
34545
+ event.preventDefault();
34546
+ entry.invoke();
34547
+ }
34548
+ });
34549
+ };
34550
+ var notifySubscribers = () => {
34551
+ const entries = Array.from(hotkeyRegistry.values());
34552
+ subscribers.forEach((subscriber) => subscriber(entries));
34553
+ };
34554
+ var ensureListener = () => {
34555
+ if (isListenerAttached) return;
34556
+ if (typeof window === "undefined") return;
34557
+ window.addEventListener("keydown", handleKeydown);
34558
+ isListenerAttached = true;
34559
+ };
34560
+ var registerHotkey = (registration) => {
34561
+ hotkeyRegistry.set(registration.id, registration);
34562
+ notifySubscribers();
34563
+ ensureListener();
34564
+ };
34565
+ var unregisterHotkey = (id) => {
34566
+ if (hotkeyRegistry.delete(id)) {
34567
+ notifySubscribers();
34568
+ }
34569
+ };
34570
+ var subscribeToRegistry = (subscriber) => {
34571
+ subscribers.add(subscriber);
34572
+ subscriber(Array.from(hotkeyRegistry.values()));
34573
+ return () => {
34574
+ subscribers.delete(subscriber);
34575
+ };
34576
+ };
34577
+ var useRegisteredHotkey = (id, handler, metadata) => {
34578
+ const handlerRef = useRef11(handler);
34579
+ handlerRef.current = handler;
34580
+ const normalizedMetadata = useMemo21(
34581
+ () => ({
34582
+ key: metadata.key,
34583
+ description: metadata.description,
34584
+ modifiers: metadata.modifiers
34585
+ }),
34586
+ [metadata.key, metadata.description, metadata.modifiers]
34587
+ );
34588
+ useEffect25(() => {
34589
+ const registration = {
34590
+ id,
34591
+ ...normalizedMetadata,
34592
+ invoke: () => handlerRef.current()
34593
+ };
34594
+ registerHotkey(registration);
34595
+ return () => {
34596
+ unregisterHotkey(id);
34597
+ };
34598
+ }, [id, normalizedMetadata]);
34599
+ };
34600
+ var useHotkeyRegistry = () => {
34601
+ const [entries, setEntries] = useState18(
34602
+ () => Array.from(hotkeyRegistry.values())
34603
+ );
34604
+ useEffect25(() => subscribeToRegistry(setEntries), []);
34605
+ return entries;
34606
+ };
34607
+
34471
34608
  // src/components/ContextMenu.tsx
34472
- import { useState as useState33 } from "react";
34609
+ import { useState as useState34 } from "react";
34473
34610
 
34474
34611
  // node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs
34475
34612
  import * as React43 from "react";
@@ -38097,9 +38234,9 @@ function assignRef(ref, value) {
38097
38234
  }
38098
38235
 
38099
38236
  // node_modules/use-callback-ref/dist/es2015/useRef.js
38100
- import { useState as useState28 } from "react";
38237
+ import { useState as useState29 } from "react";
38101
38238
  function useCallbackRef2(initialValue, callback) {
38102
- var ref = useState28(function() {
38239
+ var ref = useState29(function() {
38103
38240
  return {
38104
38241
  // value
38105
38242
  value: initialValue,
@@ -39823,7 +39960,7 @@ var SubTrigger2 = DropdownMenuSubTrigger;
39823
39960
  var SubContent2 = DropdownMenuSubContent;
39824
39961
 
39825
39962
  // src/components/AppearanceMenu.tsx
39826
- import { useState as useState32 } from "react";
39963
+ import { useState as useState33 } from "react";
39827
39964
 
39828
39965
  // src/components/Icons.tsx
39829
39966
  import { jsx as jsx32 } from "react/jsx-runtime";
@@ -39929,8 +40066,8 @@ var iconContainerStyles = {
39929
40066
  };
39930
40067
  var AppearanceMenu = () => {
39931
40068
  const { visibility, toggleLayer } = useLayerVisibility();
39932
- const [appearanceSubOpen, setAppearanceSubOpen] = useState32(false);
39933
- const [hoveredItem, setHoveredItem] = useState32(null);
40069
+ const [appearanceSubOpen, setAppearanceSubOpen] = useState33(false);
40070
+ const [hoveredItem, setHoveredItem] = useState33(null);
39934
40071
  return /* @__PURE__ */ jsxs7(Fragment9, { children: [
39935
40072
  /* @__PURE__ */ jsx33(Separator2, { style: separatorStyles }),
39936
40073
  /* @__PURE__ */ jsxs7(Sub2, { onOpenChange: setAppearanceSubOpen, children: [
@@ -40181,11 +40318,12 @@ var ContextMenu = ({
40181
40318
  onEngineSwitch,
40182
40319
  onCameraPresetSelect,
40183
40320
  onAutoRotateToggle,
40184
- onDownloadGltf
40321
+ onDownloadGltf,
40322
+ onOpenKeyboardShortcuts
40185
40323
  }) => {
40186
40324
  const { cameraType, setCameraType } = useCameraController();
40187
- const [cameraSubOpen, setCameraSubOpen] = useState33(false);
40188
- const [hoveredItem, setHoveredItem] = useState33(null);
40325
+ const [cameraSubOpen, setCameraSubOpen] = useState34(false);
40326
+ const [hoveredItem, setHoveredItem] = useState34(null);
40189
40327
  return /* @__PURE__ */ jsx34(
40190
40328
  "div",
40191
40329
  {
@@ -40378,6 +40516,35 @@ var ContextMenu = ({
40378
40516
  }
40379
40517
  ),
40380
40518
  /* @__PURE__ */ jsx34(Separator2, { style: separatorStyles2 }),
40519
+ /* @__PURE__ */ jsxs8(
40520
+ Item22,
40521
+ {
40522
+ style: {
40523
+ ...itemStyles2,
40524
+ ...itemPaddingStyles2,
40525
+ backgroundColor: hoveredItem === "shortcuts" ? "#404040" : "transparent"
40526
+ },
40527
+ onSelect: onOpenKeyboardShortcuts,
40528
+ onMouseEnter: () => setHoveredItem("shortcuts"),
40529
+ onMouseLeave: () => setHoveredItem(null),
40530
+ onTouchStart: () => setHoveredItem("shortcuts"),
40531
+ children: [
40532
+ /* @__PURE__ */ jsx34("span", { style: { flex: 1, display: "flex", alignItems: "center" }, children: "Keyboard Shortcuts" }),
40533
+ /* @__PURE__ */ jsx34(
40534
+ "div",
40535
+ {
40536
+ style: {
40537
+ ...badgeStyles,
40538
+ display: "flex",
40539
+ alignItems: "center"
40540
+ },
40541
+ children: "Shift+?"
40542
+ }
40543
+ )
40544
+ ]
40545
+ }
40546
+ ),
40547
+ /* @__PURE__ */ jsx34(Separator2, { style: separatorStyles2 }),
40381
40548
  /* @__PURE__ */ jsx34(
40382
40549
  "div",
40383
40550
  {
@@ -40415,23 +40582,216 @@ var ContextMenu = ({
40415
40582
  );
40416
40583
  };
40417
40584
 
40585
+ // src/components/KeyboardShortcutsDialog.tsx
40586
+ import { useEffect as useEffect41, useMemo as useMemo27, useRef as useRef25, useState as useState35 } from "react";
40587
+ import { Fragment as Fragment10, jsx as jsx35, jsxs as jsxs9 } from "react/jsx-runtime";
40588
+ var KeyboardShortcutsDialog = ({
40589
+ open,
40590
+ onClose
40591
+ }) => {
40592
+ const [query, setQuery] = useState35("");
40593
+ const inputRef = useRef25(null);
40594
+ const hotkeys = useHotkeyRegistry();
40595
+ useEffect41(() => {
40596
+ if (!open) return void 0;
40597
+ const handleKeyDown = (event) => {
40598
+ if (event.key === "Escape") {
40599
+ event.preventDefault();
40600
+ onClose();
40601
+ }
40602
+ };
40603
+ window.addEventListener("keydown", handleKeyDown);
40604
+ return () => window.removeEventListener("keydown", handleKeyDown);
40605
+ }, [open, onClose]);
40606
+ useEffect41(() => {
40607
+ if (open) {
40608
+ setTimeout(() => {
40609
+ inputRef.current?.focus();
40610
+ }, 0);
40611
+ }
40612
+ }, [open]);
40613
+ const filteredHotkeys = useMemo27(() => {
40614
+ const normalizedQuery = query.trim().toLowerCase();
40615
+ if (!normalizedQuery) {
40616
+ return hotkeys;
40617
+ }
40618
+ return hotkeys.filter((hotkey) => {
40619
+ const haystack = `${hotkey.key} ${hotkey.description}`;
40620
+ return haystack.toLowerCase().includes(normalizedQuery);
40621
+ });
40622
+ }, [hotkeys, query]);
40623
+ if (!open) {
40624
+ return null;
40625
+ }
40626
+ return /* @__PURE__ */ jsx35(
40627
+ "div",
40628
+ {
40629
+ role: "dialog",
40630
+ "aria-modal": "true",
40631
+ style: {
40632
+ position: "fixed",
40633
+ inset: 0,
40634
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
40635
+ display: "flex",
40636
+ alignItems: "center",
40637
+ justifyContent: "center",
40638
+ zIndex: 9999
40639
+ },
40640
+ onClick: onClose,
40641
+ children: /* @__PURE__ */ jsxs9(
40642
+ "div",
40643
+ {
40644
+ style: {
40645
+ backgroundColor: "#1f1f23",
40646
+ color: "#f8f8ff",
40647
+ borderRadius: 12,
40648
+ width: "min(640px, 90vw)",
40649
+ maxHeight: "80vh",
40650
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.45), 0 8px 20px rgba(0, 0, 0, 0.35)",
40651
+ display: "flex",
40652
+ flexDirection: "column",
40653
+ overflow: "hidden"
40654
+ },
40655
+ onClick: (event) => event.stopPropagation(),
40656
+ children: [
40657
+ /* @__PURE__ */ jsxs9(
40658
+ "header",
40659
+ {
40660
+ style: {
40661
+ padding: "20px 24px 12px",
40662
+ borderBottom: "1px solid rgba(255, 255, 255, 0.08)"
40663
+ },
40664
+ children: [
40665
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
40666
+ /* @__PURE__ */ jsx35(
40667
+ "h2",
40668
+ {
40669
+ style: {
40670
+ margin: 0,
40671
+ fontSize: "1.1rem",
40672
+ fontWeight: 600,
40673
+ letterSpacing: "0.02em"
40674
+ },
40675
+ children: "Keyboard Shortcuts"
40676
+ }
40677
+ ),
40678
+ /* @__PURE__ */ jsx35(
40679
+ "button",
40680
+ {
40681
+ type: "button",
40682
+ onClick: onClose,
40683
+ style: {
40684
+ background: "transparent",
40685
+ border: "none",
40686
+ color: "rgba(255, 255, 255, 0.8)",
40687
+ fontSize: "1rem",
40688
+ cursor: "pointer"
40689
+ },
40690
+ children: "\u2715"
40691
+ }
40692
+ )
40693
+ ] }),
40694
+ /* @__PURE__ */ jsx35(
40695
+ "input",
40696
+ {
40697
+ ref: inputRef,
40698
+ type: "text",
40699
+ placeholder: "Search shortcuts...",
40700
+ value: query,
40701
+ onChange: (event) => setQuery(event.target.value),
40702
+ style: {
40703
+ marginTop: 12,
40704
+ width: "calc(100% - 24px)",
40705
+ padding: "10px 12px",
40706
+ borderRadius: 8,
40707
+ border: "1px solid rgba(255, 255, 255, 0.1)",
40708
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
40709
+ color: "white",
40710
+ fontSize: "0.95rem"
40711
+ }
40712
+ }
40713
+ )
40714
+ ]
40715
+ }
40716
+ ),
40717
+ /* @__PURE__ */ jsx35("div", { style: { overflowY: "auto" }, children: /* @__PURE__ */ jsxs9(
40718
+ "table",
40719
+ {
40720
+ style: {
40721
+ width: "100%",
40722
+ borderCollapse: "collapse",
40723
+ fontSize: "0.95rem"
40724
+ },
40725
+ children: [
40726
+ /* @__PURE__ */ jsx35("thead", { children: /* @__PURE__ */ jsxs9("tr", { style: { textAlign: "left", color: "#a1a1b5" }, children: [
40727
+ /* @__PURE__ */ jsx35("th", { style: { padding: "12px 24px", width: "25%" }, children: "Key" }),
40728
+ /* @__PURE__ */ jsx35("th", { style: { padding: "12px 24px" }, children: "Description" })
40729
+ ] }) }),
40730
+ /* @__PURE__ */ jsx35("tbody", { children: filteredHotkeys.length === 0 ? /* @__PURE__ */ jsx35("tr", { children: /* @__PURE__ */ jsx35(
40731
+ "td",
40732
+ {
40733
+ colSpan: 2,
40734
+ style: { padding: "24px", textAlign: "center" },
40735
+ children: "No shortcuts found"
40736
+ }
40737
+ ) }) : filteredHotkeys.map((hotkey) => /* @__PURE__ */ jsxs9(
40738
+ "tr",
40739
+ {
40740
+ style: { borderTop: "1px solid rgba(255, 255, 255, 0.05)" },
40741
+ children: [
40742
+ /* @__PURE__ */ jsx35("td", { style: { padding: "12px 24px" }, children: /* @__PURE__ */ jsx35(
40743
+ "span",
40744
+ {
40745
+ style: {
40746
+ display: "inline-flex",
40747
+ alignItems: "center",
40748
+ justifyContent: "center",
40749
+ border: "1px solid rgba(255, 255, 255, 0.3)",
40750
+ borderRadius: 6,
40751
+ minWidth: 36,
40752
+ padding: "4px 8px",
40753
+ fontFamily: "monospace",
40754
+ fontSize: "0.95rem"
40755
+ },
40756
+ children: hotkey.modifiers?.length ? /* @__PURE__ */ jsxs9(Fragment10, { children: [
40757
+ hotkey.modifiers.map((mod) => `${mod}+`).join(""),
40758
+ hotkey.key.toUpperCase()
40759
+ ] }) : hotkey.key.toUpperCase()
40760
+ }
40761
+ ) }),
40762
+ /* @__PURE__ */ jsx35("td", { style: { padding: "12px 24px" }, children: hotkey.description })
40763
+ ]
40764
+ },
40765
+ hotkey.id
40766
+ )) })
40767
+ ]
40768
+ }
40769
+ ) })
40770
+ ]
40771
+ }
40772
+ )
40773
+ }
40774
+ );
40775
+ };
40776
+
40418
40777
  // src/CadViewer.tsx
40419
- import { jsx as jsx35, jsxs as jsxs9 } from "react/jsx-runtime";
40778
+ import { jsx as jsx36, jsxs as jsxs10 } from "react/jsx-runtime";
40420
40779
  var CadViewerInner = (props) => {
40421
- const [engine, setEngine] = useState34("manifold");
40422
- const containerRef = useRef24(null);
40423
- const [autoRotate, setAutoRotate] = useState34(() => {
40780
+ const [engine, setEngine] = useState36("manifold");
40781
+ const containerRef = useRef26(null);
40782
+ const [isKeyboardShortcutsDialogOpen, setIsKeyboardShortcutsDialogOpen] = useState36(false);
40783
+ const [autoRotate, setAutoRotate] = useState36(() => {
40424
40784
  const stored = window.localStorage.getItem("cadViewerAutoRotate");
40425
40785
  return stored === "false" ? false : true;
40426
40786
  });
40427
- const [autoRotateUserToggled, setAutoRotateUserToggled] = useState34(() => {
40787
+ const [autoRotateUserToggled, setAutoRotateUserToggled] = useState36(() => {
40428
40788
  const stored = window.localStorage.getItem("cadViewerAutoRotateUserToggled");
40429
40789
  return stored === "true";
40430
40790
  });
40431
- const [cameraPreset, setCameraPreset] = useState34("Custom");
40791
+ const [cameraPreset, setCameraPreset] = useState36("Custom");
40432
40792
  const { cameraType, setCameraType } = useCameraController();
40433
40793
  const { visibility, toggleLayer } = useLayerVisibility();
40434
- const cameraControllerRef = useRef24(null);
40794
+ const cameraControllerRef = useRef26(null);
40435
40795
  const externalCameraControllerReady = props.onCameraControllerReady;
40436
40796
  const {
40437
40797
  menuVisible,
@@ -40440,10 +40800,10 @@ var CadViewerInner = (props) => {
40440
40800
  contextMenuEventHandlers,
40441
40801
  setMenuVisible
40442
40802
  } = useContextMenu({ containerRef });
40443
- const autoRotateUserToggledRef = useRef24(autoRotateUserToggled);
40803
+ const autoRotateUserToggledRef = useRef26(autoRotateUserToggled);
40444
40804
  autoRotateUserToggledRef.current = autoRotateUserToggled;
40445
- const isAnimatingRef = useRef24(false);
40446
- const lastPresetSelectTime = useRef24(0);
40805
+ const isAnimatingRef = useRef26(false);
40806
+ const lastPresetSelectTime = useRef26(0);
40447
40807
  const PRESET_COOLDOWN = 1e3;
40448
40808
  const handleUserInteraction = useCallback22(() => {
40449
40809
  if (isAnimatingRef.current || Date.now() - lastPresetSelectTime.current < PRESET_COOLDOWN) {
@@ -40483,35 +40843,46 @@ var CadViewerInner = (props) => {
40483
40843
  isAnimatingRef,
40484
40844
  lastPresetSelectTime
40485
40845
  });
40486
- useEffect40(() => {
40846
+ useRegisteredHotkey(
40847
+ "open_keyboard_shortcuts_dialog",
40848
+ () => {
40849
+ setIsKeyboardShortcutsDialogOpen(true);
40850
+ },
40851
+ {
40852
+ key: "?",
40853
+ description: "Open keyboard shortcuts",
40854
+ modifiers: ["Shift"]
40855
+ }
40856
+ );
40857
+ useEffect42(() => {
40487
40858
  const stored = window.localStorage.getItem("cadViewerEngine");
40488
40859
  if (stored === "jscad" || stored === "manifold") {
40489
40860
  setEngine(stored);
40490
40861
  }
40491
40862
  }, []);
40492
- useEffect40(() => {
40863
+ useEffect42(() => {
40493
40864
  window.localStorage.setItem("cadViewerEngine", engine);
40494
40865
  }, [engine]);
40495
- useEffect40(() => {
40866
+ useEffect42(() => {
40496
40867
  window.localStorage.setItem("cadViewerAutoRotate", String(autoRotate));
40497
40868
  }, [autoRotate]);
40498
- useEffect40(() => {
40869
+ useEffect42(() => {
40499
40870
  window.localStorage.setItem(
40500
40871
  "cadViewerAutoRotateUserToggled",
40501
40872
  String(autoRotateUserToggled)
40502
40873
  );
40503
40874
  }, [autoRotateUserToggled]);
40504
- useEffect40(() => {
40875
+ useEffect42(() => {
40505
40876
  const stored = window.localStorage.getItem("cadViewerCameraType");
40506
40877
  if (stored === "orthographic" || stored === "perspective") {
40507
40878
  setCameraType(stored);
40508
40879
  }
40509
40880
  }, [setCameraType]);
40510
- useEffect40(() => {
40881
+ useEffect42(() => {
40511
40882
  window.localStorage.setItem("cadViewerCameraType", cameraType);
40512
40883
  }, [cameraType]);
40513
40884
  const viewerKey = props.circuitJson ? JSON.stringify(props.circuitJson) : void 0;
40514
- return /* @__PURE__ */ jsxs9(
40885
+ return /* @__PURE__ */ jsxs10(
40515
40886
  "div",
40516
40887
  {
40517
40888
  ref: containerRef,
@@ -40527,7 +40898,7 @@ var CadViewerInner = (props) => {
40527
40898
  },
40528
40899
  ...contextMenuEventHandlers,
40529
40900
  children: [
40530
- engine === "jscad" ? /* @__PURE__ */ jsx35(
40901
+ engine === "jscad" ? /* @__PURE__ */ jsx36(
40531
40902
  CadViewerJscad,
40532
40903
  {
40533
40904
  ...props,
@@ -40536,7 +40907,7 @@ var CadViewerInner = (props) => {
40536
40907
  onUserInteraction: handleUserInteraction,
40537
40908
  onCameraControllerReady: handleCameraControllerReady
40538
40909
  }
40539
- ) : /* @__PURE__ */ jsx35(
40910
+ ) : /* @__PURE__ */ jsx36(
40540
40911
  CadViewerManifold_default,
40541
40912
  {
40542
40913
  ...props,
@@ -40546,7 +40917,7 @@ var CadViewerInner = (props) => {
40546
40917
  onCameraControllerReady: handleCameraControllerReady
40547
40918
  }
40548
40919
  ),
40549
- /* @__PURE__ */ jsxs9(
40920
+ /* @__PURE__ */ jsxs10(
40550
40921
  "div",
40551
40922
  {
40552
40923
  style: {
@@ -40563,11 +40934,11 @@ var CadViewerInner = (props) => {
40563
40934
  },
40564
40935
  children: [
40565
40936
  "Engine: ",
40566
- /* @__PURE__ */ jsx35("b", { children: engine === "jscad" ? "JSCAD" : "Manifold" })
40937
+ /* @__PURE__ */ jsx36("b", { children: engine === "jscad" ? "JSCAD" : "Manifold" })
40567
40938
  ]
40568
40939
  }
40569
40940
  ),
40570
- menuVisible && /* @__PURE__ */ jsx35(
40941
+ menuVisible && /* @__PURE__ */ jsx36(
40571
40942
  ContextMenu,
40572
40943
  {
40573
40944
  menuRef,
@@ -40587,8 +40958,19 @@ var CadViewerInner = (props) => {
40587
40958
  onDownloadGltf: () => {
40588
40959
  downloadGltf();
40589
40960
  closeMenu();
40961
+ },
40962
+ onOpenKeyboardShortcuts: () => {
40963
+ setIsKeyboardShortcutsDialogOpen(true);
40964
+ closeMenu();
40590
40965
  }
40591
40966
  }
40967
+ ),
40968
+ /* @__PURE__ */ jsx36(
40969
+ KeyboardShortcutsDialog,
40970
+ {
40971
+ open: isKeyboardShortcutsDialogOpen,
40972
+ onClose: () => setIsKeyboardShortcutsDialogOpen(false)
40973
+ }
40592
40974
  )
40593
40975
  ]
40594
40976
  },
@@ -40596,17 +40978,17 @@ var CadViewerInner = (props) => {
40596
40978
  );
40597
40979
  };
40598
40980
  var CadViewer = (props) => {
40599
- const defaultTarget = useMemo26(() => new THREE29.Vector3(0, 0, 0), []);
40600
- const initialCameraPosition = useMemo26(
40981
+ const defaultTarget = useMemo28(() => new THREE29.Vector3(0, 0, 0), []);
40982
+ const initialCameraPosition = useMemo28(
40601
40983
  () => [5, -5, 5],
40602
40984
  []
40603
40985
  );
40604
- return /* @__PURE__ */ jsx35(
40986
+ return /* @__PURE__ */ jsx36(
40605
40987
  CameraControllerProvider,
40606
40988
  {
40607
40989
  defaultTarget,
40608
40990
  initialCameraPosition,
40609
- children: /* @__PURE__ */ jsx35(LayerVisibilityProvider, { children: /* @__PURE__ */ jsx35(CadViewerInner, { ...props }) })
40991
+ children: /* @__PURE__ */ jsx36(LayerVisibilityProvider, { children: /* @__PURE__ */ jsx36(CadViewerInner, { ...props }) })
40610
40992
  }
40611
40993
  );
40612
40994
  };
@@ -40890,10 +41272,10 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
40890
41272
 
40891
41273
  // src/hooks/exporter/gltf.ts
40892
41274
  import { GLTFExporter as GLTFExporter2 } from "three-stdlib";
40893
- import { useEffect as useEffect41, useState as useState35, useMemo as useMemo27, useCallback as useCallback23 } from "react";
41275
+ import { useEffect as useEffect43, useState as useState37, useMemo as useMemo29, useCallback as useCallback23 } from "react";
40894
41276
  function useSaveGltfAs(options = {}) {
40895
41277
  const parse2 = useParser(options);
40896
- const link = useMemo27(() => document.createElement("a"), []);
41278
+ const link = useMemo29(() => document.createElement("a"), []);
40897
41279
  const saveAs = async (filename) => {
40898
41280
  const name = filename ?? options.filename ?? "";
40899
41281
  if (options.binary == null) options.binary = name.endsWith(".glb");
@@ -40903,7 +41285,7 @@ function useSaveGltfAs(options = {}) {
40903
41285
  link.dispatchEvent(new MouseEvent("click"));
40904
41286
  URL.revokeObjectURL(url);
40905
41287
  };
40906
- useEffect41(
41288
+ useEffect43(
40907
41289
  () => () => {
40908
41290
  link.remove();
40909
41291
  instance = null;
@@ -40918,17 +41300,17 @@ function useSaveGltfAs(options = {}) {
40918
41300
  }
40919
41301
  function useExportGltfUrl(options = {}) {
40920
41302
  const parse2 = useParser(options);
40921
- const [url, setUrl] = useState35();
40922
- const [error, setError] = useState35();
41303
+ const [url, setUrl] = useState37();
41304
+ const [error, setError] = useState37();
40923
41305
  const ref = useCallback23(
40924
41306
  (instance) => parse2(instance).then(setUrl).catch(setError),
40925
41307
  []
40926
41308
  );
40927
- useEffect41(() => () => URL.revokeObjectURL(url), [url]);
41309
+ useEffect43(() => () => URL.revokeObjectURL(url), [url]);
40928
41310
  return [ref, url, error];
40929
41311
  }
40930
41312
  function useParser(options = {}) {
40931
- const exporter = useMemo27(() => new GLTFExporter2(), []);
41313
+ const exporter = useMemo29(() => new GLTFExporter2(), []);
40932
41314
  return (instance) => {
40933
41315
  const { promise, resolve, reject } = Promise.withResolvers();
40934
41316
  exporter.parse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.438",
3
+ "version": "0.0.440",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  "@vitejs/plugin-react": "^4.3.4",
62
62
  "bun-match-svg": "^0.0.9",
63
63
  "bun-types": "1.2.1",
64
- "circuit-json": "0.0.316",
64
+ "circuit-json": "0.0.317",
65
65
  "circuit-to-svg": "^0.0.179",
66
66
  "debug": "^4.4.0",
67
67
  "jscad-electronics": "^0.0.89",