@toolpath/viewer 2.3.3 → 2.6.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.
@@ -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
  }
@@ -1837,6 +2043,7 @@ var NO_MODIFIERS = {
1837
2043
  shift: false,
1838
2044
  secondary: false
1839
2045
  };
2046
+ var ignoreRay = () => void 0;
1840
2047
  function viewDirection(camera, target) {
1841
2048
  const { x, y, z } = camera.position;
1842
2049
  const dx = x - target.x;
@@ -1883,9 +2090,10 @@ import {
1883
2090
  useEffect as useEffect4,
1884
2091
  useImperativeHandle,
1885
2092
  useMemo as useMemo4,
1886
- useRef as useRef3
2093
+ useRef as useRef3,
2094
+ useSyncExternalStore
1887
2095
  } from "react";
1888
- import { Box3 as Box33, Vector3 as Vector39 } from "three";
2096
+ import { Box3 as Box33, Vector3 as Vector310 } from "three";
1889
2097
 
1890
2098
  // src/camera.tsx
1891
2099
  import { useFrame, useThree as useThree2 } from "@react-three/fiber";
@@ -1902,7 +2110,7 @@ import {
1902
2110
  Sphere,
1903
2111
  Spherical,
1904
2112
  Vector2 as Vector23,
1905
- Vector3 as Vector36,
2113
+ Vector3 as Vector37,
1906
2114
  Vector4 as Vector42
1907
2115
  } from "three";
1908
2116
 
@@ -2000,7 +2208,7 @@ CameraControls.install({
2000
2208
  Sphere,
2001
2209
  Spherical,
2002
2210
  Vector2: Vector23,
2003
- Vector3: Vector36,
2211
+ Vector3: Vector37,
2004
2212
  Vector4: Vector42
2005
2213
  }
2006
2214
  });
@@ -2023,9 +2231,9 @@ var ExtendedCameraControls = class extends CameraControls {
2023
2231
  // Scratch objects — `#onPointerMove` and `#adaptUpVector` run at pointer and
2024
2232
  // frame rate respectively, so neither may allocate.
2025
2233
  #spherical = new Spherical();
2026
- #scratchA = new Vector36();
2027
- #scratchB = new Vector36();
2028
- #scratchC = new Vector36();
2234
+ #scratchA = new Vector37();
2235
+ #scratchB = new Vector37();
2236
+ #scratchC = new Vector37();
2029
2237
  constructor(camera, domElement, options = {}) {
2030
2238
  super(camera, domElement);
2031
2239
  this.#domElement = domElement;
@@ -2363,6 +2571,22 @@ function counted(notify) {
2363
2571
  };
2364
2572
  }
2365
2573
 
2574
+ // src/render/webgl.ts
2575
+ var isWebGLAvailable = () => {
2576
+ if (typeof document === "undefined") {
2577
+ return false;
2578
+ }
2579
+ try {
2580
+ const context = document.createElement("canvas").getContext("webgl2");
2581
+ context?.getExtension("WEBGL_lose_context")?.loseContext();
2582
+ return context !== null;
2583
+ } catch {
2584
+ return false;
2585
+ }
2586
+ };
2587
+ var probed;
2588
+ var webGLAvailableSnapshot = () => probed ??= isWebGLAvailable();
2589
+
2366
2590
  // src/target-marker.tsx
2367
2591
  import { useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
2368
2592
  import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
@@ -2370,7 +2594,7 @@ import {
2370
2594
  DoubleSide as DoubleSide3,
2371
2595
  RingGeometry,
2372
2596
  SphereGeometry,
2373
- Vector3 as Vector37
2597
+ Vector3 as Vector38
2374
2598
  } from "three";
2375
2599
 
2376
2600
  // src/render/target.ts
@@ -2413,8 +2637,8 @@ var TargetMarker = ({ controlsRef }) => {
2413
2637
  const heldUntil = useRef2(0);
2414
2638
  const painted = useRef2(-1);
2415
2639
  const placed = useRef2(false);
2416
- const lastTarget = useMemo3(() => new Vector37(), []);
2417
- const scratch = useMemo3(() => new Vector37(), []);
2640
+ const lastTarget = useMemo3(() => new Vector38(), []);
2641
+ const scratch = useMemo3(() => new Vector38(), []);
2418
2642
  useEffect3(() => {
2419
2643
  const controls = controlsRef.current;
2420
2644
  if (!controls) return void 0;
@@ -2508,13 +2732,41 @@ var TargetMarker = ({ controlsRef }) => {
2508
2732
  ] });
2509
2733
  };
2510
2734
 
2735
+ // src/webgl-unavailable.tsx
2736
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2737
+ var STYLE = {
2738
+ display: "flex",
2739
+ flexDirection: "column",
2740
+ alignItems: "center",
2741
+ justifyContent: "center",
2742
+ gap: "0.5rem",
2743
+ height: "100%",
2744
+ padding: "1rem",
2745
+ boxSizing: "border-box",
2746
+ textAlign: "center"
2747
+ };
2748
+ var DEFAULT_MESSAGE = /* @__PURE__ */ jsxs2(Fragment, { children: [
2749
+ /* @__PURE__ */ jsx3("strong", { children: "3D view unavailable" }),
2750
+ /* @__PURE__ */ jsx3("span", { children: "WebGL is blocked in this browser. Turn on graphics (hardware) acceleration in your browser settings, then restart the browser." })
2751
+ ] });
2752
+ var WebGLUnavailable = ({ children = DEFAULT_MESSAGE }) => /* @__PURE__ */ jsx3(
2753
+ "div",
2754
+ {
2755
+ className: "viewer-webgl-unavailable",
2756
+ "data-viewer-webgl-unavailable": "true",
2757
+ role: "alert",
2758
+ style: STYLE,
2759
+ children
2760
+ }
2761
+ );
2762
+
2511
2763
  // src/render/view-cube.ts
2512
2764
  import {
2513
2765
  BufferGeometry as BufferGeometry2,
2514
2766
  CanvasTexture,
2515
2767
  Float32BufferAttribute as Float32BufferAttribute4,
2516
2768
  SRGBColorSpace,
2517
- Vector3 as Vector38
2769
+ Vector3 as Vector39
2518
2770
  } from "three";
2519
2771
  var VIEW_NAMES = [
2520
2772
  // The six faces.
@@ -2598,12 +2850,12 @@ function viewUp(direction) {
2598
2850
  return { x: 0, y: 0, z: 1 };
2599
2851
  }
2600
2852
  function squaredUp(direction, currentUp) {
2601
- const forward = new Vector38(direction.x, direction.y, direction.z).normalize();
2853
+ const forward = new Vector39(direction.x, direction.y, direction.z).normalize();
2602
2854
  const canonical = viewUp(direction);
2603
- const zero = new Vector38(canonical.x, canonical.y, canonical.z);
2855
+ const zero = new Vector39(canonical.x, canonical.y, canonical.z);
2604
2856
  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);
2857
+ const quarter = new Vector39().crossVectors(forward, zero);
2858
+ const current = new Vector39(currentUp.x, currentUp.y, currentUp.z);
2607
2859
  const rolls = [zero, quarter, zero.clone().negate(), quarter.clone().negate()];
2608
2860
  const nearest = rolls.reduce(
2609
2861
  (best, roll) => roll.dot(current) > best.dot(current) ? roll : best
@@ -2621,11 +2873,11 @@ function cubeZones(chamfer = CHAMFER) {
2621
2873
  const direction = viewVector(name);
2622
2874
  const [sx, sy, sz] = VIEW_SIGNS[name];
2623
2875
  const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
2624
- const normal = new Vector38(direction.x, direction.y, direction.z);
2876
+ const normal = new Vector39(direction.x, direction.y, direction.z);
2625
2877
  let polygon;
2626
2878
  if (kind === "face") {
2627
2879
  const up = viewUp(direction);
2628
- const upVector = new Vector38(up.x, up.y, up.z);
2880
+ const upVector = new Vector39(up.x, up.y, up.z);
2629
2881
  const right = normal.clone().negate().cross(upVector).normalize();
2630
2882
  const square = [
2631
2883
  [-1, -1],
@@ -2647,7 +2899,7 @@ function cubeZones(chamfer = CHAMFER) {
2647
2899
  [chamfer, 1, -chamfer]
2648
2900
  ];
2649
2901
  polygon = rectangle.map(
2650
- ([onA, onB, onC]) => new Vector38().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2902
+ ([onA, onB, onC]) => new Vector39().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2651
2903
  );
2652
2904
  } else {
2653
2905
  const triangle = [
@@ -2655,11 +2907,11 @@ function cubeZones(chamfer = CHAMFER) {
2655
2907
  [chamfer, 1, chamfer],
2656
2908
  [chamfer, chamfer, 1]
2657
2909
  ];
2658
- polygon = triangle.map(([onX, onY, onZ]) => new Vector38(sx * onX, sy * onY, sz * onZ));
2910
+ polygon = triangle.map(([onX, onY, onZ]) => new Vector39(sx * onX, sy * onY, sz * onZ));
2659
2911
  }
2660
2912
  const [first, second, third] = polygon;
2661
2913
  if (first && second && third) {
2662
- const facing = new Vector38().subVectors(second, first).cross(new Vector38().subVectors(third, first));
2914
+ const facing = new Vector39().subVectors(second, first).cross(new Vector39().subVectors(third, first));
2663
2915
  if (facing.dot(normal) < 0) polygon.reverse();
2664
2916
  }
2665
2917
  return { name, kind, direction, polygon: polygon.map(vec) };
@@ -2749,7 +3001,7 @@ function labelTexture(label, color) {
2749
3001
  }
2750
3002
 
2751
3003
  // src/viewer.tsx
2752
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
3004
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2753
3005
  var ViewerControlsContext = createContext3(null);
2754
3006
  var useViewerControls = () => {
2755
3007
  const controls = useContext3(ViewerControlsContext);
@@ -2789,10 +3041,10 @@ var ViewerScene = ({
2789
3041
  const framedRef = useRef3(null);
2790
3042
  const scratchBox = useMemo4(() => new Box33(), []);
2791
3043
  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(), []);
3044
+ const scratchDirection = useMemo4(() => new Vector310(), []);
3045
+ const scratchView = useMemo4(() => new Vector310(), []);
3046
+ const scratchTarget = useMemo4(() => new Vector310(), []);
3047
+ const scratchPosition = useMemo4(() => new Vector310(), []);
2796
3048
  const applyLimits = useCallback(
2797
3049
  (bounds, framed) => {
2798
3050
  controlsRef.current?.applyLimits(
@@ -2841,8 +3093,8 @@ var ViewerScene = ({
2841
3093
  (bounds) => {
2842
3094
  const controls = controlsRef.current;
2843
3095
  if (!controls) return false;
2844
- const target = controls.getTarget(new Vector39());
2845
- const direction = currentViewDirection(camera, target, new Vector39());
3096
+ const target = controls.getTarget(new Vector310());
3097
+ const direction = currentViewDirection(camera, target, new Vector310());
2846
3098
  const distance = fitDistance(projection, size, bounds, DEFAULT_FIT_MARGIN);
2847
3099
  const position = direction.multiplyScalar(distance).add(bounds.center);
2848
3100
  void controls.setLookAt(
@@ -2867,8 +3119,8 @@ var ViewerScene = ({
2867
3119
  const fitContent = useCallback(() => {
2868
3120
  const controls = controlsRef.current;
2869
3121
  if (!controls) return false;
2870
- const target = controls.getTarget(new Vector39());
2871
- return frame(currentViewDirection(camera, target, new Vector39()), true);
3122
+ const target = controls.getTarget(new Vector310());
3123
+ return frame(currentViewDirection(camera, target, new Vector310()), true);
2872
3124
  }, [camera, frame]);
2873
3125
  const resetContent = useCallback(() => {
2874
3126
  const bounds = measure();
@@ -2958,18 +3210,18 @@ var ViewerScene = ({
2958
3210
  initialFrameComplete.current = resetContent();
2959
3211
  }
2960
3212
  });
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(
3213
+ return /* @__PURE__ */ jsx4(ViewerTapProvider, { value: isTap, children: /* @__PURE__ */ jsxs3(ViewerRetargetContext.Provider, { value: retargetOn, children: [
3214
+ /* @__PURE__ */ jsxs3("group", { ref: lightsRef, children: [
3215
+ /* @__PURE__ */ jsx4("ambientLight", { color: theme.ambient, intensity: theme.ambientIntensity }),
3216
+ /* @__PURE__ */ jsx4(
2965
3217
  "hemisphereLight",
2966
3218
  {
2967
3219
  args: [theme.hemisphereSky, theme.hemisphereGround, theme.hemisphereIntensity]
2968
3220
  }
2969
3221
  )
2970
3222
  ] }),
2971
- /* @__PURE__ */ jsx3("group", { ref: contentRef, children }),
2972
- /* @__PURE__ */ jsx3(
3223
+ /* @__PURE__ */ jsx4("group", { ref: contentRef, children }),
3224
+ /* @__PURE__ */ jsx4(
2973
3225
  CadCameraControls,
2974
3226
  {
2975
3227
  controlsRef,
@@ -2978,9 +3230,11 @@ var ViewerScene = ({
2978
3230
  zoomTo
2979
3231
  }
2980
3232
  ),
2981
- showOrbitTarget ? /* @__PURE__ */ jsx3(TargetMarker, { controlsRef }) : null
3233
+ showOrbitTarget ? /* @__PURE__ */ jsx4(TargetMarker, { controlsRef }) : null
2982
3234
  ] }) });
2983
3235
  };
3236
+ var subscribeNever = () => () => {
3237
+ };
2984
3238
  var Viewer = forwardRef(function Viewer2({
2985
3239
  children,
2986
3240
  className,
@@ -2993,8 +3247,10 @@ var Viewer = forwardRef(function Viewer2({
2993
3247
  retargetOnDoubleClick = true,
2994
3248
  showOrbitTarget = false,
2995
3249
  theme,
2996
- onPointerMissed
3250
+ onPointerMissed,
3251
+ webglUnavailable
2997
3252
  }, ref) {
3253
+ const webglAvailable = useSyncExternalStore(subscribeNever, webGLAvailableSnapshot, () => true);
2998
3254
  const actionsRef = useRef3(null);
2999
3255
  const resolved = useMemo4(() => resolveTheme(theme), [theme]);
3000
3256
  const sectionStore = useMemo4(() => createSectionStore(), []);
@@ -3020,7 +3276,7 @@ var Viewer = forwardRef(function Viewer2({
3020
3276
  tracker.current?.dispose();
3021
3277
  tracker.current = element ? trackTaps(element) : null;
3022
3278
  }, []);
3023
- return /* @__PURE__ */ jsx3(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx3(SectionStoreContext.Provider, { value: sectionStore, children: /* @__PURE__ */ jsx3(
3279
+ return /* @__PURE__ */ jsx4(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx4(SectionStoreContext.Provider, { value: sectionStore, children: /* @__PURE__ */ jsx4(
3024
3280
  "div",
3025
3281
  {
3026
3282
  className: ["viewer-root", className].filter(Boolean).join(" "),
@@ -3030,7 +3286,7 @@ var Viewer = forwardRef(function Viewer2({
3030
3286
  if (event.target instanceof HTMLCanvasElement) event.preventDefault();
3031
3287
  },
3032
3288
  style: { height: "100%", width: "100%", ...style },
3033
- children: /* @__PURE__ */ jsx3(
3289
+ children: webglAvailable ? /* @__PURE__ */ jsx4(
3034
3290
  Canvas,
3035
3291
  {
3036
3292
  className: "viewer-canvas-container",
@@ -3051,7 +3307,7 @@ var Viewer = forwardRef(function Viewer2({
3051
3307
  if (event.button !== 0) return;
3052
3308
  if (tracker.current?.isTap(event) ?? true) onPointerMissed?.();
3053
3309
  },
3054
- children: /* @__PURE__ */ jsx3(
3310
+ children: /* @__PURE__ */ jsx4(
3055
3311
  ViewerScene,
3056
3312
  {
3057
3313
  setControls,
@@ -3068,7 +3324,7 @@ var Viewer = forwardRef(function Viewer2({
3068
3324
  )
3069
3325
  },
3070
3326
  projection
3071
- )
3327
+ ) : /* @__PURE__ */ jsx4(WebGLUnavailable, { children: webglUnavailable })
3072
3328
  }
3073
3329
  ) }) });
3074
3330
  });
@@ -3087,7 +3343,7 @@ import {
3087
3343
  Raycaster as Raycaster2,
3088
3344
  Scene,
3089
3345
  Vector2 as Vector26,
3090
- Vector3 as Vector311
3346
+ Vector3 as Vector312
3091
3347
  } from "three";
3092
3348
 
3093
3349
  // src/arrow-mesh.tsx
@@ -3178,7 +3434,7 @@ function createDepthResetMaterial() {
3178
3434
  }
3179
3435
 
3180
3436
  // src/arrow-mesh.tsx
3181
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3437
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3182
3438
  var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
3183
3439
  const invalidate = useThree5((state) => state.invalidate);
3184
3440
  const material = useMemo5(() => createArrowMaterial(), []);
@@ -3191,16 +3447,16 @@ var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
3191
3447
  applyArrowColor(material, color, opacity);
3192
3448
  invalidate();
3193
3449
  }, [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 })
3450
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
3451
+ reset ? /* @__PURE__ */ jsx5("mesh", { geometry, material: reset, renderOrder: 0, raycast: () => null }) : null,
3452
+ /* @__PURE__ */ jsx5("mesh", { geometry, material: depth, renderOrder: 1, raycast: () => null }),
3453
+ /* @__PURE__ */ jsx5("mesh", { geometry, material, renderOrder: 2, ...events })
3198
3454
  ] });
3199
3455
  };
3200
3456
 
3201
3457
  // src/render/arrow.ts
3202
- import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector310 } from "three";
3203
- var ARROW_AXIS2 = new Vector310(0, 1, 0);
3458
+ import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector311 } from "three";
3459
+ var ARROW_AXIS2 = new Vector311(0, 1, 0);
3204
3460
  var ARROW_ROUND = 32;
3205
3461
  var HEAD_LENGTH = 0.415;
3206
3462
  var HEAD_RADIUS = 0.153;
@@ -3426,12 +3682,12 @@ function createMaskMaterial() {
3426
3682
  }
3427
3683
 
3428
3684
  // src/section-view.tsx
3429
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3685
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3430
3686
  var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
3431
3687
  function resolveSectionPlane(options, box) {
3432
3688
  if (!options?.enabled || box.isEmpty()) return null;
3433
3689
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_SECTION_NORMAL;
3434
- const axis = new Vector311(normal.x, normal.y, normal.z);
3690
+ const axis = new Vector312(normal.x, normal.y, normal.z);
3435
3691
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
3436
3692
  axis.normalize();
3437
3693
  const bounds = sectionBounds(box, normal);
@@ -3452,7 +3708,7 @@ function resolveSectionPlane(options, box) {
3452
3708
  }
3453
3709
  };
3454
3710
  }
3455
- var Z = new Vector311(0, 0, 1);
3711
+ var Z = new Vector312(0, 0, 1);
3456
3712
  var SectionView = ({
3457
3713
  geometry,
3458
3714
  box,
@@ -3472,7 +3728,7 @@ var SectionView = ({
3472
3728
  const handleRef = useRef4(null);
3473
3729
  const [hovered, setHovered] = useState(false);
3474
3730
  const dragging = useRef4(null);
3475
- const span = useMemo6(() => box.getSize(new Vector311()).length(), [box]);
3731
+ const span = useMemo6(() => box.getSize(new Vector312()).length(), [box]);
3476
3732
  const frameSize = span * SECTION_GIZMO_FRAME_SCALE;
3477
3733
  const directionColor2 = useMemo6(() => sectionDirectionColor(plane.normal), [plane]);
3478
3734
  const resolvedHandleColor = sectionHandleColor(handleColor, directionColor2);
@@ -3556,7 +3812,7 @@ var SectionView = ({
3556
3812
  event.stopPropagation();
3557
3813
  const view = camera.position.clone().sub(capPosition).normalize();
3558
3814
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
3559
- const hit = event.ray.intersectPlane(surface, new Vector311());
3815
+ const hit = event.ray.intersectPlane(surface, new Vector312());
3560
3816
  const start = {
3561
3817
  plane: surface,
3562
3818
  from: hit ? hit.dot(plane.normal) : 0,
@@ -3573,7 +3829,7 @@ var SectionView = ({
3573
3829
  -((native.clientY - rect.top) / rect.height) * 2 + 1
3574
3830
  );
3575
3831
  raycaster.setFromCamera(pointer, camera);
3576
- const point = raycaster.ray.intersectPlane(start.plane, new Vector311());
3832
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector312());
3577
3833
  if (!point) return;
3578
3834
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
3579
3835
  invalidate();
@@ -3605,8 +3861,8 @@ var SectionView = ({
3605
3861
  // Excluded from framing, all of it. The cap is a quad half again as wide as
3606
3862
  // the part's diagonal, so a Fit that measured it would frame the cut rather
3607
3863
  // than the part and leave the part a speck in the middle of it.
3608
- /* @__PURE__ */ jsxs4("group", { userData: FURNITURE2, children: [
3609
- /* @__PURE__ */ jsx5(
3864
+ /* @__PURE__ */ jsxs5("group", { userData: FURNITURE2, children: [
3865
+ /* @__PURE__ */ jsx6(
3610
3866
  "mesh",
3611
3867
  {
3612
3868
  geometry,
@@ -3615,7 +3871,7 @@ var SectionView = ({
3615
3871
  raycast: () => null
3616
3872
  }
3617
3873
  ),
3618
- /* @__PURE__ */ jsx5(
3874
+ /* @__PURE__ */ jsx6(
3619
3875
  "mesh",
3620
3876
  {
3621
3877
  geometry,
@@ -3624,7 +3880,7 @@ var SectionView = ({
3624
3880
  raycast: () => null
3625
3881
  }
3626
3882
  ),
3627
- /* @__PURE__ */ jsx5(
3883
+ /* @__PURE__ */ jsx6(
3628
3884
  "mesh",
3629
3885
  {
3630
3886
  geometry: quad,
@@ -3635,15 +3891,15 @@ var SectionView = ({
3635
3891
  raycast: () => null
3636
3892
  }
3637
3893
  ),
3638
- showHandle && onDrag ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
3639
- /* @__PURE__ */ jsxs4(
3894
+ showHandle && onDrag ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
3895
+ /* @__PURE__ */ jsxs5(
3640
3896
  "group",
3641
3897
  {
3642
3898
  position: capPosition,
3643
3899
  quaternion: capQuaternion,
3644
3900
  renderOrder: SECTION_RENDER_ORDER.outline,
3645
3901
  children: [
3646
- /* @__PURE__ */ jsx5("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx5(
3902
+ /* @__PURE__ */ jsx6("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx6(
3647
3903
  "meshBasicMaterial",
3648
3904
  {
3649
3905
  color: directionColor2,
@@ -3654,18 +3910,18 @@ var SectionView = ({
3654
3910
  depthWrite: false
3655
3911
  }
3656
3912
  ) }),
3657
- /* @__PURE__ */ jsx5("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx5("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
3913
+ /* @__PURE__ */ jsx6("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx6("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
3658
3914
  ]
3659
3915
  }
3660
3916
  ),
3661
- /* @__PURE__ */ jsx5(
3917
+ /* @__PURE__ */ jsx6(
3662
3918
  "group",
3663
3919
  {
3664
3920
  ref: handleRef,
3665
3921
  position: capPosition,
3666
3922
  quaternion: handleQuaternion,
3667
3923
  renderOrder: SECTION_RENDER_ORDER.handle,
3668
- children: /* @__PURE__ */ jsx5(
3924
+ children: /* @__PURE__ */ jsx6(
3669
3925
  ArrowMesh,
3670
3926
  {
3671
3927
  geometry: handleShape,
@@ -3702,7 +3958,7 @@ function useContentBox() {
3702
3958
  }
3703
3959
 
3704
3960
  // src/part-mesh.tsx
3705
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3961
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3706
3962
  var PartMesh = ({
3707
3963
  model,
3708
3964
  geometry,
@@ -3724,6 +3980,7 @@ var PartMesh = ({
3724
3980
  theme,
3725
3981
  showEdges = true,
3726
3982
  sharpCorners = null,
3983
+ threads = null,
3727
3984
  display = "solid",
3728
3985
  children
3729
3986
  }) => {
@@ -3740,11 +3997,11 @@ var PartMesh = ({
3740
3997
  onHoverRef.current = onHover;
3741
3998
  const box = useContentBox();
3742
3999
  const store = useSectionStore();
3743
- const shared = useSyncExternalStore(store.subscribe, store.get, store.get);
4000
+ const shared = useSyncExternalStore2(store.subscribe, store.get, store.get);
3744
4001
  const controlled = section !== void 0;
3745
4002
  const options = controlled ? section : shared ?? void 0;
3746
4003
  const cut = useMemo7(() => resolveSectionPlane(options, box), [box, options]);
3747
- const engaged = useSyncExternalStore(store.subscribe, store.isEngaged, store.isEngaged);
4004
+ const engaged = useSyncExternalStore2(store.subscribe, store.isEngaged, store.isEngaged);
3748
4005
  const pressedWhileEngaged = useRef6(false);
3749
4006
  const layers = useRef6({
3750
4007
  selection,
@@ -3825,6 +4082,13 @@ var PartMesh = ({
3825
4082
  if (hoverCorner.current !== null) dropHover();
3826
4083
  invalidate();
3827
4084
  }, [dropHover, invalidate, part, sharpKey]);
4085
+ const threadsRef = useRef6(threads);
4086
+ threadsRef.current = threads;
4087
+ const threadsKey = threadKey(threads);
4088
+ useLayoutEffect3(() => {
4089
+ part.setThreads(threadsRef.current);
4090
+ invalidate();
4091
+ }, [invalidate, part, threadsKey]);
3828
4092
  const reportedSection = useRef6("");
3829
4093
  useLayoutEffect3(() => {
3830
4094
  const state = cut?.state ?? null;
@@ -3914,8 +4178,8 @@ var PartMesh = ({
3914
4178
  },
3915
4179
  [box, controlled, cut, onSectionChange, options, store]
3916
4180
  );
3917
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
3918
- /* @__PURE__ */ jsx6(
4181
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
4182
+ /* @__PURE__ */ jsx7(
3919
4183
  "primitive",
3920
4184
  {
3921
4185
  object: part.object,
@@ -3952,7 +4216,7 @@ var PartMesh = ({
3952
4216
  }
3953
4217
  }
3954
4218
  ),
3955
- cut ? /* @__PURE__ */ jsx6(
4219
+ cut ? /* @__PURE__ */ jsx7(
3956
4220
  SectionView,
3957
4221
  {
3958
4222
  geometry,
@@ -3964,12 +4228,12 @@ var PartMesh = ({
3964
4228
  onDrag: !controlled || onSectionChange ? dragSection : void 0
3965
4229
  }
3966
4230
  ) : null,
3967
- children ? /* @__PURE__ */ jsx6(PartContext.Provider, { value: context, children }) : null
4231
+ children ? /* @__PURE__ */ jsx7(PartContext.Provider, { value: context, children }) : null
3968
4232
  ] });
3969
4233
  };
3970
- var ORIGIN = new Vector312();
3971
- var UP = new Vector312(0, 0, 1);
3972
- var TARGET = new Vector312();
4234
+ var ORIGIN = new Vector313();
4235
+ var UP = new Vector313(0, 0, 1);
4236
+ var TARGET = new Vector313();
3973
4237
  function readTarget(controls) {
3974
4238
  if (controls && typeof controls.getTarget === "function") {
3975
4239
  return controls.getTarget(TARGET);
@@ -4618,7 +4882,7 @@ function compareVersions(a, b) {
4618
4882
  }
4619
4883
 
4620
4884
  // src/engine/engine-part.tsx
4621
- import { jsx as jsx7 } from "react/jsx-runtime";
4885
+ import { jsx as jsx8 } from "react/jsx-runtime";
4622
4886
  var models = /* @__PURE__ */ new WeakMap();
4623
4887
  var modelForReport = (report) => {
4624
4888
  if (typeof report !== "object" || report === null) {
@@ -4639,7 +4903,7 @@ var EnginePart = ({ report, ...props }) => {
4639
4903
  engineGeometryCache.retain(resource);
4640
4904
  return () => engineGeometryCache.release(resource);
4641
4905
  }, [resource]);
4642
- return /* @__PURE__ */ jsx7(PartMesh, { model, geometry: resource.geometry, ...props });
4906
+ return /* @__PURE__ */ jsx8(PartMesh, { model, geometry: resource.geometry, ...props });
4643
4907
  };
4644
4908
  function useGeometryResource(part) {
4645
4909
  const resource = engineGeometryCache.get(part);
@@ -4648,6 +4912,23 @@ function useGeometryResource(part) {
4648
4912
  return resource;
4649
4913
  }
4650
4914
 
4915
+ // src/engine/threads.ts
4916
+ function threadMark(feature, datasheet) {
4917
+ if (!isRecord(datasheet) || !isRecord(datasheet["facts"])) return null;
4918
+ const facts = datasheet["facts"];
4919
+ if (facts["kind"] !== "Hole" || !isRecord(facts["threading"])) return null;
4920
+ const spec = facts["threading"]["spec"];
4921
+ if (!isRecord(spec)) return null;
4922
+ const pitch = spec["threadPitch"];
4923
+ const majorDiameter = spec["basicDiameter"];
4924
+ if (!isFiniteNumber(pitch) || pitch <= 0 || !isFiniteNumber(majorDiameter)) return null;
4925
+ return {
4926
+ regions: feature.regionIdxs,
4927
+ pitch: spec["handedness"] === "Left" ? -pitch : pitch,
4928
+ majorDiameter
4929
+ };
4930
+ }
4931
+
4651
4932
  export {
4652
4933
  usePartContext,
4653
4934
  sameDirection,
@@ -4762,6 +5043,7 @@ export {
4762
5043
  bestOwner,
4763
5044
  cycleOwner,
4764
5045
  NO_MODIFIERS,
5046
+ ignoreRay,
4765
5047
  viewDirection,
4766
5048
  buildPick,
4767
5049
  focusForPick,
@@ -4822,5 +5104,6 @@ export {
4822
5104
  MIN_KERNEL_VERSION,
4823
5105
  normalizePartReport,
4824
5106
  assertSupportedKernelVersion,
4825
- EnginePart
5107
+ EnginePart,
5108
+ threadMark
4826
5109
  };