@tscircuit/3d-viewer 0.0.641 → 0.0.642

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -60,7 +60,10 @@ type CadViewerProps = Omit<React.ComponentProps<typeof CadViewerJscad>, "circuit
60
60
  circuitJson?: any[];
61
61
  /** Settled circuit retaining authored assembly.explosion stages. */
62
62
  circuit?: RootCircuit;
63
- /** Pre-rendered circuit-json-to-gltf GLB URL. Bypasses geometry/model generation. */
63
+ /**
64
+ * Pre-rendered circuit-json-to-gltf GLB URL. Bypasses geometry/model generation.
65
+ * Changing the GLB preserves the camera. Change the viewer key to frame a different board.
66
+ */
64
67
  glbUrl?: string;
65
68
  /** In-memory alternative to glbUrl. Supply only one GLB source. */
66
69
  glbData?: ArrayBuffer;
package/dist/index.js CHANGED
@@ -22344,7 +22344,7 @@ import * as THREE31 from "three";
22344
22344
  // package.json
22345
22345
  var package_default = {
22346
22346
  name: "@tscircuit/3d-viewer",
22347
- version: "0.0.640",
22347
+ version: "0.0.641",
22348
22348
  repository: {
22349
22349
  type: "git",
22350
22350
  url: "https://github.com/tscircuit/3d-viewer"
@@ -32042,33 +32042,51 @@ var CadViewerManifold = ({
32042
32042
  var CadViewerManifold_default = CadViewerManifold;
32043
32043
 
32044
32044
  // src/CadViewerGlb.tsx
32045
- import { useEffect as useEffect37, useMemo as useMemo31, useState as useState23 } from "react";
32045
+ import {
32046
+ useEffect as useEffect37,
32047
+ useLayoutEffect as useLayoutEffect2,
32048
+ useState as useState23
32049
+ } from "react";
32046
32050
 
32047
32051
  // src/hooks/useGlbModel.ts
32048
- import { useEffect as useEffect36, useMemo as useMemo30, useState as useState22 } from "react";
32052
+ import { useEffect as useEffect36, useLayoutEffect, useMemo as useMemo30, useState as useState22 } from "react";
32049
32053
  function useGlbModel(source) {
32050
32054
  const request = useMemo30(() => ({ source }), [source]);
32051
32055
  const [result, setResult] = useState22();
32056
+ const displayedGlb = result?.loadedGlb;
32052
32057
  useEffect36(() => {
32053
32058
  const controller = new AbortController();
32054
- let model;
32055
- loadGlbScene(request.source, controller.signal).then((loaded) => {
32056
- model = loaded;
32057
- if (controller.signal.aborted) disposeGlbScene(loaded);
32058
- else setResult({ request, model: loaded });
32059
+ let loadedGlb;
32060
+ loadGlbScene(request.source, controller.signal).then((model) => {
32061
+ if (controller.signal.aborted) {
32062
+ disposeGlbScene(model);
32063
+ return;
32064
+ }
32065
+ loadedGlb = { model, wasDisplayed: false };
32066
+ setResult({ request, loadedGlb });
32059
32067
  }).catch((error) => {
32060
- if (!controller.signal.aborted)
32061
- setResult({
32062
- request,
32063
- error: error instanceof Error ? error.message : String(error)
32064
- });
32068
+ if (controller.signal.aborted) return;
32069
+ setResult((previous) => ({
32070
+ request,
32071
+ loadedGlb: previous?.loadedGlb,
32072
+ error: error instanceof Error ? error.message : String(error)
32073
+ }));
32065
32074
  });
32066
32075
  return () => {
32067
32076
  controller.abort();
32068
- if (model) disposeGlbScene(model);
32077
+ if (loadedGlb && !loadedGlb.wasDisplayed) disposeGlbScene(loadedGlb.model);
32069
32078
  };
32070
32079
  }, [request]);
32071
- return result?.request === request ? result : void 0;
32080
+ useLayoutEffect(() => {
32081
+ if (!displayedGlb) return;
32082
+ displayedGlb.wasDisplayed = true;
32083
+ return () => disposeGlbScene(displayedGlb.model);
32084
+ }, [displayedGlb]);
32085
+ return {
32086
+ model: displayedGlb?.model,
32087
+ isLoading: result?.request !== request,
32088
+ error: result?.request === request ? result.error : void 0
32089
+ };
32072
32090
  }
32073
32091
 
32074
32092
  // src/CadViewerGlb.tsx
@@ -32081,19 +32099,7 @@ function GlbScene({
32081
32099
  const { rootObject } = useThree();
32082
32100
  const { addHoverable, removeHoverable } = useHover();
32083
32101
  const [hover, setHover] = useState23();
32084
- const bounds = useMemo31(() => {
32085
- const box = new THREE62.Box3().setFromObject(model);
32086
- if (box.isEmpty()) return;
32087
- return {
32088
- minX: box.min.x,
32089
- maxX: box.max.x,
32090
- minY: box.min.y,
32091
- maxY: box.max.y,
32092
- minZ: box.min.z,
32093
- maxZ: box.max.z
32094
- };
32095
- }, [model]);
32096
- useEffect37(() => {
32102
+ useLayoutEffect2(() => {
32097
32103
  rootObject.add(model);
32098
32104
  return () => {
32099
32105
  rootObject.remove(model);
@@ -32144,54 +32150,88 @@ function GlbScene({
32144
32150
  for (const mesh of meshes) removeHoverable(mesh);
32145
32151
  };
32146
32152
  }, [model, objectVisibility, addHoverable, removeHoverable]);
32153
+ return /* @__PURE__ */ jsx24(Fragment5, { children: hover && /* @__PURE__ */ jsx24(
32154
+ Html,
32155
+ {
32156
+ position: hover.position,
32157
+ style: {
32158
+ background: "white",
32159
+ color: "black",
32160
+ padding: "5px",
32161
+ borderRadius: "3px",
32162
+ fontFamily: "sans-serif",
32163
+ transform: "translate(1rem, 1rem)"
32164
+ },
32165
+ children: hover.name
32166
+ }
32167
+ ) });
32168
+ }
32169
+ function CadViewerGlb({
32170
+ source,
32171
+ objectVisibility,
32172
+ ...containerProps
32173
+ }) {
32174
+ const { model, isLoading, error } = useGlbModel(source);
32175
+ const message = error ? `Unable to load 3D view: ${error}` : "Loading 3D view...";
32176
+ const role = error ? "alert" : "status";
32177
+ if (!model) return /* @__PURE__ */ jsx24("div", { role, children: message });
32147
32178
  return /* @__PURE__ */ jsxs8(Fragment5, { children: [
32148
- bounds && /* @__PURE__ */ jsx24(FitCameraToComparison, { bounds }),
32149
- hover && /* @__PURE__ */ jsx24(
32150
- Html,
32179
+ /* @__PURE__ */ jsx24(
32180
+ GlbViewport,
32151
32181
  {
32152
- position: hover.position,
32153
- style: {
32154
- background: "white",
32155
- color: "black",
32156
- padding: "5px",
32157
- borderRadius: "3px",
32158
- fontFamily: "sans-serif",
32159
- transform: "translate(1rem, 1rem)"
32160
- },
32161
- children: hover.name
32182
+ model,
32183
+ objectVisibility,
32184
+ ...containerProps
32162
32185
  }
32163
- )
32186
+ ),
32187
+ (isLoading || error) && /* @__PURE__ */ jsx24("div", { role, style: { position: "absolute", top: 12, left: 12 }, children: message })
32164
32188
  ] });
32165
32189
  }
32166
- function CadViewerGlb({
32167
- source,
32190
+ function GlbViewport({
32191
+ model,
32168
32192
  objectVisibility,
32169
32193
  ...containerProps
32170
32194
  }) {
32171
- const result = useGlbModel(source);
32172
- const framing = useMemo31(() => {
32173
- if (!result?.model) return void 0;
32174
- const bounds = new THREE62.Box3().setFromObject(result.model);
32175
- if (bounds.isEmpty()) return void 0;
32176
- return {
32177
- boardDimensions: {
32178
- width: bounds.max.x - bounds.min.x,
32179
- height: bounds.max.y - bounds.min.y
32180
- },
32181
- boardCenter: {
32182
- x: (bounds.min.x + bounds.max.x) / 2,
32183
- y: (bounds.min.y + bounds.max.y) / 2
32184
- }
32185
- };
32186
- }, [result, source]);
32187
- if (!result) return /* @__PURE__ */ jsx24("div", { role: "status", children: "Loading 3D view..." });
32188
- if (result.error)
32189
- return /* @__PURE__ */ jsxs8("div", { role: "alert", children: [
32190
- "Unable to load 3D view: ",
32191
- result.error
32192
- ] });
32193
- if (!result.model) return null;
32194
- return /* @__PURE__ */ jsx24(CadViewerContainer, { ...containerProps, ...framing, children: /* @__PURE__ */ jsx24(GlbScene, { model: result.model, objectVisibility }) });
32195
+ const [framing, setFraming] = useState23(() => getGlbFraming(model));
32196
+ useEffect37(() => {
32197
+ if (framing) return;
32198
+ const nextFraming = getGlbFraming(model);
32199
+ if (nextFraming) setFraming(nextFraming);
32200
+ }, [framing, model]);
32201
+ return /* @__PURE__ */ jsxs8(
32202
+ CadViewerContainer,
32203
+ {
32204
+ ...containerProps,
32205
+ boardDimensions: framing?.boardDimensions,
32206
+ boardCenter: framing?.boardCenter,
32207
+ children: [
32208
+ framing && /* @__PURE__ */ jsx24(FitCameraToComparison, { bounds: framing.bounds }),
32209
+ /* @__PURE__ */ jsx24(GlbScene, { model, objectVisibility })
32210
+ ]
32211
+ }
32212
+ );
32213
+ }
32214
+ function getGlbFraming(model) {
32215
+ const box = new THREE62.Box3().setFromObject(model);
32216
+ if (box.isEmpty()) return void 0;
32217
+ return {
32218
+ bounds: {
32219
+ minX: box.min.x,
32220
+ maxX: box.max.x,
32221
+ minY: box.min.y,
32222
+ maxY: box.max.y,
32223
+ minZ: box.min.z,
32224
+ maxZ: box.max.z
32225
+ },
32226
+ boardDimensions: {
32227
+ width: box.max.x - box.min.x,
32228
+ height: box.max.y - box.min.y
32229
+ },
32230
+ boardCenter: {
32231
+ x: (box.min.x + box.max.x) / 2,
32232
+ y: (box.min.y + box.max.y) / 2
32233
+ }
32234
+ };
32195
32235
  }
32196
32236
 
32197
32237
  // node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs
@@ -32392,7 +32432,7 @@ var IS_DEVELOPMENT = false;
32392
32432
 
32393
32433
  // node_modules/@radix-ui/react-use-layout-effect/dist/index.mjs
32394
32434
  import * as React13 from "react";
32395
- var useLayoutEffect2 = globalThis?.document ? React13.useLayoutEffect : () => {
32435
+ var useLayoutEffect22 = globalThis?.document ? React13.useLayoutEffect : () => {
32396
32436
  };
32397
32437
 
32398
32438
  // node_modules/@radix-ui/react-use-controllable-state/dist/index.mjs
@@ -32416,7 +32456,7 @@ function useEffectEvent(callback) {
32416
32456
  ref.current = callback;
32417
32457
  });
32418
32458
  } else {
32419
- useLayoutEffect2(() => {
32459
+ useLayoutEffect22(() => {
32420
32460
  ref.current = callback;
32421
32461
  });
32422
32462
  }
@@ -32427,7 +32467,7 @@ __name4(useEffectEvent, "useEffectEvent");
32427
32467
  // node_modules/@radix-ui/react-use-controllable-state/dist/index.mjs
32428
32468
  var __defProp6 = Object.defineProperty;
32429
32469
  var __name5 = (target, value) => __defProp6(target, "name", { value, configurable: true });
32430
- var useInsertionEffect = React15[" useInsertionEffect ".trim().toString()] || useLayoutEffect2;
32470
+ var useInsertionEffect = React15[" useInsertionEffect ".trim().toString()] || useLayoutEffect22;
32431
32471
  function useControllableState({
32432
32472
  prop,
32433
32473
  defaultProp,
@@ -33267,7 +33307,7 @@ function createCollection2(name) {
33267
33307
  [registryRef]
33268
33308
  );
33269
33309
  const composedRefs = useComposedRefs(forwardedRef, registerElement, ref);
33270
- useLayoutEffect2(() => {
33310
+ useLayoutEffect22(() => {
33271
33311
  const node = ref.current;
33272
33312
  const registry = registryRef.current;
33273
33313
  if (node && registry.has(node)) {
@@ -33420,7 +33460,7 @@ function useCallbackRef(callback) {
33420
33460
  callbackRef.current = callback;
33421
33461
  });
33422
33462
  } else {
33423
- useLayoutEffect2(() => {
33463
+ useLayoutEffect22(() => {
33424
33464
  callbackRef.current = callback;
33425
33465
  });
33426
33466
  }
@@ -34089,7 +34129,7 @@ var useReactId = React26[" useId ".trim().toString()] || (() => void 0);
34089
34129
  var count2 = 0;
34090
34130
  function useId2(deterministicId) {
34091
34131
  const [id, setId] = React26.useState(useReactId());
34092
- useLayoutEffect2(() => {
34132
+ useLayoutEffect22(() => {
34093
34133
  if (!deterministicId) setId((reactId) => reactId ?? String(count2++));
34094
34134
  }, [deterministicId]);
34095
34135
  return deterministicId || (id ? `radix-${id}` : "");
@@ -35693,12 +35733,12 @@ var computePosition2 = (reference, floating, options) => {
35693
35733
 
35694
35734
  // node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.mjs
35695
35735
  import * as React27 from "react";
35696
- import { useLayoutEffect as useLayoutEffect3 } from "react";
35736
+ import { useLayoutEffect as useLayoutEffect4 } from "react";
35697
35737
  import * as ReactDOM3 from "react-dom";
35698
35738
  var isClient = typeof document !== "undefined";
35699
35739
  var noop = function noop2() {
35700
35740
  };
35701
- var index = isClient ? useLayoutEffect3 : noop;
35741
+ var index = isClient ? useLayoutEffect4 : noop;
35702
35742
  function deepEqual(a, b) {
35703
35743
  if (a === b) {
35704
35744
  return true;
@@ -36008,7 +36048,7 @@ var __defProp16 = Object.defineProperty;
36008
36048
  var __name15 = (target, value) => __defProp16(target, "name", { value, configurable: true });
36009
36049
  function useSize(element) {
36010
36050
  const [size4, setSize] = React28.useState(void 0);
36011
- useLayoutEffect2(() => {
36051
+ useLayoutEffect22(() => {
36012
36052
  if (element) {
36013
36053
  setSize({ width: element.offsetWidth, height: element.offsetHeight });
36014
36054
  let rAF = 0;
@@ -36215,7 +36255,7 @@ var PopperContent = /* @__PURE__ */ React29.forwardRef(
36215
36255
  ]
36216
36256
  });
36217
36257
  const setPlacementState = context.setPlacementState;
36218
- useLayoutEffect2(() => {
36258
+ useLayoutEffect22(() => {
36219
36259
  setPlacementState(placement);
36220
36260
  return () => {
36221
36261
  setPlacementState(void 0);
@@ -36223,7 +36263,7 @@ var PopperContent = /* @__PURE__ */ React29.forwardRef(
36223
36263
  }, [placement, setPlacementState]);
36224
36264
  const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
36225
36265
  const handlePlaced = useCallbackRef(onPlaced);
36226
- useLayoutEffect2(() => {
36266
+ useLayoutEffect22(() => {
36227
36267
  if (isPositioned) {
36228
36268
  handlePlaced?.();
36229
36269
  }
@@ -36232,7 +36272,7 @@ var PopperContent = /* @__PURE__ */ React29.forwardRef(
36232
36272
  const arrowY = middlewareData.arrow?.y;
36233
36273
  const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
36234
36274
  const [contentZIndex, setContentZIndex] = React29.useState();
36235
- useLayoutEffect2(() => {
36275
+ useLayoutEffect22(() => {
36236
36276
  if (content) setContentZIndex(window.getComputedStyle(content).zIndex);
36237
36277
  }, [content]);
36238
36278
  return /* @__PURE__ */ jsx32(
@@ -36342,7 +36382,7 @@ var Portal = /* @__PURE__ */ React30.forwardRef(
36342
36382
  /* @__PURE__ */ __name17(function Portal2(props, forwardedRef) {
36343
36383
  const { container: containerProp, ...portalProps } = props;
36344
36384
  const [mounted, setMounted] = React30.useState(false);
36345
- useLayoutEffect2(() => setMounted(true), []);
36385
+ useLayoutEffect22(() => setMounted(true), []);
36346
36386
  const container = containerProp || mounted && globalThis?.document?.body;
36347
36387
  return container ? ReactDOM4.createPortal(/* @__PURE__ */ jsx33(Primitive.div, { ...portalProps, ref: forwardedRef }), container) : null;
36348
36388
  }, "Portal")
@@ -36396,7 +36436,7 @@ function usePresence(present) {
36396
36436
  prevAnimationNameRef.current = "none";
36397
36437
  }
36398
36438
  }, [state]);
36399
- useLayoutEffect2(() => {
36439
+ useLayoutEffect22(() => {
36400
36440
  const styles = stylesRef.current;
36401
36441
  const wasPresent = prevPresentRef.current;
36402
36442
  const hasPresentChanged = wasPresent !== present;
@@ -36419,7 +36459,7 @@ function usePresence(present) {
36419
36459
  prevPresentRef.current = present;
36420
36460
  }
36421
36461
  }, [present, send]);
36422
- useLayoutEffect2(() => {
36462
+ useLayoutEffect22(() => {
36423
36463
  if (node) {
36424
36464
  let timeoutId;
36425
36465
  const ownerWindow = node.ownerDocument.defaultView ?? window;
@@ -36699,7 +36739,7 @@ var RovingFocusGroupItem = /* @__PURE__ */ React33.forwardRef(
36699
36739
  const getItems = useCollection(__scopeRovingFocusGroup);
36700
36740
  const { onFocusableItemAdd, onFocusableItemRemove, currentTabStopId } = context;
36701
36741
  const isHydrated = useIsHydrated2();
36702
- useLayoutEffect2(() => {
36742
+ useLayoutEffect22(() => {
36703
36743
  if (!isHydrated || !focusable) {
36704
36744
  return;
36705
36745
  }
@@ -38593,12 +38633,12 @@ var DropdownMenuSubContent = /* @__PURE__ */ React42.forwardRef(/* @__PURE__ */
38593
38633
  }, "DropdownMenuSubContent"));
38594
38634
 
38595
38635
  // src/components/ViewportSubContent.tsx
38596
- import { useLayoutEffect as useLayoutEffect5, useState as useState40 } from "react";
38636
+ import { useLayoutEffect as useLayoutEffect6, useState as useState40 } from "react";
38597
38637
  import { jsx as jsx37 } from "react/jsx-runtime";
38598
38638
  var ViewportSubContent = (props) => {
38599
38639
  const [content, setContent] = useState40(null);
38600
38640
  const [overlapWidth, setOverlapWidth] = useState40();
38601
- useLayoutEffect5(() => {
38641
+ useLayoutEffect6(() => {
38602
38642
  if (!content) return;
38603
38643
  const trigger = document.getElementById(
38604
38644
  content.getAttribute("aria-labelledby") ?? ""
@@ -39741,10 +39781,10 @@ var ContextMenu = ({
39741
39781
  };
39742
39782
 
39743
39783
  // src/components/KeyboardShortcutsDialog.tsx
39744
- import { useEffect as useEffect55, useMemo as useMemo41, useRef as useRef28, useState as useState44 } from "react";
39784
+ import { useEffect as useEffect55, useMemo as useMemo40, useRef as useRef28, useState as useState44 } from "react";
39745
39785
 
39746
39786
  // src/hooks/useRegisteredHotkey.ts
39747
- import { useEffect as useEffect54, useMemo as useMemo40, useRef as useRef27, useState as useState43 } from "react";
39787
+ import { useEffect as useEffect54, useMemo as useMemo39, useRef as useRef27, useState as useState43 } from "react";
39748
39788
  var hotkeyRegistry = /* @__PURE__ */ new Map();
39749
39789
  var subscribers = /* @__PURE__ */ new Set();
39750
39790
  var isListenerAttached = false;
@@ -39868,7 +39908,7 @@ var subscribeToRegistry = (subscriber) => {
39868
39908
  var useRegisteredHotkey = (id, handler, metadata) => {
39869
39909
  const handlerRef = useRef27(handler);
39870
39910
  handlerRef.current = handler;
39871
- const normalizedMetadata = useMemo40(
39911
+ const normalizedMetadata = useMemo39(
39872
39912
  () => ({
39873
39913
  shortcut: metadata.shortcut,
39874
39914
  description: metadata.description
@@ -39926,7 +39966,7 @@ var KeyboardShortcutsDialog = ({
39926
39966
  }, 0);
39927
39967
  }
39928
39968
  }, [open]);
39929
- const filteredHotkeys = useMemo41(() => {
39969
+ const filteredHotkeys = useMemo40(() => {
39930
39970
  const normalizedQuery = query.trim().toLowerCase();
39931
39971
  if (!normalizedQuery) {
39932
39972
  return hotkeys;
@@ -41153,10 +41193,10 @@ async function convertCircuitJsonTo3dSvg(circuitJson, options = {}) {
41153
41193
  import {
41154
41194
  GLTFExporter as GLTFExporter3
41155
41195
  } from "three/examples/jsm/exporters/GLTFExporter.js";
41156
- import { useEffect as useEffect58, useState as useState47, useMemo as useMemo42, useCallback as useCallback26 } from "react";
41196
+ import { useEffect as useEffect58, useState as useState47, useMemo as useMemo41, useCallback as useCallback26 } from "react";
41157
41197
  function useSaveGltfAs(options = {}) {
41158
41198
  const parse2 = useParser(options);
41159
- const link = useMemo42(() => document.createElement("a"), []);
41199
+ const link = useMemo41(() => document.createElement("a"), []);
41160
41200
  const saveAs = async (filename) => {
41161
41201
  const name = filename ?? options.filename ?? "";
41162
41202
  if (options.binary == null) options.binary = name.endsWith(".glb");
@@ -41191,7 +41231,7 @@ function useExportGltfUrl(options = {}) {
41191
41231
  return [ref, url, error];
41192
41232
  }
41193
41233
  function useParser(options = {}) {
41194
- const exporter = useMemo42(() => new GLTFExporter3(), []);
41234
+ const exporter = useMemo41(() => new GLTFExporter3(), []);
41195
41235
  return (instance) => {
41196
41236
  const { promise, resolve, reject } = Promise.withResolvers();
41197
41237
  exporter.parse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/3d-viewer",
3
- "version": "0.0.641",
3
+ "version": "0.0.642",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/tscircuit/3d-viewer"