@toolpath/viewer 2.3.3 → 2.5.0

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/README.md CHANGED
@@ -331,6 +331,7 @@ the part opaque. For multiselection, update your own tag array and avoid duplica
331
331
  | `showOrbitTarget` | `false` | Show the pivot marker. |
332
332
  | `theme` | Default lighting | Override lighting; part/cube/tool colors have their own theme props. |
333
333
  | `onPointerMissed` | — | Empty-space primary click, with drag guarding; use it to clear selection. |
334
+ | `webglUnavailable` | Default message | Content of the alert shown instead of the canvas when WebGL is unavailable. |
334
335
  | `style`, `className` | Full-size wrapper | Set dimensions and background on the wrapper or parent. |
335
336
  | `ref` | — | `ViewerHandle` for controls outside the canvas. |
336
337
 
@@ -340,6 +341,13 @@ the part opaque. For multiselection, update your own tag array and avoid duplica
340
341
  Named views are `top`, `bottom`, `front`, `back`, `left`, `right`, and `isometric`.
341
342
  Direction vectors point from the part toward the camera.
342
343
 
344
+ `Viewer` checks for WebGL2 once per page, on the client. If the browser will not create a context
345
+ (hardware acceleration off or the GPU blocklisted), it renders a `role="alert"`
346
+ element (`viewer-webgl-unavailable`, `data-viewer-webgl-unavailable="true"`) in
347
+ place of the canvas, asking the user to turn on graphics acceleration; children are
348
+ not rendered. Pass `webglUnavailable` to replace that message. Server rendering assumes
349
+ WebGL is available; the check runs on the client after hydration.
350
+
343
351
  #### CAD controls
344
352
 
345
353
  | Preset | Rotate | Pan | Zoom |
@@ -375,6 +383,7 @@ fails or is absent. It does not fetch a report or start an Engine job.
375
383
  | `activeDirection` | Candidate-direction index; `null`/omitted means all directions. |
376
384
  | `display`, `showEdges` | `solid` or `wireframe`; outlines default on. Wireframe always shows semantic face boundaries, not triangle diagonals. |
377
385
  | `sharpCorners` | `{ features: tags }` for externally flagged features, or `null`. |
386
+ | `threads` | `{ threads: marks }` to draw threads in their holes, or `null`. See "Showing threads". |
378
387
  | `section`, `onSectionChange` | Controlled section options and change reporting. Omit `section` to follow the viewer's section store. |
379
388
  | `onAdjacency` | Receive touching-region sets for the prepared mesh. |
380
389
  | `theme` | Partial `ViewerTheme` for the part. |
@@ -468,7 +477,7 @@ With no children, `ViewerToolbar` renders configured controls in standard order.
468
477
  For custom composition use `ViewerToolbar.Controls`, `ViewerToolbar.Divider`, and
469
478
  its `*Button` components. A button without a matching provider control throws.
470
479
  The supported action names are `stock`, `axes`, `grid`, `banana`, `tools`,
471
- `directions`, `dfm`, `sharpCorners`, `hover`, `focus`, `wireframe`, `section`,
480
+ `directions`, `dfm`, `sharpCorners`, `threads`, `hover`, `focus`, `wireframe`, `section`,
472
481
  `measure`, `fit`, `reset`, and `top`; button names capitalize the action, such as
473
482
  `ViewerToolbar.SharpCornersButton`. `useViewerToolbar()` reads those controls.
474
483
 
@@ -670,6 +679,19 @@ axis legs also have `data-axis`, and finished labels have `data-measurement-id`.
670
679
  millimetres; the `format(mm)` callback changes their displayed string, not geometry
671
680
  units or angles (which remain degrees).
672
681
 
682
+ ### Showing threads
683
+
684
+ The Engine cuts a modeled internal thread out of the mesh, so a tapped hole arrives as a
685
+ plain bore. Pass `threads={{ threads: marks }}` to draw each thread back in as shading
686
+ over its hole's cylinders, at its pitch and hand. Build a mark from a hole's Engine
687
+ datasheet with `threadMark(feature, datasheet)` from `@toolpath/viewer/engine`; it is
688
+ `null` unless the datasheet carries a thread, so only threads the Engine reports are
689
+ drawn. A `ThreadMark` is `{ regions, pitch, majorDiameter }`: `pitch` is in mm and
690
+ negative for a left-hand thread, and cylinders wider than `majorDiameter` (a counterbore)
691
+ stay plain. The axis is fitted from the mesh, so marks need no coordinates. The datasheet
692
+ gives no thread length, so a tap-drill section of the same bore below the thread is
693
+ drawn threaded too.
694
+
673
695
  ### Showing sharp corners and tool marks
674
696
 
675
697
  DFM decisions belong to the application. Pass flagged feature tags as
@@ -719,9 +741,9 @@ Part keys include `part`, `partEmissive`, `hover`, `highlight`, `picked`, `edge`
719
741
  use `measure`/`measureSnap`; cube keys are `cube`, `cubeEdge`, and `cubeLabel`.
720
742
  The canvas background comes from the wrapper/parent, independently of those colors.
721
743
 
722
- Stable DOM classes include `viewer-root`, `viewer-canvas-container`,
723
- `viewer-canvas-frame`, `viewer-canvas`, `viewer-hover-card`, `viewer-toolbar-stack`,
724
- `viewer-toolbar`, `viewer-toolbar-button`, `viewer-toolbar-icon`,
744
+ Stable DOM classes include `viewer-root`, `viewer-webgl-unavailable`,
745
+ `viewer-canvas-container`, `viewer-canvas-frame`, `viewer-canvas`, `viewer-hover-card`,
746
+ `viewer-toolbar-stack`, `viewer-toolbar`, `viewer-toolbar-button`, `viewer-toolbar-icon`,
725
747
  `viewer-toolbar-tooltip`, and `viewer-toolbar-divider`. Caller `className` values
726
748
  append to the component's hook. Corresponding `data-viewer-*` attributes identify
727
749
  the DOM boundaries; `data-viewer-toolbar-action` contains the control's action name.
@@ -9,9 +9,9 @@ import {
9
9
  useLayoutEffect as useLayoutEffect3,
10
10
  useMemo as useMemo7,
11
11
  useRef as useRef6,
12
- useSyncExternalStore
12
+ useSyncExternalStore as useSyncExternalStore2
13
13
  } from "react";
14
- import { Vector3 as Vector312 } from "three";
14
+ import { Vector3 as Vector313 } from "three";
15
15
 
16
16
  // src/part-context.tsx
17
17
  import { createContext, useContext } from "react";
@@ -863,6 +863,7 @@ import {
863
863
  DataTexture,
864
864
  DoubleSide as DoubleSide2,
865
865
  Float32BufferAttribute as Float32BufferAttribute3,
866
+ FloatType,
866
867
  Group as Group2,
867
868
  LineBasicMaterial,
868
869
  LineSegments,
@@ -871,7 +872,7 @@ import {
871
872
  RedFormat,
872
873
  RGBAFormat,
873
874
  UnsignedByteType,
874
- Vector3 as Vector35
875
+ Vector3 as Vector36
875
876
  } from "three";
876
877
 
877
878
  // src/render/edges.ts
@@ -1488,9 +1489,147 @@ function emptyCreases() {
1488
1489
  };
1489
1490
  }
1490
1491
 
1492
+ // src/render/threads.ts
1493
+ import {
1494
+ Vector3 as Vector35
1495
+ } from "three";
1496
+ var THREAD_TEXTURE_ROWS = 2;
1497
+ var MAX_AXIAL_NORMAL = 0.2;
1498
+ var MIN_AXIS_CROSS = 0.1;
1499
+ var DIAMETER_SLACK = 1.1;
1500
+ function facetsOf(position, ranges) {
1501
+ const facets = [];
1502
+ const a = new Vector35();
1503
+ const b = new Vector35();
1504
+ const c = new Vector35();
1505
+ for (const range of ranges) {
1506
+ for (let triangle = range.start; triangle < range.end; triangle += 1) {
1507
+ const first = triangle * 3;
1508
+ if (first + 2 >= position.count) break;
1509
+ a.fromBufferAttribute(position, first);
1510
+ b.fromBufferAttribute(position, first + 1);
1511
+ c.fromBufferAttribute(position, first + 2);
1512
+ const normal = new Vector35().subVectors(b, a).cross(new Vector35().subVectors(c, a));
1513
+ const doubleArea = normal.length();
1514
+ if (doubleArea === 0) continue;
1515
+ facets.push({
1516
+ centre: new Vector35().add(a).add(b).add(c).divideScalar(3),
1517
+ normal: normal.divideScalar(doubleArea),
1518
+ area: doubleArea / 2
1519
+ });
1520
+ }
1521
+ }
1522
+ return facets;
1523
+ }
1524
+ function axisOf(facets) {
1525
+ const first = facets[0];
1526
+ if (!first) return null;
1527
+ let furthest = first;
1528
+ for (const facet of facets) {
1529
+ if (Math.abs(facet.normal.dot(first.normal)) < Math.abs(furthest.normal.dot(first.normal))) {
1530
+ furthest = facet;
1531
+ }
1532
+ }
1533
+ const axis = new Vector35().crossVectors(first.normal, furthest.normal);
1534
+ if (axis.length() < MIN_AXIS_CROSS) return null;
1535
+ axis.normalize();
1536
+ const allSquare = facets.every((facet) => Math.abs(facet.normal.dot(axis)) <= MAX_AXIAL_NORMAL);
1537
+ return allSquare ? axis : null;
1538
+ }
1539
+ function fitBore(position, ranges) {
1540
+ const facets = facetsOf(position, ranges);
1541
+ const axis = axisOf(facets);
1542
+ if (!axis) return null;
1543
+ const u = new Vector35().crossVectors(axis, Math.abs(axis.x) < 0.9 ? new Vector35(1, 0, 0) : new Vector35(0, 1, 0)).normalize();
1544
+ const v = new Vector35().crossVectors(axis, u);
1545
+ let xx = 0;
1546
+ let xy = 0;
1547
+ let yy = 0;
1548
+ let rx = 0;
1549
+ let ry = 0;
1550
+ let along = 0;
1551
+ let total = 0;
1552
+ for (const { centre, normal, area } of facets) {
1553
+ const mx = normal.dot(u);
1554
+ const my = normal.dot(v);
1555
+ const length = Math.hypot(mx, my);
1556
+ const nx = mx / length;
1557
+ const ny = my / length;
1558
+ const qx = centre.dot(u);
1559
+ const qy = centre.dot(v);
1560
+ const pxx = 1 - nx * nx;
1561
+ const pxy = -nx * ny;
1562
+ const pyy = 1 - ny * ny;
1563
+ xx += area * pxx;
1564
+ xy += area * pxy;
1565
+ yy += area * pyy;
1566
+ rx += area * (pxx * qx + pxy * qy);
1567
+ ry += area * (pxy * qx + pyy * qy);
1568
+ along += area * centre.dot(axis);
1569
+ total += area;
1570
+ }
1571
+ const determinant = xx * yy - xy * xy;
1572
+ if (Math.abs(determinant) < 1e-9 * total * total) return null;
1573
+ const cx = (yy * rx - xy * ry) / determinant;
1574
+ const cy = (xx * ry - xy * rx) / determinant;
1575
+ let radius = 0;
1576
+ for (const { centre, area } of facets) {
1577
+ radius += area * Math.hypot(centre.dot(u) - cx, centre.dot(v) - cy);
1578
+ }
1579
+ return {
1580
+ origin: new Vector35().addScaledVector(u, cx).addScaledVector(v, cy).addScaledVector(axis, along / total),
1581
+ axis,
1582
+ radius: radius / total
1583
+ };
1584
+ }
1585
+ function threadedRanges(position, model, mark) {
1586
+ const regions = [];
1587
+ const ranges = [];
1588
+ for (const region of mark.regions) {
1589
+ const range = model.regionIndex.rangeForRegion(region);
1590
+ if (!range) continue;
1591
+ const bore = fitBore(position, [range]);
1592
+ if (!bore || bore.radius * 2 > mark.majorDiameter * DIAMETER_SLACK) continue;
1593
+ regions.push(region);
1594
+ ranges.push(range);
1595
+ }
1596
+ return { regions, ranges };
1597
+ }
1598
+ function writeThreadTexels(data, width, geometry, model, texels, options) {
1599
+ data.fill(0);
1600
+ if (!options) return;
1601
+ const position = geometry.getAttribute("position");
1602
+ for (const mark of options.threads) {
1603
+ if (!Number.isFinite(mark.pitch) || mark.pitch === 0) continue;
1604
+ const { regions, ranges } = threadedRanges(position, model, mark);
1605
+ const bore = fitBore(position, ranges);
1606
+ if (!bore) continue;
1607
+ for (const region of regions) {
1608
+ const column = texels.get(region);
1609
+ if (column === void 0) continue;
1610
+ const first = column * 4;
1611
+ const second = (width + column) * 4;
1612
+ data[first] = bore.origin.x;
1613
+ data[first + 1] = bore.origin.y;
1614
+ data[first + 2] = bore.origin.z;
1615
+ data[first + 3] = mark.pitch;
1616
+ data[second] = bore.axis.x;
1617
+ data[second + 1] = bore.axis.y;
1618
+ data[second + 2] = bore.axis.z;
1619
+ }
1620
+ }
1621
+ }
1622
+ function threadKey(options) {
1623
+ if (!options) return "";
1624
+ return options.threads.map((mark) => `${mark.regions.join(",")}:${mark.pitch}:${mark.majorDiameter}`).join("|");
1625
+ }
1626
+
1491
1627
  // src/render/part.ts
1492
1628
  var REGION_ATTRIBUTE = "aRegion";
1493
1629
  var EMISSIVE_MIX = 0.4;
1630
+ var THREAD_TILT = Math.tan(Math.PI / 4);
1631
+ var THREAD_ROOT_SHADE = 0.45;
1632
+ var THREAD_CREST_LIGHT = 1.15;
1494
1633
  function buildRegionTexels(model) {
1495
1634
  return new Map(model.regions.map((region, column) => [region.idx, column]));
1496
1635
  }
@@ -1522,6 +1661,10 @@ function createPart(model, geometry, theme) {
1522
1661
  const opacity = new Uint8Array(width).fill(255);
1523
1662
  const opacityTexture = new DataTexture(opacity, width, 1, RedFormat, UnsignedByteType);
1524
1663
  opacityTexture.needsUpdate = true;
1664
+ const threads = new Float32Array(width * THREAD_TEXTURE_ROWS * 4);
1665
+ const threadTexture = new DataTexture(threads, width, THREAD_TEXTURE_ROWS, RGBAFormat, FloatType);
1666
+ threadTexture.needsUpdate = true;
1667
+ let threadsKey = "";
1525
1668
  const focusState = { value: false };
1526
1669
  const wireframe = { value: false };
1527
1670
  let focused = false;
@@ -1547,15 +1690,26 @@ function createPart(model, geometry, theme) {
1547
1690
  shader.uniforms["uWireframe"] = wireframe;
1548
1691
  shader.uniforms["uFocus"] = focusState;
1549
1692
  shader.uniforms["uOpaqueFocusPass"] = { value: opaqueFocusPass };
1693
+ shader.uniforms["uThreads"] = { value: threadTexture };
1550
1694
  shader.vertexShader = shader.vertexShader.replace(
1551
1695
  "#include <common>",
1552
1696
  `#include <common>
1553
1697
  attribute float ${REGION_ATTRIBUTE};
1554
- varying float vRegion;`
1698
+ uniform sampler2D uThreads;
1699
+ varying float vRegion;
1700
+ varying vec3 vThreadPosition;
1701
+ varying vec4 vThreadOrigin;
1702
+ varying vec3 vThreadAxis;
1703
+ varying vec3 vThreadAxisView;`
1555
1704
  ).replace(
1556
1705
  "#include <begin_vertex>",
1557
1706
  `#include <begin_vertex>
1558
- vRegion = ${REGION_ATTRIBUTE};`
1707
+ vRegion = ${REGION_ATTRIBUTE};
1708
+ // Every vertex of a triangle is in one region, so these are flat across it.
1709
+ vThreadOrigin = texelFetch(uThreads, ivec2(int(vRegion + 0.5), 0), 0);
1710
+ vThreadAxis = texelFetch(uThreads, ivec2(int(vRegion + 0.5), 1), 0).xyz;
1711
+ vThreadAxisView = normalMatrix * vThreadAxis;
1712
+ vThreadPosition = position;`
1559
1713
  );
1560
1714
  shader.fragmentShader = shader.fragmentShader.replace(
1561
1715
  "#include <common>",
@@ -1566,6 +1720,10 @@ function createPart(model, geometry, theme) {
1566
1720
  uniform bool uFocus;
1567
1721
  uniform bool uOpaqueFocusPass;
1568
1722
  varying float vRegion;
1723
+ varying vec3 vThreadPosition;
1724
+ varying vec4 vThreadOrigin;
1725
+ varying vec3 vThreadAxis;
1726
+ varying vec3 vThreadAxisView;
1569
1727
  vec4 regionState;`
1570
1728
  ).replace(
1571
1729
  "#include <color_fragment>",
@@ -1578,6 +1736,46 @@ function createPart(model, geometry, theme) {
1578
1736
  diffuseColor.a *= regionOpacity;
1579
1737
  if (uWireframe) diffuseColor.a *= regionState.a;
1580
1738
  `
1739
+ ).replace(
1740
+ "#include <normal_fragment_maps>",
1741
+ `#include <normal_fragment_maps>
1742
+ // Worked out for every pixel, threaded or not, and applied only where
1743
+ // there is a thread: derivatives are undefined inside a branch that
1744
+ // differs between neighbouring pixels, as it does along a region's edge.
1745
+ bool threaded = vThreadOrigin.w != 0.0;
1746
+ float threadPitch = threaded ? vThreadOrigin.w : 1.0;
1747
+ vec3 threadAxis = threaded ? normalize(vThreadAxis) : vec3(0.0, 0.0, 1.0);
1748
+ vec3 threadOffset = vThreadPosition - vThreadOrigin.xyz;
1749
+ float threadAlong = dot(threadOffset, threadAxis);
1750
+ vec3 threadU = normalize(cross(
1751
+ threadAxis,
1752
+ abs(threadAxis.x) < 0.9 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0)
1753
+ ));
1754
+ vec3 threadV = cross(threadAxis, threadU);
1755
+ float threadX = dot(threadOffset, threadU);
1756
+ float threadY = dot(threadOffset, threadV);
1757
+ float threadTurn = atan(threadY, threadX) / PI2;
1758
+ float threadPhase = threadAlong / threadPitch - threadTurn;
1759
+ // Two angle seams on opposite sides, so the blur estimate has none.
1760
+ float threadBlur = fwidth(threadAlong / threadPitch) + min(
1761
+ fwidth(threadTurn),
1762
+ fwidth(atan(-threadY, -threadX) / PI2)
1763
+ );
1764
+ if (threaded) {
1765
+ float threadStrength = 1.0 - smoothstep(0.2, 0.45, threadBlur);
1766
+ // The V of the thread: 1 at its root (phase 0), 0 at its crest (phase \xBD).
1767
+ float threadDepth = abs(fract(threadPhase) - 0.5) * 2.0;
1768
+ float threadSlope = clamp(sin(PI2 * threadPhase) * 3.0, -1.0, 1.0) * sign(threadPitch);
1769
+ normal = normalize(
1770
+ normal - ${THREAD_TILT.toFixed(4)} * threadStrength * threadSlope * normalize(vThreadAxisView)
1771
+ );
1772
+ float threadShade = mix(
1773
+ ${THREAD_CREST_LIGHT.toFixed(2)},
1774
+ ${THREAD_ROOT_SHADE.toFixed(2)},
1775
+ smoothstep(0.0, 1.0, threadDepth)
1776
+ );
1777
+ diffuseColor.rgb *= mix(1.0, threadShade, threadStrength);
1778
+ }`
1581
1779
  ).replace(
1582
1780
  "#include <emissivemap_fragment>",
1583
1781
  `#include <emissivemap_fragment>
@@ -1629,7 +1827,7 @@ function createPart(model, geometry, theme) {
1629
1827
  const object = new Group2();
1630
1828
  object.add(focusMesh, mesh, edges);
1631
1829
  const scratchColor = new Color2();
1632
- const scratchVector = new Vector35();
1830
+ const scratchVector = new Vector36();
1633
1831
  const paintRegion = (region, color, weight) => {
1634
1832
  const column = texels.get(region);
1635
1833
  if (column === void 0) return;
@@ -1758,6 +1956,13 @@ function createPart(model, geometry, theme) {
1758
1956
  setHoveredSharpCorner(index) {
1759
1957
  sharpLines?.setHovered(index);
1760
1958
  },
1959
+ setThreads(options) {
1960
+ const key = threadKey(options);
1961
+ if (key === threadsKey) return;
1962
+ threadsKey = key;
1963
+ writeThreadTexels(threads, width, geometry, model, texels, options);
1964
+ threadTexture.needsUpdate = true;
1965
+ },
1761
1966
  dispose() {
1762
1967
  object.clear();
1763
1968
  if (geometry.getAttribute(REGION_ATTRIBUTE) === regionAttribute) {
@@ -1770,6 +1975,7 @@ function createPart(model, geometry, theme) {
1770
1975
  sharpLines?.dispose();
1771
1976
  stateTexture.dispose();
1772
1977
  opacityTexture.dispose();
1978
+ threadTexture.dispose();
1773
1979
  }
1774
1980
  };
1775
1981
  }
@@ -1883,9 +2089,10 @@ import {
1883
2089
  useEffect as useEffect4,
1884
2090
  useImperativeHandle,
1885
2091
  useMemo as useMemo4,
1886
- useRef as useRef3
2092
+ useRef as useRef3,
2093
+ useSyncExternalStore
1887
2094
  } from "react";
1888
- import { Box3 as Box33, Vector3 as Vector39 } from "three";
2095
+ import { Box3 as Box33, Vector3 as Vector310 } from "three";
1889
2096
 
1890
2097
  // src/camera.tsx
1891
2098
  import { useFrame, useThree as useThree2 } from "@react-three/fiber";
@@ -1902,7 +2109,7 @@ import {
1902
2109
  Sphere,
1903
2110
  Spherical,
1904
2111
  Vector2 as Vector23,
1905
- Vector3 as Vector36,
2112
+ Vector3 as Vector37,
1906
2113
  Vector4 as Vector42
1907
2114
  } from "three";
1908
2115
 
@@ -2000,7 +2207,7 @@ CameraControls.install({
2000
2207
  Sphere,
2001
2208
  Spherical,
2002
2209
  Vector2: Vector23,
2003
- Vector3: Vector36,
2210
+ Vector3: Vector37,
2004
2211
  Vector4: Vector42
2005
2212
  }
2006
2213
  });
@@ -2023,9 +2230,9 @@ var ExtendedCameraControls = class extends CameraControls {
2023
2230
  // Scratch objects — `#onPointerMove` and `#adaptUpVector` run at pointer and
2024
2231
  // frame rate respectively, so neither may allocate.
2025
2232
  #spherical = new Spherical();
2026
- #scratchA = new Vector36();
2027
- #scratchB = new Vector36();
2028
- #scratchC = new Vector36();
2233
+ #scratchA = new Vector37();
2234
+ #scratchB = new Vector37();
2235
+ #scratchC = new Vector37();
2029
2236
  constructor(camera, domElement, options = {}) {
2030
2237
  super(camera, domElement);
2031
2238
  this.#domElement = domElement;
@@ -2363,6 +2570,22 @@ function counted(notify) {
2363
2570
  };
2364
2571
  }
2365
2572
 
2573
+ // src/render/webgl.ts
2574
+ var isWebGLAvailable = () => {
2575
+ if (typeof document === "undefined") {
2576
+ return false;
2577
+ }
2578
+ try {
2579
+ const context = document.createElement("canvas").getContext("webgl2");
2580
+ context?.getExtension("WEBGL_lose_context")?.loseContext();
2581
+ return context !== null;
2582
+ } catch {
2583
+ return false;
2584
+ }
2585
+ };
2586
+ var probed;
2587
+ var webGLAvailableSnapshot = () => probed ??= isWebGLAvailable();
2588
+
2366
2589
  // src/target-marker.tsx
2367
2590
  import { useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
2368
2591
  import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
@@ -2370,7 +2593,7 @@ import {
2370
2593
  DoubleSide as DoubleSide3,
2371
2594
  RingGeometry,
2372
2595
  SphereGeometry,
2373
- Vector3 as Vector37
2596
+ Vector3 as Vector38
2374
2597
  } from "three";
2375
2598
 
2376
2599
  // src/render/target.ts
@@ -2413,8 +2636,8 @@ var TargetMarker = ({ controlsRef }) => {
2413
2636
  const heldUntil = useRef2(0);
2414
2637
  const painted = useRef2(-1);
2415
2638
  const placed = useRef2(false);
2416
- const lastTarget = useMemo3(() => new Vector37(), []);
2417
- const scratch = useMemo3(() => new Vector37(), []);
2639
+ const lastTarget = useMemo3(() => new Vector38(), []);
2640
+ const scratch = useMemo3(() => new Vector38(), []);
2418
2641
  useEffect3(() => {
2419
2642
  const controls = controlsRef.current;
2420
2643
  if (!controls) return void 0;
@@ -2508,13 +2731,41 @@ var TargetMarker = ({ controlsRef }) => {
2508
2731
  ] });
2509
2732
  };
2510
2733
 
2734
+ // src/webgl-unavailable.tsx
2735
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2736
+ var STYLE = {
2737
+ display: "flex",
2738
+ flexDirection: "column",
2739
+ alignItems: "center",
2740
+ justifyContent: "center",
2741
+ gap: "0.5rem",
2742
+ height: "100%",
2743
+ padding: "1rem",
2744
+ boxSizing: "border-box",
2745
+ textAlign: "center"
2746
+ };
2747
+ var DEFAULT_MESSAGE = /* @__PURE__ */ jsxs2(Fragment, { children: [
2748
+ /* @__PURE__ */ jsx3("strong", { children: "3D view unavailable" }),
2749
+ /* @__PURE__ */ jsx3("span", { children: "WebGL is blocked in this browser. Turn on graphics (hardware) acceleration in your browser settings, then restart the browser." })
2750
+ ] });
2751
+ var WebGLUnavailable = ({ children = DEFAULT_MESSAGE }) => /* @__PURE__ */ jsx3(
2752
+ "div",
2753
+ {
2754
+ className: "viewer-webgl-unavailable",
2755
+ "data-viewer-webgl-unavailable": "true",
2756
+ role: "alert",
2757
+ style: STYLE,
2758
+ children
2759
+ }
2760
+ );
2761
+
2511
2762
  // src/render/view-cube.ts
2512
2763
  import {
2513
2764
  BufferGeometry as BufferGeometry2,
2514
2765
  CanvasTexture,
2515
2766
  Float32BufferAttribute as Float32BufferAttribute4,
2516
2767
  SRGBColorSpace,
2517
- Vector3 as Vector38
2768
+ Vector3 as Vector39
2518
2769
  } from "three";
2519
2770
  var VIEW_NAMES = [
2520
2771
  // The six faces.
@@ -2598,12 +2849,12 @@ function viewUp(direction) {
2598
2849
  return { x: 0, y: 0, z: 1 };
2599
2850
  }
2600
2851
  function squaredUp(direction, currentUp) {
2601
- const forward = new Vector38(direction.x, direction.y, direction.z).normalize();
2852
+ const forward = new Vector39(direction.x, direction.y, direction.z).normalize();
2602
2853
  const canonical = viewUp(direction);
2603
- const zero = new Vector38(canonical.x, canonical.y, canonical.z);
2854
+ const zero = new Vector39(canonical.x, canonical.y, canonical.z);
2604
2855
  zero.addScaledVector(forward, -forward.dot(zero)).normalize();
2605
- const quarter = new Vector38().crossVectors(forward, zero);
2606
- const current = new Vector38(currentUp.x, currentUp.y, currentUp.z);
2856
+ const quarter = new Vector39().crossVectors(forward, zero);
2857
+ const current = new Vector39(currentUp.x, currentUp.y, currentUp.z);
2607
2858
  const rolls = [zero, quarter, zero.clone().negate(), quarter.clone().negate()];
2608
2859
  const nearest = rolls.reduce(
2609
2860
  (best, roll) => roll.dot(current) > best.dot(current) ? roll : best
@@ -2621,11 +2872,11 @@ function cubeZones(chamfer = CHAMFER) {
2621
2872
  const direction = viewVector(name);
2622
2873
  const [sx, sy, sz] = VIEW_SIGNS[name];
2623
2874
  const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
2624
- const normal = new Vector38(direction.x, direction.y, direction.z);
2875
+ const normal = new Vector39(direction.x, direction.y, direction.z);
2625
2876
  let polygon;
2626
2877
  if (kind === "face") {
2627
2878
  const up = viewUp(direction);
2628
- const upVector = new Vector38(up.x, up.y, up.z);
2879
+ const upVector = new Vector39(up.x, up.y, up.z);
2629
2880
  const right = normal.clone().negate().cross(upVector).normalize();
2630
2881
  const square = [
2631
2882
  [-1, -1],
@@ -2647,7 +2898,7 @@ function cubeZones(chamfer = CHAMFER) {
2647
2898
  [chamfer, 1, -chamfer]
2648
2899
  ];
2649
2900
  polygon = rectangle.map(
2650
- ([onA, onB, onC]) => new Vector38().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2901
+ ([onA, onB, onC]) => new Vector39().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2651
2902
  );
2652
2903
  } else {
2653
2904
  const triangle = [
@@ -2655,11 +2906,11 @@ function cubeZones(chamfer = CHAMFER) {
2655
2906
  [chamfer, 1, chamfer],
2656
2907
  [chamfer, chamfer, 1]
2657
2908
  ];
2658
- polygon = triangle.map(([onX, onY, onZ]) => new Vector38(sx * onX, sy * onY, sz * onZ));
2909
+ polygon = triangle.map(([onX, onY, onZ]) => new Vector39(sx * onX, sy * onY, sz * onZ));
2659
2910
  }
2660
2911
  const [first, second, third] = polygon;
2661
2912
  if (first && second && third) {
2662
- const facing = new Vector38().subVectors(second, first).cross(new Vector38().subVectors(third, first));
2913
+ const facing = new Vector39().subVectors(second, first).cross(new Vector39().subVectors(third, first));
2663
2914
  if (facing.dot(normal) < 0) polygon.reverse();
2664
2915
  }
2665
2916
  return { name, kind, direction, polygon: polygon.map(vec) };
@@ -2749,7 +3000,7 @@ function labelTexture(label, color) {
2749
3000
  }
2750
3001
 
2751
3002
  // src/viewer.tsx
2752
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
3003
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2753
3004
  var ViewerControlsContext = createContext3(null);
2754
3005
  var useViewerControls = () => {
2755
3006
  const controls = useContext3(ViewerControlsContext);
@@ -2789,10 +3040,10 @@ var ViewerScene = ({
2789
3040
  const framedRef = useRef3(null);
2790
3041
  const scratchBox = useMemo4(() => new Box33(), []);
2791
3042
  const scratchBoundary = useMemo4(() => new Box33(), []);
2792
- const scratchDirection = useMemo4(() => new Vector39(), []);
2793
- const scratchView = useMemo4(() => new Vector39(), []);
2794
- const scratchTarget = useMemo4(() => new Vector39(), []);
2795
- const scratchPosition = useMemo4(() => new Vector39(), []);
3043
+ const scratchDirection = useMemo4(() => new Vector310(), []);
3044
+ const scratchView = useMemo4(() => new Vector310(), []);
3045
+ const scratchTarget = useMemo4(() => new Vector310(), []);
3046
+ const scratchPosition = useMemo4(() => new Vector310(), []);
2796
3047
  const applyLimits = useCallback(
2797
3048
  (bounds, framed) => {
2798
3049
  controlsRef.current?.applyLimits(
@@ -2841,8 +3092,8 @@ var ViewerScene = ({
2841
3092
  (bounds) => {
2842
3093
  const controls = controlsRef.current;
2843
3094
  if (!controls) return false;
2844
- const target = controls.getTarget(new Vector39());
2845
- const direction = currentViewDirection(camera, target, new Vector39());
3095
+ const target = controls.getTarget(new Vector310());
3096
+ const direction = currentViewDirection(camera, target, new Vector310());
2846
3097
  const distance = fitDistance(projection, size, bounds, DEFAULT_FIT_MARGIN);
2847
3098
  const position = direction.multiplyScalar(distance).add(bounds.center);
2848
3099
  void controls.setLookAt(
@@ -2867,8 +3118,8 @@ var ViewerScene = ({
2867
3118
  const fitContent = useCallback(() => {
2868
3119
  const controls = controlsRef.current;
2869
3120
  if (!controls) return false;
2870
- const target = controls.getTarget(new Vector39());
2871
- return frame(currentViewDirection(camera, target, new Vector39()), true);
3121
+ const target = controls.getTarget(new Vector310());
3122
+ return frame(currentViewDirection(camera, target, new Vector310()), true);
2872
3123
  }, [camera, frame]);
2873
3124
  const resetContent = useCallback(() => {
2874
3125
  const bounds = measure();
@@ -2958,18 +3209,18 @@ var ViewerScene = ({
2958
3209
  initialFrameComplete.current = resetContent();
2959
3210
  }
2960
3211
  });
2961
- return /* @__PURE__ */ jsx3(ViewerTapProvider, { value: isTap, children: /* @__PURE__ */ jsxs2(ViewerRetargetContext.Provider, { value: retargetOn, children: [
2962
- /* @__PURE__ */ jsxs2("group", { ref: lightsRef, children: [
2963
- /* @__PURE__ */ jsx3("ambientLight", { color: theme.ambient, intensity: theme.ambientIntensity }),
2964
- /* @__PURE__ */ jsx3(
3212
+ return /* @__PURE__ */ jsx4(ViewerTapProvider, { value: isTap, children: /* @__PURE__ */ jsxs3(ViewerRetargetContext.Provider, { value: retargetOn, children: [
3213
+ /* @__PURE__ */ jsxs3("group", { ref: lightsRef, children: [
3214
+ /* @__PURE__ */ jsx4("ambientLight", { color: theme.ambient, intensity: theme.ambientIntensity }),
3215
+ /* @__PURE__ */ jsx4(
2965
3216
  "hemisphereLight",
2966
3217
  {
2967
3218
  args: [theme.hemisphereSky, theme.hemisphereGround, theme.hemisphereIntensity]
2968
3219
  }
2969
3220
  )
2970
3221
  ] }),
2971
- /* @__PURE__ */ jsx3("group", { ref: contentRef, children }),
2972
- /* @__PURE__ */ jsx3(
3222
+ /* @__PURE__ */ jsx4("group", { ref: contentRef, children }),
3223
+ /* @__PURE__ */ jsx4(
2973
3224
  CadCameraControls,
2974
3225
  {
2975
3226
  controlsRef,
@@ -2978,9 +3229,11 @@ var ViewerScene = ({
2978
3229
  zoomTo
2979
3230
  }
2980
3231
  ),
2981
- showOrbitTarget ? /* @__PURE__ */ jsx3(TargetMarker, { controlsRef }) : null
3232
+ showOrbitTarget ? /* @__PURE__ */ jsx4(TargetMarker, { controlsRef }) : null
2982
3233
  ] }) });
2983
3234
  };
3235
+ var subscribeNever = () => () => {
3236
+ };
2984
3237
  var Viewer = forwardRef(function Viewer2({
2985
3238
  children,
2986
3239
  className,
@@ -2993,8 +3246,10 @@ var Viewer = forwardRef(function Viewer2({
2993
3246
  retargetOnDoubleClick = true,
2994
3247
  showOrbitTarget = false,
2995
3248
  theme,
2996
- onPointerMissed
3249
+ onPointerMissed,
3250
+ webglUnavailable
2997
3251
  }, ref) {
3252
+ const webglAvailable = useSyncExternalStore(subscribeNever, webGLAvailableSnapshot, () => true);
2998
3253
  const actionsRef = useRef3(null);
2999
3254
  const resolved = useMemo4(() => resolveTheme(theme), [theme]);
3000
3255
  const sectionStore = useMemo4(() => createSectionStore(), []);
@@ -3020,7 +3275,7 @@ var Viewer = forwardRef(function Viewer2({
3020
3275
  tracker.current?.dispose();
3021
3276
  tracker.current = element ? trackTaps(element) : null;
3022
3277
  }, []);
3023
- return /* @__PURE__ */ jsx3(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx3(SectionStoreContext.Provider, { value: sectionStore, children: /* @__PURE__ */ jsx3(
3278
+ return /* @__PURE__ */ jsx4(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx4(SectionStoreContext.Provider, { value: sectionStore, children: /* @__PURE__ */ jsx4(
3024
3279
  "div",
3025
3280
  {
3026
3281
  className: ["viewer-root", className].filter(Boolean).join(" "),
@@ -3030,7 +3285,7 @@ var Viewer = forwardRef(function Viewer2({
3030
3285
  if (event.target instanceof HTMLCanvasElement) event.preventDefault();
3031
3286
  },
3032
3287
  style: { height: "100%", width: "100%", ...style },
3033
- children: /* @__PURE__ */ jsx3(
3288
+ children: webglAvailable ? /* @__PURE__ */ jsx4(
3034
3289
  Canvas,
3035
3290
  {
3036
3291
  className: "viewer-canvas-container",
@@ -3051,7 +3306,7 @@ var Viewer = forwardRef(function Viewer2({
3051
3306
  if (event.button !== 0) return;
3052
3307
  if (tracker.current?.isTap(event) ?? true) onPointerMissed?.();
3053
3308
  },
3054
- children: /* @__PURE__ */ jsx3(
3309
+ children: /* @__PURE__ */ jsx4(
3055
3310
  ViewerScene,
3056
3311
  {
3057
3312
  setControls,
@@ -3068,7 +3323,7 @@ var Viewer = forwardRef(function Viewer2({
3068
3323
  )
3069
3324
  },
3070
3325
  projection
3071
- )
3326
+ ) : /* @__PURE__ */ jsx4(WebGLUnavailable, { children: webglUnavailable })
3072
3327
  }
3073
3328
  ) }) });
3074
3329
  });
@@ -3087,7 +3342,7 @@ import {
3087
3342
  Raycaster as Raycaster2,
3088
3343
  Scene,
3089
3344
  Vector2 as Vector26,
3090
- Vector3 as Vector311
3345
+ Vector3 as Vector312
3091
3346
  } from "three";
3092
3347
 
3093
3348
  // src/arrow-mesh.tsx
@@ -3178,7 +3433,7 @@ function createDepthResetMaterial() {
3178
3433
  }
3179
3434
 
3180
3435
  // src/arrow-mesh.tsx
3181
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3436
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3182
3437
  var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
3183
3438
  const invalidate = useThree5((state) => state.invalidate);
3184
3439
  const material = useMemo5(() => createArrowMaterial(), []);
@@ -3191,16 +3446,16 @@ var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
3191
3446
  applyArrowColor(material, color, opacity);
3192
3447
  invalidate();
3193
3448
  }, [color, invalidate, material, opacity]);
3194
- return /* @__PURE__ */ jsxs3(Fragment, { children: [
3195
- reset ? /* @__PURE__ */ jsx4("mesh", { geometry, material: reset, renderOrder: 0, raycast: () => null }) : null,
3196
- /* @__PURE__ */ jsx4("mesh", { geometry, material: depth, renderOrder: 1, raycast: () => null }),
3197
- /* @__PURE__ */ jsx4("mesh", { geometry, material, renderOrder: 2, ...events })
3449
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
3450
+ reset ? /* @__PURE__ */ jsx5("mesh", { geometry, material: reset, renderOrder: 0, raycast: () => null }) : null,
3451
+ /* @__PURE__ */ jsx5("mesh", { geometry, material: depth, renderOrder: 1, raycast: () => null }),
3452
+ /* @__PURE__ */ jsx5("mesh", { geometry, material, renderOrder: 2, ...events })
3198
3453
  ] });
3199
3454
  };
3200
3455
 
3201
3456
  // src/render/arrow.ts
3202
- import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector310 } from "three";
3203
- var ARROW_AXIS2 = new Vector310(0, 1, 0);
3457
+ import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector311 } from "three";
3458
+ var ARROW_AXIS2 = new Vector311(0, 1, 0);
3204
3459
  var ARROW_ROUND = 32;
3205
3460
  var HEAD_LENGTH = 0.415;
3206
3461
  var HEAD_RADIUS = 0.153;
@@ -3426,12 +3681,12 @@ function createMaskMaterial() {
3426
3681
  }
3427
3682
 
3428
3683
  // src/section-view.tsx
3429
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3684
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3430
3685
  var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
3431
3686
  function resolveSectionPlane(options, box) {
3432
3687
  if (!options?.enabled || box.isEmpty()) return null;
3433
3688
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_SECTION_NORMAL;
3434
- const axis = new Vector311(normal.x, normal.y, normal.z);
3689
+ const axis = new Vector312(normal.x, normal.y, normal.z);
3435
3690
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
3436
3691
  axis.normalize();
3437
3692
  const bounds = sectionBounds(box, normal);
@@ -3452,7 +3707,7 @@ function resolveSectionPlane(options, box) {
3452
3707
  }
3453
3708
  };
3454
3709
  }
3455
- var Z = new Vector311(0, 0, 1);
3710
+ var Z = new Vector312(0, 0, 1);
3456
3711
  var SectionView = ({
3457
3712
  geometry,
3458
3713
  box,
@@ -3472,7 +3727,7 @@ var SectionView = ({
3472
3727
  const handleRef = useRef4(null);
3473
3728
  const [hovered, setHovered] = useState(false);
3474
3729
  const dragging = useRef4(null);
3475
- const span = useMemo6(() => box.getSize(new Vector311()).length(), [box]);
3730
+ const span = useMemo6(() => box.getSize(new Vector312()).length(), [box]);
3476
3731
  const frameSize = span * SECTION_GIZMO_FRAME_SCALE;
3477
3732
  const directionColor2 = useMemo6(() => sectionDirectionColor(plane.normal), [plane]);
3478
3733
  const resolvedHandleColor = sectionHandleColor(handleColor, directionColor2);
@@ -3556,7 +3811,7 @@ var SectionView = ({
3556
3811
  event.stopPropagation();
3557
3812
  const view = camera.position.clone().sub(capPosition).normalize();
3558
3813
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
3559
- const hit = event.ray.intersectPlane(surface, new Vector311());
3814
+ const hit = event.ray.intersectPlane(surface, new Vector312());
3560
3815
  const start = {
3561
3816
  plane: surface,
3562
3817
  from: hit ? hit.dot(plane.normal) : 0,
@@ -3573,7 +3828,7 @@ var SectionView = ({
3573
3828
  -((native.clientY - rect.top) / rect.height) * 2 + 1
3574
3829
  );
3575
3830
  raycaster.setFromCamera(pointer, camera);
3576
- const point = raycaster.ray.intersectPlane(start.plane, new Vector311());
3831
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector312());
3577
3832
  if (!point) return;
3578
3833
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
3579
3834
  invalidate();
@@ -3605,8 +3860,8 @@ var SectionView = ({
3605
3860
  // Excluded from framing, all of it. The cap is a quad half again as wide as
3606
3861
  // the part's diagonal, so a Fit that measured it would frame the cut rather
3607
3862
  // than the part and leave the part a speck in the middle of it.
3608
- /* @__PURE__ */ jsxs4("group", { userData: FURNITURE2, children: [
3609
- /* @__PURE__ */ jsx5(
3863
+ /* @__PURE__ */ jsxs5("group", { userData: FURNITURE2, children: [
3864
+ /* @__PURE__ */ jsx6(
3610
3865
  "mesh",
3611
3866
  {
3612
3867
  geometry,
@@ -3615,7 +3870,7 @@ var SectionView = ({
3615
3870
  raycast: () => null
3616
3871
  }
3617
3872
  ),
3618
- /* @__PURE__ */ jsx5(
3873
+ /* @__PURE__ */ jsx6(
3619
3874
  "mesh",
3620
3875
  {
3621
3876
  geometry,
@@ -3624,7 +3879,7 @@ var SectionView = ({
3624
3879
  raycast: () => null
3625
3880
  }
3626
3881
  ),
3627
- /* @__PURE__ */ jsx5(
3882
+ /* @__PURE__ */ jsx6(
3628
3883
  "mesh",
3629
3884
  {
3630
3885
  geometry: quad,
@@ -3635,15 +3890,15 @@ var SectionView = ({
3635
3890
  raycast: () => null
3636
3891
  }
3637
3892
  ),
3638
- showHandle && onDrag ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
3639
- /* @__PURE__ */ jsxs4(
3893
+ showHandle && onDrag ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
3894
+ /* @__PURE__ */ jsxs5(
3640
3895
  "group",
3641
3896
  {
3642
3897
  position: capPosition,
3643
3898
  quaternion: capQuaternion,
3644
3899
  renderOrder: SECTION_RENDER_ORDER.outline,
3645
3900
  children: [
3646
- /* @__PURE__ */ jsx5("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx5(
3901
+ /* @__PURE__ */ jsx6("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx6(
3647
3902
  "meshBasicMaterial",
3648
3903
  {
3649
3904
  color: directionColor2,
@@ -3654,18 +3909,18 @@ var SectionView = ({
3654
3909
  depthWrite: false
3655
3910
  }
3656
3911
  ) }),
3657
- /* @__PURE__ */ jsx5("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx5("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
3912
+ /* @__PURE__ */ jsx6("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx6("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
3658
3913
  ]
3659
3914
  }
3660
3915
  ),
3661
- /* @__PURE__ */ jsx5(
3916
+ /* @__PURE__ */ jsx6(
3662
3917
  "group",
3663
3918
  {
3664
3919
  ref: handleRef,
3665
3920
  position: capPosition,
3666
3921
  quaternion: handleQuaternion,
3667
3922
  renderOrder: SECTION_RENDER_ORDER.handle,
3668
- children: /* @__PURE__ */ jsx5(
3923
+ children: /* @__PURE__ */ jsx6(
3669
3924
  ArrowMesh,
3670
3925
  {
3671
3926
  geometry: handleShape,
@@ -3702,7 +3957,7 @@ function useContentBox() {
3702
3957
  }
3703
3958
 
3704
3959
  // src/part-mesh.tsx
3705
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3960
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3706
3961
  var PartMesh = ({
3707
3962
  model,
3708
3963
  geometry,
@@ -3724,6 +3979,7 @@ var PartMesh = ({
3724
3979
  theme,
3725
3980
  showEdges = true,
3726
3981
  sharpCorners = null,
3982
+ threads = null,
3727
3983
  display = "solid",
3728
3984
  children
3729
3985
  }) => {
@@ -3740,11 +3996,11 @@ var PartMesh = ({
3740
3996
  onHoverRef.current = onHover;
3741
3997
  const box = useContentBox();
3742
3998
  const store = useSectionStore();
3743
- const shared = useSyncExternalStore(store.subscribe, store.get, store.get);
3999
+ const shared = useSyncExternalStore2(store.subscribe, store.get, store.get);
3744
4000
  const controlled = section !== void 0;
3745
4001
  const options = controlled ? section : shared ?? void 0;
3746
4002
  const cut = useMemo7(() => resolveSectionPlane(options, box), [box, options]);
3747
- const engaged = useSyncExternalStore(store.subscribe, store.isEngaged, store.isEngaged);
4003
+ const engaged = useSyncExternalStore2(store.subscribe, store.isEngaged, store.isEngaged);
3748
4004
  const pressedWhileEngaged = useRef6(false);
3749
4005
  const layers = useRef6({
3750
4006
  selection,
@@ -3825,6 +4081,13 @@ var PartMesh = ({
3825
4081
  if (hoverCorner.current !== null) dropHover();
3826
4082
  invalidate();
3827
4083
  }, [dropHover, invalidate, part, sharpKey]);
4084
+ const threadsRef = useRef6(threads);
4085
+ threadsRef.current = threads;
4086
+ const threadsKey = threadKey(threads);
4087
+ useLayoutEffect3(() => {
4088
+ part.setThreads(threadsRef.current);
4089
+ invalidate();
4090
+ }, [invalidate, part, threadsKey]);
3828
4091
  const reportedSection = useRef6("");
3829
4092
  useLayoutEffect3(() => {
3830
4093
  const state = cut?.state ?? null;
@@ -3914,8 +4177,8 @@ var PartMesh = ({
3914
4177
  },
3915
4178
  [box, controlled, cut, onSectionChange, options, store]
3916
4179
  );
3917
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
3918
- /* @__PURE__ */ jsx6(
4180
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
4181
+ /* @__PURE__ */ jsx7(
3919
4182
  "primitive",
3920
4183
  {
3921
4184
  object: part.object,
@@ -3952,7 +4215,7 @@ var PartMesh = ({
3952
4215
  }
3953
4216
  }
3954
4217
  ),
3955
- cut ? /* @__PURE__ */ jsx6(
4218
+ cut ? /* @__PURE__ */ jsx7(
3956
4219
  SectionView,
3957
4220
  {
3958
4221
  geometry,
@@ -3964,12 +4227,12 @@ var PartMesh = ({
3964
4227
  onDrag: !controlled || onSectionChange ? dragSection : void 0
3965
4228
  }
3966
4229
  ) : null,
3967
- children ? /* @__PURE__ */ jsx6(PartContext.Provider, { value: context, children }) : null
4230
+ children ? /* @__PURE__ */ jsx7(PartContext.Provider, { value: context, children }) : null
3968
4231
  ] });
3969
4232
  };
3970
- var ORIGIN = new Vector312();
3971
- var UP = new Vector312(0, 0, 1);
3972
- var TARGET = new Vector312();
4233
+ var ORIGIN = new Vector313();
4234
+ var UP = new Vector313(0, 0, 1);
4235
+ var TARGET = new Vector313();
3973
4236
  function readTarget(controls) {
3974
4237
  if (controls && typeof controls.getTarget === "function") {
3975
4238
  return controls.getTarget(TARGET);
@@ -4618,7 +4881,7 @@ function compareVersions(a, b) {
4618
4881
  }
4619
4882
 
4620
4883
  // src/engine/engine-part.tsx
4621
- import { jsx as jsx7 } from "react/jsx-runtime";
4884
+ import { jsx as jsx8 } from "react/jsx-runtime";
4622
4885
  var models = /* @__PURE__ */ new WeakMap();
4623
4886
  var modelForReport = (report) => {
4624
4887
  if (typeof report !== "object" || report === null) {
@@ -4639,7 +4902,7 @@ var EnginePart = ({ report, ...props }) => {
4639
4902
  engineGeometryCache.retain(resource);
4640
4903
  return () => engineGeometryCache.release(resource);
4641
4904
  }, [resource]);
4642
- return /* @__PURE__ */ jsx7(PartMesh, { model, geometry: resource.geometry, ...props });
4905
+ return /* @__PURE__ */ jsx8(PartMesh, { model, geometry: resource.geometry, ...props });
4643
4906
  };
4644
4907
  function useGeometryResource(part) {
4645
4908
  const resource = engineGeometryCache.get(part);
@@ -4648,6 +4911,23 @@ function useGeometryResource(part) {
4648
4911
  return resource;
4649
4912
  }
4650
4913
 
4914
+ // src/engine/threads.ts
4915
+ function threadMark(feature, datasheet) {
4916
+ if (!isRecord(datasheet) || !isRecord(datasheet["facts"])) return null;
4917
+ const facts = datasheet["facts"];
4918
+ if (facts["kind"] !== "Hole" || !isRecord(facts["threading"])) return null;
4919
+ const spec = facts["threading"]["spec"];
4920
+ if (!isRecord(spec)) return null;
4921
+ const pitch = spec["threadPitch"];
4922
+ const majorDiameter = spec["basicDiameter"];
4923
+ if (!isFiniteNumber(pitch) || pitch <= 0 || !isFiniteNumber(majorDiameter)) return null;
4924
+ return {
4925
+ regions: feature.regionIdxs,
4926
+ pitch: spec["handedness"] === "Left" ? -pitch : pitch,
4927
+ majorDiameter
4928
+ };
4929
+ }
4930
+
4651
4931
  export {
4652
4932
  usePartContext,
4653
4933
  sameDirection,
@@ -4822,5 +5102,6 @@ export {
4822
5102
  MIN_KERNEL_VERSION,
4823
5103
  normalizePartReport,
4824
5104
  assertSupportedKernelVersion,
4825
- EnginePart
5105
+ EnginePart,
5106
+ threadMark
4826
5107
  };
@@ -1,5 +1,5 @@
1
- import { P as PartModel, a as PartMeshRefs } from '../normalize-C4Wj4IGK.js';
2
- export { E as EnginePart, b as EnginePartProps, M as MIN_KERNEL_VERSION, c as assertSupportedKernelVersion, n as normalizePartReport, s as smoothRegionNormals } from '../normalize-C4Wj4IGK.js';
1
+ import { P as PartModel, a as PartMeshRefs, b as PartModelFeature, T as ThreadMark } from '../normalize-BBADLCmd.js';
2
+ export { E as EnginePart, c as EnginePartProps, M as MIN_KERNEL_VERSION, d as assertSupportedKernelVersion, n as normalizePartReport, s as smoothRegionNormals } from '../normalize-BBADLCmd.js';
3
3
  import { BufferGeometry } from 'three';
4
4
  import 'react';
5
5
 
@@ -115,4 +115,15 @@ declare function loadPartMesh(mesh: PartMeshRefs, options?: LoadPartGeometryOpti
115
115
  */
116
116
  declare function parsePartGeometry(data: ArrayBuffer, mesh: PartMeshExpectation, format?: MeshFormat): Promise<BufferGeometry>;
117
117
 
118
- export { type CacheablePart, type EngineGeometryCache, type EngineGeometryResource, type FetchLike, type LoadPartGeometryOptions, type MeshAsset, type MeshFormat, PartMeshError, type PartMeshExpectation, createEngineGeometryCache, engineGeometryCache, engineGeometryResourceKey, loadPartGeometry, loadPartMesh, parsePartGeometry, partMeshAssets };
118
+ /**
119
+ * The thread to draw in a hole, read from its Engine datasheet: a hole's
120
+ * `facts.threading.spec`, which the Engine fills for a modeled thread it
121
+ * took for a catalog thread (ISO 6H, ANSI 2B, straight pipe). `null` for a
122
+ * plain hole, any other feature, and a datasheet of an unexpected shape.
123
+ *
124
+ * Only what the Engine sends is drawn: a thread it did not report is not
125
+ * guessed at from the mesh.
126
+ */
127
+ declare function threadMark(feature: PartModelFeature, datasheet: unknown): ThreadMark | null;
128
+
129
+ export { type CacheablePart, type EngineGeometryCache, type EngineGeometryResource, type FetchLike, type LoadPartGeometryOptions, type MeshAsset, type MeshFormat, PartMeshError, type PartMeshExpectation, createEngineGeometryCache, engineGeometryCache, engineGeometryResourceKey, loadPartGeometry, loadPartMesh, parsePartGeometry, partMeshAssets, threadMark };
@@ -11,8 +11,9 @@ import {
11
11
  normalizePartReport,
12
12
  parsePartGeometry,
13
13
  partMeshAssets,
14
- smoothRegionNormals
15
- } from "../chunk-M2B3CQPQ.js";
14
+ smoothRegionNormals,
15
+ threadMark
16
+ } from "../chunk-LBG6GVGE.js";
16
17
  export {
17
18
  EnginePart,
18
19
  MIN_KERNEL_VERSION,
@@ -26,5 +27,6 @@ export {
26
27
  normalizePartReport,
27
28
  parsePartGeometry,
28
29
  partMeshAssets,
29
- smoothRegionNormals
30
+ smoothRegionNormals,
31
+ threadMark
30
32
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PartModel, F as FeatureTag, d as PartPick, V as Vec3, R as RegionHighlight, e as ViewerTheme, S as SectionOptions, f as SectionState, g as ViewerCamera, h as ViewportSize, i as PartModelRegion, C as CameraLimits, j as ViewerView, k as Projection, T as TriangleRange, l as RegionIndex, m as PartModelFeature, o as FeatureType } from './normalize-C4Wj4IGK.js';
2
- export { A as AXES_PLANE_OFFSET, p as AXES_PLANE_SCALE, q as AxesPlane, B as BuildPickInput, r as CAD_CAMERA_UP, t as CANDIDATE_WEIGHT, u as CAP_HATCH, D as DEFAULT_FIT_MARGIN, v as DEFAULT_SECTION_NORMAL, w as DEFAULT_THEME, x as DIRECTION_COLORS, y as DISABLED_SECTION, z as EXCLUDE_FROM_FRAME, E as EnginePart, G as FeatureHighlight, H as FocusOptions, I as HANDLE_PIXELS, J as HIGHLIGHT_COLORS, K as HIGHLIGHT_WEIGHT, L as HOVER_WEIGHT, N as HighlightLayers, O as KnownFeatureType, Q as KnownShapeKind, U as MAX_FRAME_RATIO, W as MIN_FRAME_RATIO, X as NO_MODIFIERS, Y as OUTLINE_SCALE, Z as PERSPECTIVE_FOV, _ as PICKED_SURFACE_LABEL, $ as PREVIEW_SCALE, a0 as PartDisplay, a1 as PartMesh, a2 as PartMeshProps, a as PartMeshRefs, a3 as PartObject, a4 as PickModifiers, a5 as PointerLocation, a6 as REGION_ATTRIBUTE, a7 as RegionPaint, a8 as SECTION_GIZMO_FRAME_SCALE, a9 as SECTION_RENDER_ORDER, aa as SceneBounds, ab as SectionAnchor, ac as SectionBounds, ad as SectionPlacement, ae as ShapeKind, af as SharpCorner, ag as SharpCornerOptions, ah as SurfaceCreases, ai as SurfaceHit, aj as adaptedUp, ak as applyHighlightLayers, al as applyProjection, am as aspectRatio, an as axesPlanes, ao as boundsFromBox, ap as buildPick, aq as buildRegionAttribute, ar as buildRegionTexels, as as cadViewDirections, at as cameraLimits, au as contentBounds, av as createPart, aw as currentViewDirection, ax as defaultBounds, ay as directionColor, az as dragPlane, aA as excludedFromFrame, aB as findSharpCorners, aC as fitDistance, aD as focusForPick, aE as hitUnderRay, n as normalizePartReport, aF as orthographicHalfHeight, aG as perspectiveFitDistance, aH as pickedStartDepth, aI as resolveTheme, aJ as screenLength, aK as sectionAnchor, aL as sectionBounds, aM as sectionConstant, aN as sectionDepth, aO as sectionDepthConstant, aP as sectionDepthRange, aQ as sectionFromPick, aR as sectionMeasurement, aS as sectionOffset, aT as sectionOptionsFromState, aU as sectionPlane, aV as sharpCornerPositions, s as smoothRegionNormals, aW as startPosition, aX as surfaceCreases, aY as surfaceUnderRay, aZ as targetBoundary, a_ as themesEqual, a$ as viewDirection } from './normalize-C4Wj4IGK.js';
1
+ import { P as PartModel, F as FeatureTag, e as PartPick, V as Vec3, R as RegionHighlight, f as ViewerTheme, S as SectionOptions, g as SectionState, h as ViewerCamera, i as ViewportSize, j as PartModelRegion, C as CameraLimits, k as ViewerView, l as Projection, m as TriangleRange, o as RegionIndex, b as PartModelFeature, p as FeatureType } from './normalize-BBADLCmd.js';
2
+ export { A as AXES_PLANE_OFFSET, q as AXES_PLANE_SCALE, r as AxesPlane, B as BuildPickInput, t as CAD_CAMERA_UP, u as CANDIDATE_WEIGHT, v as CAP_HATCH, D as DEFAULT_FIT_MARGIN, w as DEFAULT_SECTION_NORMAL, x as DEFAULT_THEME, y as DIRECTION_COLORS, z as DISABLED_SECTION, G as EXCLUDE_FROM_FRAME, E as EnginePart, H as FeatureHighlight, I as FocusOptions, J as HANDLE_PIXELS, K as HIGHLIGHT_COLORS, L as HIGHLIGHT_WEIGHT, N as HOVER_WEIGHT, O as HighlightLayers, Q as KnownFeatureType, U as KnownShapeKind, W as MAX_FRAME_RATIO, X as MIN_FRAME_RATIO, Y as NO_MODIFIERS, Z as OUTLINE_SCALE, _ as PERSPECTIVE_FOV, $ as PICKED_SURFACE_LABEL, a0 as PREVIEW_SCALE, a1 as PartDisplay, a2 as PartMesh, a3 as PartMeshProps, a as PartMeshRefs, a4 as PartObject, a5 as PickModifiers, a6 as PointerLocation, a7 as REGION_ATTRIBUTE, a8 as RegionPaint, a9 as SECTION_GIZMO_FRAME_SCALE, aa as SECTION_RENDER_ORDER, ab as SceneBounds, ac as SectionAnchor, ad as SectionBounds, ae as SectionPlacement, af as ShapeKind, ag as SharpCorner, ah as SharpCornerOptions, ai as SurfaceCreases, aj as SurfaceHit, T as ThreadMark, ak as ThreadOptions, al as adaptedUp, am as applyHighlightLayers, an as applyProjection, ao as aspectRatio, ap as axesPlanes, aq as boundsFromBox, ar as buildPick, as as buildRegionAttribute, at as buildRegionTexels, au as cadViewDirections, av as cameraLimits, aw as contentBounds, ax as createPart, ay as currentViewDirection, az as defaultBounds, aA as directionColor, aB as dragPlane, aC as excludedFromFrame, aD as findSharpCorners, aE as fitDistance, aF as focusForPick, aG as hitUnderRay, n as normalizePartReport, aH as orthographicHalfHeight, aI as perspectiveFitDistance, aJ as pickedStartDepth, aK as resolveTheme, aL as screenLength, aM as sectionAnchor, aN as sectionBounds, aO as sectionConstant, aP as sectionDepth, aQ as sectionDepthConstant, aR as sectionDepthRange, aS as sectionFromPick, aT as sectionMeasurement, aU as sectionOffset, aV as sectionOptionsFromState, aW as sectionPlane, aX as sharpCornerPositions, s as smoothRegionNormals, aY as startPosition, aZ as surfaceCreases, a_ as surfaceUnderRay, a$ as targetBoundary, b0 as themesEqual, b1 as viewDirection } from './normalize-BBADLCmd.js';
3
3
  import { BufferGeometry, Box3, CanvasTexture, Plane, Vector3, Matrix4, Vector2, Ray, ShaderMaterial, Texture, Color, MeshBasicMaterial, WebGLRenderTarget } from 'three';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, PropsWithChildren, RefObject, CSSProperties } from 'react';
@@ -93,6 +93,8 @@ interface ViewerToolbarControls {
93
93
  dfm?: ViewerToolbarControl;
94
94
  /** The DFM rules' sharp corners, drawn on the part: see `sharpCorners` on `<PartMesh>`. */
95
95
  sharpCorners?: ViewerToolbarControl;
96
+ /** Threads drawn in their holes: see `threads` on `<PartMesh>`. */
97
+ threads?: ViewerToolbarControl;
96
98
  hover?: ViewerToolbarControl;
97
99
  focus?: ViewerToolbarControl;
98
100
  wireframe?: ViewerToolbarControl;
@@ -136,6 +138,7 @@ declare const ViewerToolbar: (({ className, children }: ViewerToolbarProps) => r
136
138
  DirectionsButton: () => react.JSX.Element;
137
139
  DfmButton: () => react.JSX.Element;
138
140
  SharpCornersButton: () => react.JSX.Element;
141
+ ThreadsButton: () => react.JSX.Element;
139
142
  HoverButton: () => react.JSX.Element;
140
143
  FocusButton: () => react.JSX.Element;
141
144
  WireframeButton: () => react.JSX.Element;
@@ -1501,6 +1504,13 @@ interface ViewerProps {
1501
1504
  * section handle. The usual meaning is "put the selection down".
1502
1505
  */
1503
1506
  onPointerMissed?: () => void;
1507
+ /**
1508
+ * What to show in place of the canvas when the browser will not create a
1509
+ * WebGL context — hardware acceleration off, or the GPU blocklisted. It goes
1510
+ * inside a `role="alert"` element (`viewer-webgl-unavailable`), and replaces
1511
+ * the default message asking the user to turn acceleration on.
1512
+ */
1513
+ webglUnavailable?: ReactNode;
1504
1514
  }
1505
1515
  declare const Viewer: react.ForwardRefExoticComponent<ViewerProps & react.RefAttributes<ViewerHandle>>;
1506
1516
 
package/dist/index.js CHANGED
@@ -163,7 +163,7 @@ import {
163
163
  viewUp,
164
164
  viewVector,
165
165
  visualSurfaces
166
- } from "./chunk-M2B3CQPQ.js";
166
+ } from "./chunk-LBG6GVGE.js";
167
167
 
168
168
  // src/render/feature-triangles.ts
169
169
  var EMPTY = { positions: new Float32Array(), normals: new Float32Array() };
@@ -274,6 +274,7 @@ var SharpCornersIcon = () => /* @__PURE__ */ jsxs(SvgIcon, { children: [
274
274
  /* @__PURE__ */ jsx2("path", { d: "M4 3v17h17" }),
275
275
  /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "15", r: "4" })
276
276
  ] });
277
+ var ThreadsIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M8 3h8v18H8zM8 8l8-2M8 12l8-2M8 16l8-2M8 20l8-2" }) });
277
278
  var HoverIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v12H9l-5 4zm4 5h8m-8 3h5" }) });
278
279
  var FocusIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v16H4zM8 8h8v8H8zM2 12h4m12 0h4M12 2v4m0 12v4" }) });
279
280
  var WireframeIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 7l8-4 8 4v10l-8 4-8-4zM4 7l8 4 8-4M12 11v10M4 17l8-4 8 4M12 3v10" }) });
@@ -291,6 +292,7 @@ var ICONS = {
291
292
  directions: DirectionsIcon,
292
293
  dfm: DfmIcon,
293
294
  sharpCorners: SharpCornersIcon,
295
+ threads: ThreadsIcon,
294
296
  hover: HoverIcon,
295
297
  focus: FocusIcon,
296
298
  wireframe: WireframeIcon,
@@ -401,6 +403,14 @@ var SharpCornersButton = () => /* @__PURE__ */ jsx3(
401
403
  label: (pressed) => pressed ? "Hide sharp corners" : "Show sharp corners"
402
404
  }
403
405
  );
406
+ var ThreadsButton = () => /* @__PURE__ */ jsx3(
407
+ ToolbarButton,
408
+ {
409
+ action: "threads",
410
+ icon: "threads",
411
+ label: (pressed) => pressed ? "Hide threads" : "Show threads"
412
+ }
413
+ );
404
414
  var HoverButton = () => /* @__PURE__ */ jsx3(
405
415
  ToolbarButton,
406
416
  {
@@ -461,6 +471,7 @@ var DefaultControls = () => {
461
471
  controls.directions ? /* @__PURE__ */ jsx3(DirectionsButton, {}, "directions") : null,
462
472
  controls.dfm ? /* @__PURE__ */ jsx3(DfmButton, {}, "dfm") : null,
463
473
  controls.sharpCorners ? /* @__PURE__ */ jsx3(SharpCornersButton, {}, "sharpCorners") : null,
474
+ controls.threads ? /* @__PURE__ */ jsx3(ThreadsButton, {}, "threads") : null,
464
475
  controls.hover ? /* @__PURE__ */ jsx3(HoverButton, {}, "hover") : null,
465
476
  controls.focus ? /* @__PURE__ */ jsx3(FocusButton, {}, "focus") : null,
466
477
  controls.wireframe ? /* @__PURE__ */ jsx3(WireframeButton, {}, "wireframe") : null,
@@ -502,6 +513,7 @@ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
502
513
  DirectionsButton,
503
514
  DfmButton,
504
515
  SharpCornersButton,
516
+ ThreadsButton,
505
517
  HoverButton,
506
518
  FocusButton,
507
519
  WireframeButton,
@@ -546,6 +546,39 @@ declare function findSharpCorners(creases: SurfaceCreases, model: Pick<PartModel
546
546
  /** The flagged features' corners, as `x y z x y z` endpoint pairs. See {@link findSharpCorners}. */
547
547
  declare function sharpCornerPositions(creases: SurfaceCreases, model: Pick<PartModel, 'features' | 'regionIndex'>, options: SharpCornerOptions): Float32Array;
548
548
 
549
+ /**
550
+ * Threads drawn into the bores they are cut in.
551
+ *
552
+ * The kernel cuts a modeled internal thread out of the body before it meshes
553
+ * it, so a tapped hole arrives as a plain bore. A displacement map cannot put
554
+ * the thread back: it moves vertices, and wants UVs and a dense mesh, where
555
+ * the Engine's has neither — a small bore is a handful of 30° facets. So the
556
+ * thread is drawn the way a bump map draws: the part's shader tilts each
557
+ * pixel's normal along the bore's axis as a helix of the thread's pitch runs
558
+ * past it, and the light falls as it would on the flanks of a real thread.
559
+ * The silhouette stays round, which inside a bore nobody sees.
560
+ *
561
+ * What the shader needs per region is the bore's axis, a point on it, and the
562
+ * pitch. The pitch comes from the caller; the axis and the point are fitted
563
+ * here from the region's own triangles, so the drawing sits on the mesh
564
+ * exactly and needs nothing from the report it does not already carry.
565
+ */
566
+ /** One thread to draw, on the faces of the hole it is cut in. */
567
+ interface ThreadMark {
568
+ /** The regions of the threaded hole, as the report numbers them. */
569
+ readonly regions: readonly number[];
570
+ /** From one crest to the next, in mm. Negative for a left-hand thread. */
571
+ readonly pitch: number;
572
+ /**
573
+ * The thread's major diameter, in mm. A cylinder of the hole wider than
574
+ * this — a counterbore — is not threaded, and is left plain.
575
+ */
576
+ readonly majorDiameter: number;
577
+ }
578
+ interface ThreadOptions {
579
+ readonly threads: readonly ThreadMark[];
580
+ }
581
+
549
582
  /** The vertex attribute carrying each vertex's column in the state texture. */
550
583
  declare const REGION_ATTRIBUTE = "aRegion";
551
584
  type PartDisplay = 'solid' | 'wireframe';
@@ -610,6 +643,11 @@ interface PartObject {
610
643
  }, camera: ViewerCamera, viewport: ViewportSize): SharpCorner | null;
611
644
  /** Draws one corner up as the one under the pointer; `null` for none. */
612
645
  setHoveredSharpCorner(index: number | null): void;
646
+ /**
647
+ * Draws threads into the bores they are cut in — see `render/threads.ts`.
648
+ * `null` draws none. Keyed by content, like `setSharpCorners`.
649
+ */
650
+ setThreads(options: ThreadOptions | null): void;
613
651
  dispose(): void;
614
652
  }
615
653
  interface RegionPaint {
@@ -1180,6 +1218,13 @@ interface PartMeshProps {
1180
1218
  * Coloured by the theme's `sharpCorner`.
1181
1219
  */
1182
1220
  sharpCorners?: SharpCornerOptions | null;
1221
+ /**
1222
+ * Draws threads into the bores they are cut in: each mark's pitch, as a
1223
+ * helix of light and shade over the hole's cylinders. The Engine cuts a
1224
+ * modeled internal thread out of the mesh, so without this a tapped hole is
1225
+ * a plain bore. Off when omitted or `null`. See `threadMark` in `/engine`.
1226
+ */
1227
+ threads?: ThreadOptions | null;
1183
1228
  /** Wireframe shows region boundaries and any hovered/painted faces, with no triangle diagonals. */
1184
1229
  display?: PartDisplay;
1185
1230
  /**
@@ -1198,7 +1243,7 @@ interface PartMeshProps {
1198
1243
  * React render at all: the hovered face lives in a ref, and routing it through
1199
1244
  * state is what this rewrite exists to stop doing.
1200
1245
  */
1201
- declare const PartMesh: ({ model, geometry, selection, focus, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, hover, activeDirection, section, onSectionChange, onAdjacency, focusFeature, onHover, onPick, theme, showEdges, sharpCorners, display, children, }: PartMeshProps) => react.JSX.Element;
1246
+ declare const PartMesh: ({ model, geometry, selection, focus, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, hover, activeDirection, section, onSectionChange, onAdjacency, focusFeature, onHover, onPick, theme, showEdges, sharpCorners, threads, display, children, }: PartMeshProps) => react.JSX.Element;
1202
1247
 
1203
1248
  interface EnginePartProps extends Omit<PartMeshProps, 'model' | 'geometry'> {
1204
1249
  /**
@@ -1273,4 +1318,4 @@ declare function normalizePartReport(report: unknown): PartModel;
1273
1318
  */
1274
1319
  declare function assertSupportedKernelVersion(kernelVersion: string): void;
1275
1320
 
1276
- export { PREVIEW_SCALE as $, AXES_PLANE_OFFSET as A, type BuildPickInput as B, type CameraLimits as C, DEFAULT_FIT_MARGIN as D, EnginePart as E, type FeatureTag as F, type FeatureHighlight as G, type FocusOptions as H, HANDLE_PIXELS as I, HIGHLIGHT_COLORS as J, HIGHLIGHT_WEIGHT as K, HOVER_WEIGHT as L, MIN_KERNEL_VERSION as M, type HighlightLayers as N, type KnownFeatureType as O, type PartModel as P, type KnownShapeKind as Q, type RegionHighlight as R, type SectionOptions as S, type TriangleRange as T, MAX_FRAME_RATIO as U, type Vec3 as V, MIN_FRAME_RATIO as W, NO_MODIFIERS as X, OUTLINE_SCALE as Y, PERSPECTIVE_FOV as Z, PICKED_SURFACE_LABEL as _, type PartMeshRefs as a, viewDirection as a$, type PartDisplay as a0, PartMesh as a1, type PartMeshProps as a2, type PartObject as a3, type PickModifiers as a4, type PointerLocation as a5, REGION_ATTRIBUTE as a6, type RegionPaint as a7, SECTION_GIZMO_FRAME_SCALE as a8, SECTION_RENDER_ORDER as a9, excludedFromFrame as aA, findSharpCorners as aB, fitDistance as aC, focusForPick as aD, hitUnderRay as aE, orthographicHalfHeight as aF, perspectiveFitDistance as aG, pickedStartDepth as aH, resolveTheme as aI, screenLength as aJ, sectionAnchor as aK, sectionBounds as aL, sectionConstant as aM, sectionDepth as aN, sectionDepthConstant as aO, sectionDepthRange as aP, sectionFromPick as aQ, sectionMeasurement as aR, sectionOffset as aS, sectionOptionsFromState as aT, sectionPlane as aU, sharpCornerPositions as aV, startPosition as aW, surfaceCreases as aX, surfaceUnderRay as aY, targetBoundary as aZ, themesEqual as a_, type SceneBounds as aa, type SectionAnchor as ab, type SectionBounds as ac, type SectionPlacement as ad, type ShapeKind as ae, type SharpCorner as af, type SharpCornerOptions as ag, type SurfaceCreases as ah, type SurfaceHit as ai, adaptedUp as aj, applyHighlightLayers as ak, applyProjection as al, aspectRatio as am, axesPlanes as an, boundsFromBox as ao, buildPick as ap, buildRegionAttribute as aq, buildRegionTexels as ar, cadViewDirections as as, cameraLimits as at, contentBounds as au, createPart as av, currentViewDirection as aw, defaultBounds as ax, directionColor as ay, dragPlane as az, type EnginePartProps as b, assertSupportedKernelVersion as c, type PartPick as d, type ViewerTheme as e, type SectionState as f, type ViewerCamera as g, type ViewportSize as h, type PartModelRegion as i, type ViewerView as j, type Projection as k, type RegionIndex as l, type PartModelFeature as m, normalizePartReport as n, type FeatureType as o, AXES_PLANE_SCALE as p, type AxesPlane as q, CAD_CAMERA_UP as r, smoothRegionNormals as s, CANDIDATE_WEIGHT as t, CAP_HATCH as u, DEFAULT_SECTION_NORMAL as v, DEFAULT_THEME as w, DIRECTION_COLORS as x, DISABLED_SECTION as y, EXCLUDE_FROM_FRAME as z };
1321
+ export { PICKED_SURFACE_LABEL as $, AXES_PLANE_OFFSET as A, type BuildPickInput as B, type CameraLimits as C, DEFAULT_FIT_MARGIN as D, EnginePart as E, type FeatureTag as F, EXCLUDE_FROM_FRAME as G, type FeatureHighlight as H, type FocusOptions as I, HANDLE_PIXELS as J, HIGHLIGHT_COLORS as K, HIGHLIGHT_WEIGHT as L, MIN_KERNEL_VERSION as M, HOVER_WEIGHT as N, type HighlightLayers as O, type PartModel as P, type KnownFeatureType as Q, type RegionHighlight as R, type SectionOptions as S, type ThreadMark as T, type KnownShapeKind as U, type Vec3 as V, MAX_FRAME_RATIO as W, MIN_FRAME_RATIO as X, NO_MODIFIERS as Y, OUTLINE_SCALE as Z, PERSPECTIVE_FOV as _, type PartMeshRefs as a, targetBoundary as a$, PREVIEW_SCALE as a0, type PartDisplay as a1, PartMesh as a2, type PartMeshProps as a3, type PartObject as a4, type PickModifiers as a5, type PointerLocation as a6, REGION_ATTRIBUTE as a7, type RegionPaint as a8, SECTION_GIZMO_FRAME_SCALE as a9, directionColor as aA, dragPlane as aB, excludedFromFrame as aC, findSharpCorners as aD, fitDistance as aE, focusForPick as aF, hitUnderRay as aG, orthographicHalfHeight as aH, perspectiveFitDistance as aI, pickedStartDepth as aJ, resolveTheme as aK, screenLength as aL, sectionAnchor as aM, sectionBounds as aN, sectionConstant as aO, sectionDepth as aP, sectionDepthConstant as aQ, sectionDepthRange as aR, sectionFromPick as aS, sectionMeasurement as aT, sectionOffset as aU, sectionOptionsFromState as aV, sectionPlane as aW, sharpCornerPositions as aX, startPosition as aY, surfaceCreases as aZ, surfaceUnderRay as a_, SECTION_RENDER_ORDER as aa, type SceneBounds as ab, type SectionAnchor as ac, type SectionBounds as ad, type SectionPlacement as ae, type ShapeKind as af, type SharpCorner as ag, type SharpCornerOptions as ah, type SurfaceCreases as ai, type SurfaceHit as aj, type ThreadOptions as ak, adaptedUp as al, applyHighlightLayers as am, applyProjection as an, aspectRatio as ao, axesPlanes as ap, boundsFromBox as aq, buildPick as ar, buildRegionAttribute as as, buildRegionTexels as at, cadViewDirections as au, cameraLimits as av, contentBounds as aw, createPart as ax, currentViewDirection as ay, defaultBounds as az, type PartModelFeature as b, themesEqual as b0, viewDirection as b1, type EnginePartProps as c, assertSupportedKernelVersion as d, type PartPick as e, type ViewerTheme as f, type SectionState as g, type ViewerCamera as h, type ViewportSize as i, type PartModelRegion as j, type ViewerView as k, type Projection as l, type TriangleRange as m, normalizePartReport as n, type RegionIndex as o, type FeatureType as p, AXES_PLANE_SCALE as q, type AxesPlane as r, smoothRegionNormals as s, CAD_CAMERA_UP as t, CANDIDATE_WEIGHT as u, CAP_HATCH as v, DEFAULT_SECTION_NORMAL as w, DEFAULT_THEME as x, DIRECTION_COLORS as y, DISABLED_SECTION as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/viewer",
3
- "version": "2.3.3",
3
+ "version": "2.5.0",
4
4
  "description": "React Three Fiber viewer for Toolpath Engine part reports",
5
5
  "license": "MIT",
6
6
  "engines": {