@tscircuit/3d-viewer 0.0.439 → 0.0.441

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 +402 -60
  2. package/package.json +1 -1
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
@@ -28113,12 +28113,6 @@ var defaultVisibility = {
28113
28113
  var LayerVisibilityContext = createContext3(void 0);
28114
28114
  var LayerVisibilityProvider = ({ children }) => {
28115
28115
  const [visibility, setVisibility] = useState5(defaultVisibility);
28116
- const toggleLayer = useCallback2((layer) => {
28117
- setVisibility((prev) => ({
28118
- ...prev,
28119
- [layer]: !prev[layer]
28120
- }));
28121
- }, []);
28122
28116
  const setLayerVisibility = useCallback2(
28123
28117
  (layer, visible) => {
28124
28118
  setVisibility((prev) => ({
@@ -28134,11 +28128,10 @@ var LayerVisibilityProvider = ({ children }) => {
28134
28128
  const value = useMemo6(
28135
28129
  () => ({
28136
28130
  visibility,
28137
- toggleLayer,
28138
28131
  setLayerVisibility,
28139
28132
  resetToDefaults
28140
28133
  }),
28141
- [visibility, toggleLayer, setLayerVisibility, resetToDefaults]
28134
+ [visibility, setLayerVisibility, resetToDefaults]
28142
28135
  );
28143
28136
  return /* @__PURE__ */ jsx8(LayerVisibilityContext.Provider, { value, children });
28144
28137
  };
@@ -28288,7 +28281,7 @@ import * as THREE15 from "three";
28288
28281
  // package.json
28289
28282
  var package_default = {
28290
28283
  name: "@tscircuit/3d-viewer",
28291
- version: "0.0.438",
28284
+ version: "0.0.440",
28292
28285
  main: "./dist/index.js",
28293
28286
  module: "./dist/index.js",
28294
28287
  type: "module",
@@ -34503,8 +34496,112 @@ var useGlobalDownloadGltf = () => {
34503
34496
  }, []);
34504
34497
  };
34505
34498
 
34499
+ // src/hooks/useRegisteredHotkey.ts
34500
+ import { useEffect as useEffect25, useMemo as useMemo21, useRef as useRef11, useState as useState18 } from "react";
34501
+ var hotkeyRegistry = /* @__PURE__ */ new Map();
34502
+ var subscribers = /* @__PURE__ */ new Set();
34503
+ var isListenerAttached = false;
34504
+ var parseShortcut = (shortcut) => {
34505
+ const parts = shortcut.toLowerCase().split("+");
34506
+ const key = parts[parts.length - 1];
34507
+ const modifierParts = parts.slice(0, -1);
34508
+ return {
34509
+ key,
34510
+ ctrl: modifierParts.includes("ctrl"),
34511
+ cmd: modifierParts.includes("cmd"),
34512
+ shift: modifierParts.includes("shift"),
34513
+ alt: modifierParts.includes("alt")
34514
+ };
34515
+ };
34516
+ var matchesShortcut = (event, shortcut) => {
34517
+ const parsed = parseShortcut(shortcut);
34518
+ const keyMatches = event.key.toLowerCase() === parsed.key;
34519
+ const ctrlMatches = parsed.ctrl === event.ctrlKey;
34520
+ const cmdMatches = parsed.cmd === event.metaKey;
34521
+ const shiftMatches = parsed.shift === event.shiftKey;
34522
+ const altMatches = parsed.alt === event.altKey;
34523
+ return keyMatches && ctrlMatches && cmdMatches && shiftMatches && altMatches;
34524
+ };
34525
+ var isEditableTarget = (target) => {
34526
+ if (!target || typeof target !== "object") return false;
34527
+ const element = target;
34528
+ const tagName = element.tagName;
34529
+ const editableTags = ["INPUT", "TEXTAREA", "SELECT"];
34530
+ if (editableTags.includes(tagName)) {
34531
+ return true;
34532
+ }
34533
+ return Boolean(element.getAttribute?.("contenteditable"));
34534
+ };
34535
+ var handleKeydown = (event) => {
34536
+ if (isEditableTarget(event.target)) {
34537
+ return;
34538
+ }
34539
+ hotkeyRegistry.forEach((entry) => {
34540
+ if (matchesShortcut(event, entry.shortcut)) {
34541
+ event.preventDefault();
34542
+ entry.invoke();
34543
+ }
34544
+ });
34545
+ };
34546
+ var notifySubscribers = () => {
34547
+ const entries = Array.from(hotkeyRegistry.values());
34548
+ subscribers.forEach((subscriber) => subscriber(entries));
34549
+ };
34550
+ var ensureListener = () => {
34551
+ if (isListenerAttached) return;
34552
+ if (typeof window === "undefined") return;
34553
+ window.addEventListener("keydown", handleKeydown);
34554
+ isListenerAttached = true;
34555
+ };
34556
+ var registerHotkey = (registration) => {
34557
+ hotkeyRegistry.set(registration.id, registration);
34558
+ notifySubscribers();
34559
+ ensureListener();
34560
+ };
34561
+ var unregisterHotkey = (id) => {
34562
+ if (hotkeyRegistry.delete(id)) {
34563
+ notifySubscribers();
34564
+ }
34565
+ };
34566
+ var subscribeToRegistry = (subscriber) => {
34567
+ subscribers.add(subscriber);
34568
+ subscriber(Array.from(hotkeyRegistry.values()));
34569
+ return () => {
34570
+ subscribers.delete(subscriber);
34571
+ };
34572
+ };
34573
+ var useRegisteredHotkey = (id, handler, metadata) => {
34574
+ const handlerRef = useRef11(handler);
34575
+ handlerRef.current = handler;
34576
+ const normalizedMetadata = useMemo21(
34577
+ () => ({
34578
+ shortcut: metadata.shortcut,
34579
+ description: metadata.description
34580
+ }),
34581
+ [metadata.shortcut, metadata.description]
34582
+ );
34583
+ useEffect25(() => {
34584
+ const registration = {
34585
+ id,
34586
+ ...normalizedMetadata,
34587
+ invoke: () => handlerRef.current()
34588
+ };
34589
+ registerHotkey(registration);
34590
+ return () => {
34591
+ unregisterHotkey(id);
34592
+ };
34593
+ }, [id, normalizedMetadata]);
34594
+ };
34595
+ var useHotkeyRegistry = () => {
34596
+ const [entries, setEntries] = useState18(
34597
+ () => Array.from(hotkeyRegistry.values())
34598
+ );
34599
+ useEffect25(() => subscribeToRegistry(setEntries), []);
34600
+ return entries;
34601
+ };
34602
+
34506
34603
  // src/components/ContextMenu.tsx
34507
- import { useState as useState33 } from "react";
34604
+ import { useState as useState34 } from "react";
34508
34605
 
34509
34606
  // node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs
34510
34607
  import * as React43 from "react";
@@ -38132,9 +38229,9 @@ function assignRef(ref, value) {
38132
38229
  }
38133
38230
 
38134
38231
  // node_modules/use-callback-ref/dist/es2015/useRef.js
38135
- import { useState as useState28 } from "react";
38232
+ import { useState as useState29 } from "react";
38136
38233
  function useCallbackRef2(initialValue, callback) {
38137
- var ref = useState28(function() {
38234
+ var ref = useState29(function() {
38138
38235
  return {
38139
38236
  // value
38140
38237
  value: initialValue,
@@ -39858,7 +39955,7 @@ var SubTrigger2 = DropdownMenuSubTrigger;
39858
39955
  var SubContent2 = DropdownMenuSubContent;
39859
39956
 
39860
39957
  // src/components/AppearanceMenu.tsx
39861
- import { useState as useState32 } from "react";
39958
+ import { useState as useState33 } from "react";
39862
39959
 
39863
39960
  // src/components/Icons.tsx
39864
39961
  import { jsx as jsx32 } from "react/jsx-runtime";
@@ -39963,9 +40060,9 @@ var iconContainerStyles = {
39963
40060
  flexShrink: 0
39964
40061
  };
39965
40062
  var AppearanceMenu = () => {
39966
- const { visibility, toggleLayer } = useLayerVisibility();
39967
- const [appearanceSubOpen, setAppearanceSubOpen] = useState32(false);
39968
- const [hoveredItem, setHoveredItem] = useState32(null);
40063
+ const { visibility, setLayerVisibility } = useLayerVisibility();
40064
+ const [appearanceSubOpen, setAppearanceSubOpen] = useState33(false);
40065
+ const [hoveredItem, setHoveredItem] = useState33(null);
39969
40066
  return /* @__PURE__ */ jsxs7(Fragment9, { children: [
39970
40067
  /* @__PURE__ */ jsx33(Separator2, { style: separatorStyles }),
39971
40068
  /* @__PURE__ */ jsxs7(Sub2, { onOpenChange: setAppearanceSubOpen, children: [
@@ -40014,7 +40111,7 @@ var AppearanceMenu = () => {
40014
40111
  onSelect: (e) => e.preventDefault(),
40015
40112
  onPointerDown: (e) => {
40016
40113
  e.preventDefault();
40017
- toggleLayer("boardBody");
40114
+ setLayerVisibility("boardBody", !visibility.boardBody);
40018
40115
  },
40019
40116
  onMouseEnter: () => setHoveredItem("boardBody"),
40020
40117
  onMouseLeave: () => setHoveredItem(null),
@@ -40035,7 +40132,7 @@ var AppearanceMenu = () => {
40035
40132
  onSelect: (e) => e.preventDefault(),
40036
40133
  onPointerDown: (e) => {
40037
40134
  e.preventDefault();
40038
- toggleLayer("topCopper");
40135
+ setLayerVisibility("topCopper", !visibility.topCopper);
40039
40136
  },
40040
40137
  onMouseEnter: () => setHoveredItem("topCopper"),
40041
40138
  onMouseLeave: () => setHoveredItem(null),
@@ -40056,7 +40153,7 @@ var AppearanceMenu = () => {
40056
40153
  onSelect: (e) => e.preventDefault(),
40057
40154
  onPointerDown: (e) => {
40058
40155
  e.preventDefault();
40059
- toggleLayer("bottomCopper");
40156
+ setLayerVisibility("bottomCopper", !visibility.bottomCopper);
40060
40157
  },
40061
40158
  onMouseEnter: () => setHoveredItem("bottomCopper"),
40062
40159
  onMouseLeave: () => setHoveredItem(null),
@@ -40077,7 +40174,7 @@ var AppearanceMenu = () => {
40077
40174
  onSelect: (e) => e.preventDefault(),
40078
40175
  onPointerDown: (e) => {
40079
40176
  e.preventDefault();
40080
- toggleLayer("topSilkscreen");
40177
+ setLayerVisibility("topSilkscreen", !visibility.topSilkscreen);
40081
40178
  },
40082
40179
  onMouseEnter: () => setHoveredItem("topSilkscreen"),
40083
40180
  onMouseLeave: () => setHoveredItem(null),
@@ -40098,7 +40195,10 @@ var AppearanceMenu = () => {
40098
40195
  onSelect: (e) => e.preventDefault(),
40099
40196
  onPointerDown: (e) => {
40100
40197
  e.preventDefault();
40101
- toggleLayer("bottomSilkscreen");
40198
+ setLayerVisibility(
40199
+ "bottomSilkscreen",
40200
+ !visibility.bottomSilkscreen
40201
+ );
40102
40202
  },
40103
40203
  onMouseEnter: () => setHoveredItem("bottomSilkscreen"),
40104
40204
  onMouseLeave: () => setHoveredItem(null),
@@ -40119,7 +40219,7 @@ var AppearanceMenu = () => {
40119
40219
  onSelect: (e) => e.preventDefault(),
40120
40220
  onPointerDown: (e) => {
40121
40221
  e.preventDefault();
40122
- toggleLayer("smtModels");
40222
+ setLayerVisibility("smtModels", !visibility.smtModels);
40123
40223
  },
40124
40224
  onMouseEnter: () => setHoveredItem("smtModels"),
40125
40225
  onMouseLeave: () => setHoveredItem(null),
@@ -40216,11 +40316,12 @@ var ContextMenu = ({
40216
40316
  onEngineSwitch,
40217
40317
  onCameraPresetSelect,
40218
40318
  onAutoRotateToggle,
40219
- onDownloadGltf
40319
+ onDownloadGltf,
40320
+ onOpenKeyboardShortcuts
40220
40321
  }) => {
40221
40322
  const { cameraType, setCameraType } = useCameraController();
40222
- const [cameraSubOpen, setCameraSubOpen] = useState33(false);
40223
- const [hoveredItem, setHoveredItem] = useState33(null);
40323
+ const [cameraSubOpen, setCameraSubOpen] = useState34(false);
40324
+ const [hoveredItem, setHoveredItem] = useState34(null);
40224
40325
  return /* @__PURE__ */ jsx34(
40225
40326
  "div",
40226
40327
  {
@@ -40413,6 +40514,35 @@ var ContextMenu = ({
40413
40514
  }
40414
40515
  ),
40415
40516
  /* @__PURE__ */ jsx34(Separator2, { style: separatorStyles2 }),
40517
+ /* @__PURE__ */ jsxs8(
40518
+ Item22,
40519
+ {
40520
+ style: {
40521
+ ...itemStyles2,
40522
+ ...itemPaddingStyles2,
40523
+ backgroundColor: hoveredItem === "shortcuts" ? "#404040" : "transparent"
40524
+ },
40525
+ onSelect: onOpenKeyboardShortcuts,
40526
+ onMouseEnter: () => setHoveredItem("shortcuts"),
40527
+ onMouseLeave: () => setHoveredItem(null),
40528
+ onTouchStart: () => setHoveredItem("shortcuts"),
40529
+ children: [
40530
+ /* @__PURE__ */ jsx34("span", { style: { flex: 1, display: "flex", alignItems: "center" }, children: "Keyboard Shortcuts" }),
40531
+ /* @__PURE__ */ jsx34(
40532
+ "div",
40533
+ {
40534
+ style: {
40535
+ ...badgeStyles,
40536
+ display: "flex",
40537
+ alignItems: "center"
40538
+ },
40539
+ children: "Shift+?"
40540
+ }
40541
+ )
40542
+ ]
40543
+ }
40544
+ ),
40545
+ /* @__PURE__ */ jsx34(Separator2, { style: separatorStyles2 }),
40416
40546
  /* @__PURE__ */ jsx34(
40417
40547
  "div",
40418
40548
  {
@@ -40450,23 +40580,214 @@ var ContextMenu = ({
40450
40580
  );
40451
40581
  };
40452
40582
 
40453
- // src/CadViewer.tsx
40583
+ // src/components/KeyboardShortcutsDialog.tsx
40584
+ import { useEffect as useEffect41, useMemo as useMemo27, useRef as useRef25, useState as useState35 } from "react";
40454
40585
  import { jsx as jsx35, jsxs as jsxs9 } from "react/jsx-runtime";
40586
+ var KeyboardShortcutsDialog = ({
40587
+ open,
40588
+ onClose
40589
+ }) => {
40590
+ const [query, setQuery] = useState35("");
40591
+ const inputRef = useRef25(null);
40592
+ const hotkeys = useHotkeyRegistry();
40593
+ useEffect41(() => {
40594
+ if (!open) return void 0;
40595
+ const handleKeyDown = (event) => {
40596
+ if (event.key === "Escape") {
40597
+ event.preventDefault();
40598
+ onClose();
40599
+ }
40600
+ };
40601
+ window.addEventListener("keydown", handleKeyDown);
40602
+ return () => window.removeEventListener("keydown", handleKeyDown);
40603
+ }, [open, onClose]);
40604
+ useEffect41(() => {
40605
+ if (open) {
40606
+ setTimeout(() => {
40607
+ inputRef.current?.focus();
40608
+ }, 0);
40609
+ }
40610
+ }, [open]);
40611
+ const filteredHotkeys = useMemo27(() => {
40612
+ const normalizedQuery = query.trim().toLowerCase();
40613
+ if (!normalizedQuery) {
40614
+ return hotkeys;
40615
+ }
40616
+ return hotkeys.filter((hotkey) => {
40617
+ const haystack = `${hotkey.shortcut} ${hotkey.description}`;
40618
+ return haystack.toLowerCase().includes(normalizedQuery);
40619
+ });
40620
+ }, [hotkeys, query]);
40621
+ if (!open) {
40622
+ return null;
40623
+ }
40624
+ return /* @__PURE__ */ jsx35(
40625
+ "div",
40626
+ {
40627
+ role: "dialog",
40628
+ "aria-modal": "true",
40629
+ style: {
40630
+ position: "fixed",
40631
+ inset: 0,
40632
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
40633
+ display: "flex",
40634
+ alignItems: "center",
40635
+ justifyContent: "center",
40636
+ zIndex: 9999
40637
+ },
40638
+ onClick: onClose,
40639
+ children: /* @__PURE__ */ jsxs9(
40640
+ "div",
40641
+ {
40642
+ style: {
40643
+ backgroundColor: "#1f1f23",
40644
+ color: "#f8f8ff",
40645
+ borderRadius: 12,
40646
+ width: "min(640px, 90vw)",
40647
+ maxHeight: "80vh",
40648
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.45), 0 8px 20px rgba(0, 0, 0, 0.35)",
40649
+ display: "flex",
40650
+ flexDirection: "column",
40651
+ overflow: "hidden"
40652
+ },
40653
+ onClick: (event) => event.stopPropagation(),
40654
+ children: [
40655
+ /* @__PURE__ */ jsxs9(
40656
+ "header",
40657
+ {
40658
+ style: {
40659
+ padding: "20px 24px 12px",
40660
+ borderBottom: "1px solid rgba(255, 255, 255, 0.08)"
40661
+ },
40662
+ children: [
40663
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
40664
+ /* @__PURE__ */ jsx35(
40665
+ "h2",
40666
+ {
40667
+ style: {
40668
+ margin: 0,
40669
+ fontSize: "1.1rem",
40670
+ fontWeight: 600,
40671
+ letterSpacing: "0.02em"
40672
+ },
40673
+ children: "Keyboard Shortcuts"
40674
+ }
40675
+ ),
40676
+ /* @__PURE__ */ jsx35(
40677
+ "button",
40678
+ {
40679
+ type: "button",
40680
+ onClick: onClose,
40681
+ style: {
40682
+ background: "transparent",
40683
+ border: "none",
40684
+ color: "rgba(255, 255, 255, 0.8)",
40685
+ fontSize: "1rem",
40686
+ cursor: "pointer"
40687
+ },
40688
+ children: "\u2715"
40689
+ }
40690
+ )
40691
+ ] }),
40692
+ /* @__PURE__ */ jsx35(
40693
+ "input",
40694
+ {
40695
+ ref: inputRef,
40696
+ type: "text",
40697
+ placeholder: "Search shortcuts...",
40698
+ value: query,
40699
+ onChange: (event) => setQuery(event.target.value),
40700
+ style: {
40701
+ marginTop: 12,
40702
+ width: "calc(100% - 24px)",
40703
+ padding: "10px 12px",
40704
+ borderRadius: 8,
40705
+ border: "1px solid rgba(255, 255, 255, 0.1)",
40706
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
40707
+ color: "white",
40708
+ fontSize: "0.95rem"
40709
+ }
40710
+ }
40711
+ )
40712
+ ]
40713
+ }
40714
+ ),
40715
+ /* @__PURE__ */ jsx35("div", { style: { overflowY: "auto" }, children: /* @__PURE__ */ jsxs9(
40716
+ "table",
40717
+ {
40718
+ style: {
40719
+ width: "100%",
40720
+ borderCollapse: "collapse",
40721
+ fontSize: "0.95rem"
40722
+ },
40723
+ children: [
40724
+ /* @__PURE__ */ jsx35("thead", { children: /* @__PURE__ */ jsxs9("tr", { style: { textAlign: "left", color: "#a1a1b5" }, children: [
40725
+ /* @__PURE__ */ jsx35("th", { style: { padding: "12px 24px", width: "25%" }, children: "Key" }),
40726
+ /* @__PURE__ */ jsx35("th", { style: { padding: "12px 24px" }, children: "Description" })
40727
+ ] }) }),
40728
+ /* @__PURE__ */ jsx35("tbody", { children: filteredHotkeys.length === 0 ? /* @__PURE__ */ jsx35("tr", { children: /* @__PURE__ */ jsx35(
40729
+ "td",
40730
+ {
40731
+ colSpan: 2,
40732
+ style: { padding: "24px", textAlign: "center" },
40733
+ children: "No shortcuts found"
40734
+ }
40735
+ ) }) : filteredHotkeys.map((hotkey) => /* @__PURE__ */ jsxs9(
40736
+ "tr",
40737
+ {
40738
+ style: { borderTop: "1px solid rgba(255, 255, 255, 0.05)" },
40739
+ children: [
40740
+ /* @__PURE__ */ jsx35("td", { style: { padding: "12px 24px" }, children: /* @__PURE__ */ jsx35(
40741
+ "span",
40742
+ {
40743
+ style: {
40744
+ display: "inline-flex",
40745
+ alignItems: "center",
40746
+ justifyContent: "center",
40747
+ border: "1px solid rgba(255, 255, 255, 0.3)",
40748
+ borderRadius: 6,
40749
+ minWidth: 36,
40750
+ padding: "4px 8px",
40751
+ fontFamily: "monospace",
40752
+ fontSize: "0.95rem",
40753
+ textTransform: "capitalize"
40754
+ },
40755
+ children: hotkey.shortcut
40756
+ }
40757
+ ) }),
40758
+ /* @__PURE__ */ jsx35("td", { style: { padding: "12px 24px" }, children: hotkey.description })
40759
+ ]
40760
+ },
40761
+ hotkey.id
40762
+ )) })
40763
+ ]
40764
+ }
40765
+ ) })
40766
+ ]
40767
+ }
40768
+ )
40769
+ }
40770
+ );
40771
+ };
40772
+
40773
+ // src/CadViewer.tsx
40774
+ import { jsx as jsx36, jsxs as jsxs10 } from "react/jsx-runtime";
40455
40775
  var CadViewerInner = (props) => {
40456
- const [engine, setEngine] = useState34("manifold");
40457
- const containerRef = useRef24(null);
40458
- const [autoRotate, setAutoRotate] = useState34(() => {
40776
+ const [engine, setEngine] = useState36("manifold");
40777
+ const containerRef = useRef26(null);
40778
+ const [isKeyboardShortcutsDialogOpen, setIsKeyboardShortcutsDialogOpen] = useState36(false);
40779
+ const [autoRotate, setAutoRotate] = useState36(() => {
40459
40780
  const stored = window.localStorage.getItem("cadViewerAutoRotate");
40460
40781
  return stored === "false" ? false : true;
40461
40782
  });
40462
- const [autoRotateUserToggled, setAutoRotateUserToggled] = useState34(() => {
40783
+ const [autoRotateUserToggled, setAutoRotateUserToggled] = useState36(() => {
40463
40784
  const stored = window.localStorage.getItem("cadViewerAutoRotateUserToggled");
40464
40785
  return stored === "true";
40465
40786
  });
40466
- const [cameraPreset, setCameraPreset] = useState34("Custom");
40787
+ const [cameraPreset, setCameraPreset] = useState36("Custom");
40467
40788
  const { cameraType, setCameraType } = useCameraController();
40468
- const { visibility, toggleLayer } = useLayerVisibility();
40469
- const cameraControllerRef = useRef24(null);
40789
+ const { visibility, setLayerVisibility } = useLayerVisibility();
40790
+ const cameraControllerRef = useRef26(null);
40470
40791
  const externalCameraControllerReady = props.onCameraControllerReady;
40471
40792
  const {
40472
40793
  menuVisible,
@@ -40475,10 +40796,10 @@ var CadViewerInner = (props) => {
40475
40796
  contextMenuEventHandlers,
40476
40797
  setMenuVisible
40477
40798
  } = useContextMenu({ containerRef });
40478
- const autoRotateUserToggledRef = useRef24(autoRotateUserToggled);
40799
+ const autoRotateUserToggledRef = useRef26(autoRotateUserToggled);
40479
40800
  autoRotateUserToggledRef.current = autoRotateUserToggled;
40480
- const isAnimatingRef = useRef24(false);
40481
- const lastPresetSelectTime = useRef24(0);
40801
+ const isAnimatingRef = useRef26(false);
40802
+ const lastPresetSelectTime = useRef26(0);
40482
40803
  const PRESET_COOLDOWN = 1e3;
40483
40804
  const handleUserInteraction = useCallback22(() => {
40484
40805
  if (isAnimatingRef.current || Date.now() - lastPresetSelectTime.current < PRESET_COOLDOWN) {
@@ -40518,35 +40839,45 @@ var CadViewerInner = (props) => {
40518
40839
  isAnimatingRef,
40519
40840
  lastPresetSelectTime
40520
40841
  });
40521
- useEffect40(() => {
40842
+ useRegisteredHotkey(
40843
+ "open_keyboard_shortcuts_dialog",
40844
+ () => {
40845
+ setIsKeyboardShortcutsDialogOpen(true);
40846
+ },
40847
+ {
40848
+ shortcut: "shift+?",
40849
+ description: "Open keyboard shortcuts"
40850
+ }
40851
+ );
40852
+ useEffect42(() => {
40522
40853
  const stored = window.localStorage.getItem("cadViewerEngine");
40523
40854
  if (stored === "jscad" || stored === "manifold") {
40524
40855
  setEngine(stored);
40525
40856
  }
40526
40857
  }, []);
40527
- useEffect40(() => {
40858
+ useEffect42(() => {
40528
40859
  window.localStorage.setItem("cadViewerEngine", engine);
40529
40860
  }, [engine]);
40530
- useEffect40(() => {
40861
+ useEffect42(() => {
40531
40862
  window.localStorage.setItem("cadViewerAutoRotate", String(autoRotate));
40532
40863
  }, [autoRotate]);
40533
- useEffect40(() => {
40864
+ useEffect42(() => {
40534
40865
  window.localStorage.setItem(
40535
40866
  "cadViewerAutoRotateUserToggled",
40536
40867
  String(autoRotateUserToggled)
40537
40868
  );
40538
40869
  }, [autoRotateUserToggled]);
40539
- useEffect40(() => {
40870
+ useEffect42(() => {
40540
40871
  const stored = window.localStorage.getItem("cadViewerCameraType");
40541
40872
  if (stored === "orthographic" || stored === "perspective") {
40542
40873
  setCameraType(stored);
40543
40874
  }
40544
40875
  }, [setCameraType]);
40545
- useEffect40(() => {
40876
+ useEffect42(() => {
40546
40877
  window.localStorage.setItem("cadViewerCameraType", cameraType);
40547
40878
  }, [cameraType]);
40548
40879
  const viewerKey = props.circuitJson ? JSON.stringify(props.circuitJson) : void 0;
40549
- return /* @__PURE__ */ jsxs9(
40880
+ return /* @__PURE__ */ jsxs10(
40550
40881
  "div",
40551
40882
  {
40552
40883
  ref: containerRef,
@@ -40562,7 +40893,7 @@ var CadViewerInner = (props) => {
40562
40893
  },
40563
40894
  ...contextMenuEventHandlers,
40564
40895
  children: [
40565
- engine === "jscad" ? /* @__PURE__ */ jsx35(
40896
+ engine === "jscad" ? /* @__PURE__ */ jsx36(
40566
40897
  CadViewerJscad,
40567
40898
  {
40568
40899
  ...props,
@@ -40571,7 +40902,7 @@ var CadViewerInner = (props) => {
40571
40902
  onUserInteraction: handleUserInteraction,
40572
40903
  onCameraControllerReady: handleCameraControllerReady
40573
40904
  }
40574
- ) : /* @__PURE__ */ jsx35(
40905
+ ) : /* @__PURE__ */ jsx36(
40575
40906
  CadViewerManifold_default,
40576
40907
  {
40577
40908
  ...props,
@@ -40581,7 +40912,7 @@ var CadViewerInner = (props) => {
40581
40912
  onCameraControllerReady: handleCameraControllerReady
40582
40913
  }
40583
40914
  ),
40584
- /* @__PURE__ */ jsxs9(
40915
+ /* @__PURE__ */ jsxs10(
40585
40916
  "div",
40586
40917
  {
40587
40918
  style: {
@@ -40598,11 +40929,11 @@ var CadViewerInner = (props) => {
40598
40929
  },
40599
40930
  children: [
40600
40931
  "Engine: ",
40601
- /* @__PURE__ */ jsx35("b", { children: engine === "jscad" ? "JSCAD" : "Manifold" })
40932
+ /* @__PURE__ */ jsx36("b", { children: engine === "jscad" ? "JSCAD" : "Manifold" })
40602
40933
  ]
40603
40934
  }
40604
40935
  ),
40605
- menuVisible && /* @__PURE__ */ jsx35(
40936
+ menuVisible && /* @__PURE__ */ jsx36(
40606
40937
  ContextMenu,
40607
40938
  {
40608
40939
  menuRef,
@@ -40622,8 +40953,19 @@ var CadViewerInner = (props) => {
40622
40953
  onDownloadGltf: () => {
40623
40954
  downloadGltf();
40624
40955
  closeMenu();
40956
+ },
40957
+ onOpenKeyboardShortcuts: () => {
40958
+ setIsKeyboardShortcutsDialogOpen(true);
40959
+ closeMenu();
40625
40960
  }
40626
40961
  }
40962
+ ),
40963
+ /* @__PURE__ */ jsx36(
40964
+ KeyboardShortcutsDialog,
40965
+ {
40966
+ open: isKeyboardShortcutsDialogOpen,
40967
+ onClose: () => setIsKeyboardShortcutsDialogOpen(false)
40968
+ }
40627
40969
  )
40628
40970
  ]
40629
40971
  },
@@ -40631,17 +40973,17 @@ var CadViewerInner = (props) => {
40631
40973
  );
40632
40974
  };
40633
40975
  var CadViewer = (props) => {
40634
- const defaultTarget = useMemo26(() => new THREE29.Vector3(0, 0, 0), []);
40635
- const initialCameraPosition = useMemo26(
40976
+ const defaultTarget = useMemo28(() => new THREE29.Vector3(0, 0, 0), []);
40977
+ const initialCameraPosition = useMemo28(
40636
40978
  () => [5, -5, 5],
40637
40979
  []
40638
40980
  );
40639
- return /* @__PURE__ */ jsx35(
40981
+ return /* @__PURE__ */ jsx36(
40640
40982
  CameraControllerProvider,
40641
40983
  {
40642
40984
  defaultTarget,
40643
40985
  initialCameraPosition,
40644
- children: /* @__PURE__ */ jsx35(LayerVisibilityProvider, { children: /* @__PURE__ */ jsx35(CadViewerInner, { ...props }) })
40986
+ children: /* @__PURE__ */ jsx36(LayerVisibilityProvider, { children: /* @__PURE__ */ jsx36(CadViewerInner, { ...props }) })
40645
40987
  }
40646
40988
  );
40647
40989
  };
@@ -40925,10 +41267,10 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
40925
41267
 
40926
41268
  // src/hooks/exporter/gltf.ts
40927
41269
  import { GLTFExporter as GLTFExporter2 } from "three-stdlib";
40928
- import { useEffect as useEffect41, useState as useState35, useMemo as useMemo27, useCallback as useCallback23 } from "react";
41270
+ import { useEffect as useEffect43, useState as useState37, useMemo as useMemo29, useCallback as useCallback23 } from "react";
40929
41271
  function useSaveGltfAs(options = {}) {
40930
41272
  const parse2 = useParser(options);
40931
- const link = useMemo27(() => document.createElement("a"), []);
41273
+ const link = useMemo29(() => document.createElement("a"), []);
40932
41274
  const saveAs = async (filename) => {
40933
41275
  const name = filename ?? options.filename ?? "";
40934
41276
  if (options.binary == null) options.binary = name.endsWith(".glb");
@@ -40938,7 +41280,7 @@ function useSaveGltfAs(options = {}) {
40938
41280
  link.dispatchEvent(new MouseEvent("click"));
40939
41281
  URL.revokeObjectURL(url);
40940
41282
  };
40941
- useEffect41(
41283
+ useEffect43(
40942
41284
  () => () => {
40943
41285
  link.remove();
40944
41286
  instance = null;
@@ -40953,17 +41295,17 @@ function useSaveGltfAs(options = {}) {
40953
41295
  }
40954
41296
  function useExportGltfUrl(options = {}) {
40955
41297
  const parse2 = useParser(options);
40956
- const [url, setUrl] = useState35();
40957
- const [error, setError] = useState35();
41298
+ const [url, setUrl] = useState37();
41299
+ const [error, setError] = useState37();
40958
41300
  const ref = useCallback23(
40959
41301
  (instance) => parse2(instance).then(setUrl).catch(setError),
40960
41302
  []
40961
41303
  );
40962
- useEffect41(() => () => URL.revokeObjectURL(url), [url]);
41304
+ useEffect43(() => () => URL.revokeObjectURL(url), [url]);
40963
41305
  return [ref, url, error];
40964
41306
  }
40965
41307
  function useParser(options = {}) {
40966
- const exporter = useMemo27(() => new GLTFExporter2(), []);
41308
+ const exporter = useMemo29(() => new GLTFExporter2(), []);
40967
41309
  return (instance) => {
40968
41310
  const { promise, resolve, reject } = Promise.withResolvers();
40969
41311
  exporter.parse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.439",
3
+ "version": "0.0.441",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",