@toolpath/viewer 2.0.0 → 2.1.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
@@ -357,8 +357,8 @@ individual buttons to use only the controls and order your application needs.
357
357
  </ViewerToolbarProvider>
358
358
  ```
359
359
 
360
- `ViewerToolbar.Controls`, `Divider`, and the thirteen `*Button` components are available on
361
- `ViewerToolbar`: `StockButton`, `AxesButton`, `GridButton`, `BananaButton`, `DirectionsButton`,
360
+ `ViewerToolbar.Controls`, `Divider`, and the fourteen `*Button` components are available on
361
+ `ViewerToolbar`: `StockButton`, `AxesButton`, `GridButton`, `BananaButton`, `ToolsButton`, `DirectionsButton`,
362
362
  `HoverButton`, `FocusButton`, `WireframeButton`, `SectionButton`, `MeasureButton`, `FitButton`,
363
363
  `ResetButton`, and `TopButton`. A rendered button requires its matching provider control. For an
364
364
  application-specific toolbar component, `useViewerToolbar()` reads the same controls from inside
@@ -388,19 +388,19 @@ override any of these selectors in your application stylesheet.
388
388
  The following `data-*` attributes identify generated elements without depending on labels, which
389
389
  can change with state or localization:
390
390
 
391
- | Attribute | Element and values |
392
- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
393
- | `data-viewer-root="true"` | `<Viewer>` wrapper |
394
- | `data-viewer-canvas-container="true"` | R3F canvas event container |
395
- | `data-viewer-canvas-frame="true"` | R3F canvas-sizing frame |
396
- | `data-viewer-canvas="true"` | The `<canvas>` itself |
397
- | `data-viewer-hover-card="true"` | `<HoverCard>` shell |
398
- | `data-viewer-toolbar="true"` | Toolbar stack |
399
- | `data-viewer-toolbar-controls="true"` | Controls group |
400
- | `data-viewer-toolbar-action` | Standard button: `stock`, `axes`, `grid`, `banana`, `directions`, `hover`, `focus`, `wireframe`, `section`, `measure`, `fit`, `reset`, or `top` |
401
- | `data-viewer-toolbar-icon="true"` | Standard button SVG |
402
- | `data-viewer-toolbar-tooltip="true"` | Standard button tooltip |
403
- | `data-viewer-toolbar-divider="true"` | Toolbar separator |
391
+ | Attribute | Element and values |
392
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
393
+ | `data-viewer-root="true"` | `<Viewer>` wrapper |
394
+ | `data-viewer-canvas-container="true"` | R3F canvas event container |
395
+ | `data-viewer-canvas-frame="true"` | R3F canvas-sizing frame |
396
+ | `data-viewer-canvas="true"` | The `<canvas>` itself |
397
+ | `data-viewer-hover-card="true"` | `<HoverCard>` shell |
398
+ | `data-viewer-toolbar="true"` | Toolbar stack |
399
+ | `data-viewer-toolbar-controls="true"` | Controls group |
400
+ | `data-viewer-toolbar-action` | Standard button: `stock`, `axes`, `grid`, `banana`, `tools`, `directions`, `hover`, `focus`, `wireframe`, `section`, `measure`, `fit`, `reset`, or `top` |
401
+ | `data-viewer-toolbar-icon="true"` | Standard button SVG |
402
+ | `data-viewer-toolbar-tooltip="true"` | Standard button tooltip |
403
+ | `data-viewer-toolbar-divider="true"` | Toolbar separator |
404
404
 
405
405
  Toggle buttons also carry standard `aria-pressed="true"` or `"false"`; use that to style their
406
406
  current state. `Fit`, `Reset`, and `Top view` are actions rather than toggles, so they omit the
@@ -501,6 +501,7 @@ Draws the part and handles clicks. You only use it directly when you
501
501
  | `section` | `SectionOptions` | Cut the part open. Omit it to follow the viewer's own cut. See [Section view](#section-view). |
502
502
  | `onSectionChange` | `(state: SectionState) => void` | Called when the cut moves or goes away. With `section`, passing it also shows the drag handle. |
503
503
  | `onAdjacency` | `(map) => void` | Called once per mesh with which faces touch which. |
504
+ | `children` | `ReactNode` | Objects drawn on the part. They read its model and mesh with `usePartContext()`. |
504
505
 
505
506
  Hovering over the part is handled for you. You only need `onHover` if you want to show the hovered
506
507
  feature elsewhere in your UI.
@@ -606,6 +607,56 @@ what a label looks like is your stylesheet's, through the `toolpath-measure-labe
606
607
  data attributes on each one — see [Measuring](#measuring) for a starting point. While the tool is
607
608
  mounted the part reports no hovers or picks, as with `<SectionTool>`.
608
609
 
610
+ ### `<ToolMarks>`
611
+
612
+ Cutting tools drawn on the part, each to scale where it stands: a see-through end mill — flat,
613
+ bull nose or ball — or any tool given by its profile, rimmed at its bottom and top, with a dimension
614
+ across its top and a label on it. What a tool stands for is yours: the widest tool a pocket admits, the tool an operation uses.
615
+ Nothing here reads a datasheet.
616
+
617
+ ```tsx
618
+ <EnginePart report={report}>
619
+ <ToolMarks
620
+ marks={[
621
+ {
622
+ base: { x: 12, y: 4, z: -8 },
623
+ axis: { x: 0, y: 0, z: 1 },
624
+ diameter: 6,
625
+ height: 8,
626
+ cornerRadius: 0.5,
627
+ label: '⌀ 6.000 mm',
628
+ note: 'bull nose R 0.5',
629
+ },
630
+ ]}
631
+ />
632
+ </EnginePart>
633
+ ```
634
+
635
+ | Prop | What it does |
636
+ | ---------------- | ---------------------------------------------------------------------------------- |
637
+ | `marks` | The tools, in the coordinates the part is drawn in. See `ToolMark` below. |
638
+ | `visible` | `false` hides them without unmounting. On by default. |
639
+ | `labelClassName` | Added to every label, beside `toolpath-measure-label`. |
640
+ | `theme` | `measure` colours the rims and the dimension, as it does the measure tool's lines. |
641
+
642
+ A `ToolMark` is `base` (the centre of the tool's bottom), `axis` (up the tool), `diameter`,
643
+ `height`, and optionally `cornerRadius` (0 flat, half the diameter a ball), `across` (which way the
644
+ dimension runs; any direction square to the axis otherwise), `label` and `note`. Any other tool —
645
+ a drill, a chamfer mill, a tool in its holder — takes `profile`: its side as `{ r, z }` points from
646
+ the tip up, such as `@toolpath/tool-drawing`'s outline, turned about the axis in place of the end
647
+ mill.
648
+
649
+ Show and hide them with `visible`, from the toolbar's `ToolsButton` and its `tools` control.
650
+
651
+ It draws as `<MeasureTool>` draws: the lines show through the part, and each label is an unstyled
652
+ `<div class="toolpath-measure-label" data-measure-label="tool">` with its text in a
653
+ `data-measure-value` span and its note in a `data-measure-note` one. The marks take no pointer
654
+ events and are left out of framing.
655
+
656
+ Inside `<PartMesh>` or `<EnginePart>`, `usePartContext()` gives the part's `model` and `geometry`,
657
+ and `featureTriangles(model, geometry, tag)` a feature's triangles, for working out where a tool
658
+ stands.
659
+
609
660
  ### `<Grid>` and `<Axes>`
610
661
 
611
662
  Reference geometry. The camera ignores both when it frames the part.
@@ -1185,6 +1236,7 @@ is a complete app built this way, with no API key needed.
1185
1236
  | `useViewerToolbar()` | Inside `<ViewerToolbarProvider>` | Get the actions and state supplied to the provider. |
1186
1237
  | `useSectionStore()` | Inside `<Viewer>` | Read, set, or subscribe to the viewer's own cut. |
1187
1238
  | `useContentBox()` | Inside `<Viewer>` | Get the part's bounding box (a `THREE.Box3`, empty until loaded). |
1239
+ | `usePartContext()` | Inside `<PartMesh>` | Get the part's `model` and the `geometry` it is drawn from, for an overlay. |
1188
1240
  | `useTapGuard()` | Inside `<Viewer>` | Check whether a pointer event was a click and not a drag. |
1189
1241
 
1190
1242
  ### Helpers
@@ -1198,6 +1250,7 @@ is a complete app built this way, with no API key needed.
1198
1250
  | `focusForPick(pick, lastRegion, lastFocus)` | Steps through a face's matches on repeated clicks. |
1199
1251
  | `rankOwners` / `bestOwner` | The ranking `onPick` uses, if you want to run it yourself. |
1200
1252
  | `groupByDirection(model)` | Features grouped by machining direction. |
1253
+ | `featureTriangles(model, geometry, tag)` | A feature's triangles and outward normals, as plain arrays. |
1201
1254
  | `directionLabel(v)` / `directionColor(i)` | A direction's label and colour. |
1202
1255
  | `sectionFromPick({ point, normal })` | Turns a clicked surface into a section plane. |
1203
1256
  | `measurementLabel(measurement, format?)` | Writes a measurement the way `<MeasureTool>` labels it. |
@@ -13,6 +13,15 @@ import {
13
13
  } from "react";
14
14
  import { Vector3 as Vector311 } from "three";
15
15
 
16
+ // src/part-context.tsx
17
+ import { createContext, useContext } from "react";
18
+ var PartContext = createContext(null);
19
+ var usePartContext = () => {
20
+ const part = useContext(PartContext);
21
+ if (!part) throw new Error("usePartContext must be used inside <PartMesh> or <EnginePart>");
22
+ return part;
23
+ };
24
+
16
25
  // src/model/directions.ts
17
26
  var EPSILON = 1e-6;
18
27
  function sameDirection(a, b) {
@@ -781,8 +790,8 @@ function perpendicular(axis) {
781
790
  // src/tap.tsx
782
791
  import { useThree } from "@react-three/fiber";
783
792
  import {
784
- createContext,
785
- useContext,
793
+ createContext as createContext2,
794
+ useContext as useContext2,
786
795
  useEffect,
787
796
  useMemo,
788
797
  useRef
@@ -823,10 +832,10 @@ function trackDoubleTaps(within = DOUBLE_TAP_MS, slop = TAP_SLOP) {
823
832
 
824
833
  // src/tap.tsx
825
834
  import { jsx } from "react/jsx-runtime";
826
- var ViewerTapContext = createContext(null);
835
+ var ViewerTapContext = createContext2(null);
827
836
  var ViewerTapProvider = ({ value, children }) => /* @__PURE__ */ jsx(ViewerTapContext.Provider, { value, children });
828
837
  function useTapGuard() {
829
- const shared = useContext(ViewerTapContext);
838
+ const shared = useContext2(ViewerTapContext);
830
839
  const domElement = useThree((state) => state.gl.domElement);
831
840
  const tracker = useRef(null);
832
841
  useEffect(() => {
@@ -1319,10 +1328,10 @@ function focusForPick(pick, previousRegion, previousFocus) {
1319
1328
  // src/viewer.tsx
1320
1329
  import { Canvas, useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
1321
1330
  import {
1322
- createContext as createContext2,
1331
+ createContext as createContext3,
1323
1332
  forwardRef,
1324
1333
  useCallback,
1325
- useContext as useContext2,
1334
+ useContext as useContext3,
1326
1335
  useEffect as useEffect4,
1327
1336
  useImperativeHandle,
1328
1337
  useMemo as useMemo4,
@@ -2193,20 +2202,20 @@ function labelTexture(label, color) {
2193
2202
 
2194
2203
  // src/viewer.tsx
2195
2204
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2196
- var ViewerControlsContext = createContext2(null);
2205
+ var ViewerControlsContext = createContext3(null);
2197
2206
  var useViewerControls = () => {
2198
- const controls = useContext2(ViewerControlsContext);
2207
+ const controls = useContext3(ViewerControlsContext);
2199
2208
  if (!controls) throw new Error("useViewerControls must be used inside <Viewer>");
2200
2209
  return controls;
2201
2210
  };
2202
- var SectionStoreContext = createContext2(null);
2211
+ var SectionStoreContext = createContext3(null);
2203
2212
  var useSectionStore = () => {
2204
- const store = useContext2(SectionStoreContext);
2213
+ const store = useContext3(SectionStoreContext);
2205
2214
  if (!store) throw new Error("useSectionStore must be used inside <Viewer>");
2206
2215
  return store;
2207
2216
  };
2208
- var ViewerRetargetContext = createContext2(null);
2209
- var useRetarget = () => useContext2(ViewerRetargetContext);
2217
+ var ViewerRetargetContext = createContext3(null);
2218
+ var useRetarget = () => useContext3(ViewerRetargetContext);
2210
2219
  var ViewerScene = ({
2211
2220
  children,
2212
2221
  setControls,
@@ -3029,9 +3038,11 @@ var PartMesh = ({
3029
3038
  onPick,
3030
3039
  theme,
3031
3040
  showEdges = true,
3032
- display = "solid"
3041
+ display = "solid",
3042
+ children
3033
3043
  }) => {
3034
3044
  const { camera, controls, invalidate } = useThree7();
3045
+ const context = useMemo6(() => ({ model, geometry }), [model, geometry]);
3035
3046
  const viewerControls = useViewerControls();
3036
3047
  const resolved = useStableTheme(theme);
3037
3048
  const currentTheme = useRef6(resolved);
@@ -3243,7 +3254,8 @@ var PartMesh = ({
3243
3254
  showHandle: !controlled || onSectionChange !== void 0,
3244
3255
  onDrag: !controlled || onSectionChange ? dragSection : void 0
3245
3256
  }
3246
- ) : null
3257
+ ) : null,
3258
+ children ? /* @__PURE__ */ jsx5(PartContext.Provider, { value: context, children }) : null
3247
3259
  ] });
3248
3260
  };
3249
3261
  var ORIGIN = new Vector311();
@@ -3890,6 +3902,7 @@ function useGeometryResource(part) {
3890
3902
  }
3891
3903
 
3892
3904
  export {
3905
+ usePartContext,
3893
3906
  sameDirection,
3894
3907
  directionIndexOf,
3895
3908
  groupByDirection,
@@ -1,5 +1,5 @@
1
- import { P as PartModel, a as PartMeshRefs } from '../normalize-CvnV9Ggh.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-CvnV9Ggh.js';
1
+ import { P as PartModel, a as PartMeshRefs } from '../normalize--FiXUhO4.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--FiXUhO4.js';
3
3
  import { BufferGeometry } from 'three';
4
4
  import 'react';
5
5
 
@@ -12,7 +12,7 @@ import {
12
12
  parsePartGeometry,
13
13
  partMeshAssets,
14
14
  smoothRegionNormals
15
- } from "../chunk-4TBLMR4Z.js";
15
+ } from "../chunk-WTTOUG4P.js";
16
16
  export {
17
17
  EnginePart,
18
18
  MIN_KERNEL_VERSION,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PartModel, d as PartPick, V as Vec3, R as RegionHighlight, e as ViewerTheme, S as SectionOptions, f as SectionState, g as PartModelRegion, T as TriangleRange, F as FeatureTag, h as RegionIndex, i as PartModelFeature, j as FeatureType } from './normalize-CvnV9Ggh.js';
2
- export { A as AXES_PLANE_OFFSET, k as AXES_PLANE_SCALE, l as AxesPlane, B as BuildPickInput, C as CANDIDATE_WEIGHT, m as CAP_HATCH, D as DEFAULT_SECTION_NORMAL, o as DEFAULT_THEME, p as DIRECTION_COLORS, q as DISABLED_SECTION, E as EnginePart, r as FeatureHighlight, t as FocusOptions, H as HANDLE_PIXELS, u as HIGHLIGHT_COLORS, v as HIGHLIGHT_WEIGHT, w as HOVER_WEIGHT, x as HighlightLayers, K as KnownFeatureType, y as KnownShapeKind, N as NO_MODIFIERS, O as OUTLINE_SCALE, z as PICKED_SURFACE_LABEL, G as PREVIEW_SCALE, I as PartDisplay, J as PartMesh, L as PartMeshProps, a as PartMeshRefs, Q as PartObject, U as PickModifiers, W as PointerLocation, X as REGION_ATTRIBUTE, Y as RegionPaint, Z as SECTION_GIZMO_FRAME_SCALE, _ as SECTION_RENDER_ORDER, $ as SectionAnchor, a0 as SectionBounds, a1 as SectionPlacement, a2 as ShapeKind, a3 as SurfaceHit, a4 as applyHighlightLayers, a5 as axesPlanes, a6 as buildPick, a7 as buildRegionAttribute, a8 as buildRegionTexels, a9 as createPart, aa as directionColor, ab as dragPlane, ac as focusForPick, ad as hitUnderRay, n as normalizePartReport, ae as pickedStartDepth, af as resolveTheme, ag as sectionAnchor, ah as sectionBounds, ai as sectionConstant, aj as sectionDepth, ak as sectionDepthConstant, al as sectionDepthRange, am as sectionFromPick, an as sectionMeasurement, ao as sectionOffset, ap as sectionOptionsFromState, aq as sectionPlane, s as smoothRegionNormals, ar as surfaceUnderRay, as as themesEqual, at as viewDirection } from './normalize-CvnV9Ggh.js';
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 PartModelRegion, T as TriangleRange, h as RegionIndex, i as PartModelFeature, j as FeatureType } from './normalize--FiXUhO4.js';
2
+ export { A as AXES_PLANE_OFFSET, k as AXES_PLANE_SCALE, l as AxesPlane, B as BuildPickInput, C as CANDIDATE_WEIGHT, m as CAP_HATCH, D as DEFAULT_SECTION_NORMAL, o as DEFAULT_THEME, p as DIRECTION_COLORS, q as DISABLED_SECTION, E as EnginePart, r as FeatureHighlight, t as FocusOptions, H as HANDLE_PIXELS, u as HIGHLIGHT_COLORS, v as HIGHLIGHT_WEIGHT, w as HOVER_WEIGHT, x as HighlightLayers, K as KnownFeatureType, y as KnownShapeKind, N as NO_MODIFIERS, O as OUTLINE_SCALE, z as PICKED_SURFACE_LABEL, G as PREVIEW_SCALE, I as PartDisplay, J as PartMesh, L as PartMeshProps, a as PartMeshRefs, Q as PartObject, U as PickModifiers, W as PointerLocation, X as REGION_ATTRIBUTE, Y as RegionPaint, Z as SECTION_GIZMO_FRAME_SCALE, _ as SECTION_RENDER_ORDER, $ as SectionAnchor, a0 as SectionBounds, a1 as SectionPlacement, a2 as ShapeKind, a3 as SurfaceHit, a4 as applyHighlightLayers, a5 as axesPlanes, a6 as buildPick, a7 as buildRegionAttribute, a8 as buildRegionTexels, a9 as createPart, aa as directionColor, ab as dragPlane, ac as focusForPick, ad as hitUnderRay, n as normalizePartReport, ae as pickedStartDepth, af as resolveTheme, ag as sectionAnchor, ah as sectionBounds, ai as sectionConstant, aj as sectionDepth, ak as sectionDepthConstant, al as sectionDepthRange, am as sectionFromPick, an as sectionMeasurement, ao as sectionOffset, ap as sectionOptionsFromState, aq as sectionPlane, s as smoothRegionNormals, ar as surfaceUnderRay, as as themesEqual, at as viewDirection } from './normalize--FiXUhO4.js';
3
3
  import { BufferGeometry, Box3, CanvasTexture, Plane, Vector3, OrthographicCamera, PerspectiveCamera, Object3D, 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';
@@ -13,6 +13,49 @@ import CameraControls from 'camera-controls';
13
13
  */
14
14
  declare function regionAdjacency(model: PartModel, geometry: BufferGeometry): Map<number, Set<number>>;
15
15
 
16
+ /**
17
+ * A feature's faces as plain triangles: what something placed on the part
18
+ * fits itself against, without needing three.js to read them.
19
+ */
20
+ interface FeatureTriangles {
21
+ /** Nine numbers a triangle — its three corners, x, y, z each — in the geometry's coordinates. */
22
+ readonly positions: Float32Array;
23
+ /** Three a triangle: its unit normal, pointing out of the part. */
24
+ readonly normals: Float32Array;
25
+ }
26
+ /**
27
+ * Every triangle of a feature's faces, read out of the mesh through the
28
+ * region index.
29
+ *
30
+ * Each normal is the mesh's own where it has them — the average of the three
31
+ * corners' — because they say which side is outside whatever order a
32
+ * triangle's corners are wound in. A mesh without normals gives each triangle
33
+ * its winding's. A triangle with no area has no normal and is left out.
34
+ *
35
+ * Indexed and non-indexed meshes both read: triangle `t` is corners `3t` to
36
+ * `3t + 2` of the index where there is one, of the positions where there is
37
+ * not. Empty for a tag the part does not have, or a geometry with no positions.
38
+ */
39
+ declare function featureTriangles(model: Pick<PartModel, 'regionIndex'>, geometry: BufferGeometry, tag: FeatureTag): FeatureTriangles;
40
+
41
+ /**
42
+ * The part an overlay is drawn on: its model and the mesh `<PartMesh>` draws.
43
+ *
44
+ * `<EnginePart>` loads its mesh itself, so a child of it has no other way to
45
+ * reach the geometry — and an overlay placed on the part, such as
46
+ * `<ToolMarks>` at a feature's faces, needs the triangles it is drawn against.
47
+ */
48
+ interface PartContextValue {
49
+ readonly model: PartModel;
50
+ /** In the coordinates the part is drawn in: the caller's, whatever it did to centre it. */
51
+ readonly geometry: BufferGeometry;
52
+ }
53
+ /**
54
+ * The part this is drawn inside: a child of `<PartMesh>` or `<EnginePart>`.
55
+ * Throws anywhere else, as `useViewerControls` does outside `<Viewer>`.
56
+ */
57
+ declare const usePartContext: () => PartContextValue;
58
+
16
59
  interface HoverCardProps {
17
60
  /** The current `onHover` pick, or `null` after the pointer leaves the part. */
18
61
  pick: PartPick | null;
@@ -44,6 +87,8 @@ interface ViewerToolbarControls {
44
87
  axes?: ViewerToolbarControl;
45
88
  grid?: ViewerToolbarControl;
46
89
  banana?: ViewerToolbarControl;
90
+ /** Tools drawn on the part: see `<ToolMarks>`. */
91
+ tools?: ViewerToolbarControl;
47
92
  directions?: ViewerToolbarControl;
48
93
  hover?: ViewerToolbarControl;
49
94
  focus?: ViewerToolbarControl;
@@ -84,6 +129,7 @@ declare const ViewerToolbar: (({ className, children }: ViewerToolbarProps) => r
84
129
  AxesButton: () => react.JSX.Element;
85
130
  GridButton: () => react.JSX.Element;
86
131
  BananaButton: () => react.JSX.Element;
132
+ ToolsButton: () => react.JSX.Element;
87
133
  DirectionsButton: () => react.JSX.Element;
88
134
  HoverButton: () => react.JSX.Element;
89
135
  FocusButton: () => react.JSX.Element;
@@ -947,6 +993,81 @@ interface MeasureToolProps {
947
993
  */
948
994
  declare const MeasureTool: ({ mode, measurements, onChange, showDeltas, format, labelClassName, theme, }: MeasureToolProps) => react.JSX.Element | null;
949
995
 
996
+ /**
997
+ * A cutting tool drawn on the part: an end mill standing at a place, up an
998
+ * axis, to scale. What it stands for — the widest tool a pocket admits, the
999
+ * tool an operation uses — is the caller's; this is only where and how big.
1000
+ */
1001
+ interface ToolMark {
1002
+ /** The centre of the tool's bottom, in the coordinates the part is drawn in. */
1003
+ readonly base: Vec3;
1004
+ /** Up the tool, from its tip to its shank. Need not be unit length. */
1005
+ readonly axis: Vec3;
1006
+ readonly diameter: number;
1007
+ /** How tall the drawn tool stands from its tip. */
1008
+ readonly height: number;
1009
+ /**
1010
+ * The radius on the tool's corner: 0 for a flat end mill, half the diameter
1011
+ * for a ball, anything between for a bull nose. Held to that range.
1012
+ */
1013
+ readonly cornerRadius?: number;
1014
+ /**
1015
+ * Which way the dimension across the tool's top runs: toward what it
1016
+ * touches, say. Any direction square to the axis when left out or along it.
1017
+ */
1018
+ readonly across?: Vec3;
1019
+ /**
1020
+ * The tool's own side, to turn about its axis in place of the end mill
1021
+ * `diameter` and `cornerRadius` make: radius out from the axis and height
1022
+ * above the tip, from the tip up — `@toolpath/tool-drawing`'s outline of a
1023
+ * drill, a chamfer mill, a tool in its holder. `diameter` and `height` still
1024
+ * place the rims and the dimension.
1025
+ */
1026
+ readonly profile?: readonly ToolProfilePoint[];
1027
+ /** Written on the dimension: `⌀ 3.200 mm`. */
1028
+ readonly label?: string;
1029
+ /** After the label, quieter: `bull nose R 0.5 · 3 places`. */
1030
+ readonly note?: string;
1031
+ }
1032
+ /** A point on a tool's side: radius out from its axis and height above its tip. */
1033
+ interface ToolProfilePoint {
1034
+ readonly r: number;
1035
+ readonly z: number;
1036
+ }
1037
+
1038
+ interface ToolMarksProps {
1039
+ /** The tools to draw, in the coordinates the part is drawn in. */
1040
+ marks: readonly ToolMark[];
1041
+ /** Draws nothing while false, and keeps its place in the scene. On by default. */
1042
+ visible?: boolean;
1043
+ /**
1044
+ * Added to every label's `className`, beside {@link MEASURE_LABEL_CLASS} —
1045
+ * a Tailwind utility list, or a CSS module's class.
1046
+ */
1047
+ labelClassName?: string;
1048
+ /** `measure` colours the rims and the dimension, as it does the measure tool's lines. */
1049
+ theme?: Partial<ViewerTheme>;
1050
+ }
1051
+ /**
1052
+ * Cutting tools drawn on the part, each to scale where it stands: a
1053
+ * see-through end mill — flat, bull nose or ball — or any tool turned from its
1054
+ * `profile`, rimmed at its bottom and top, with a dimension across its top and
1055
+ * a label on it. Mount it inside
1056
+ * `<Viewer>`, beside or inside the part.
1057
+ *
1058
+ * What a tool stands for is the caller's. A DFM panel stands the widest tool
1059
+ * a pocket admits in its tightest place; a plan could show the tool an
1060
+ * operation uses. Nothing here reads a datasheet.
1061
+ *
1062
+ * Drawn as the measure tool draws: the lines show through the part, in the
1063
+ * theme's `measure` colour, and the labels are unstyled DOM elements with the
1064
+ * {@link MEASURE_LABEL_CLASS} class and `data-measure-label="tool"`, the
1065
+ * label's text in a `data-measure-value` span and its note in a
1066
+ * `data-measure-note` one. The marks take no pointer events and are left out
1067
+ * of framing, so fitting the view fits the part.
1068
+ */
1069
+ declare const ToolMarks: ({ marks, visible, labelClassName, theme }: ToolMarksProps) => react.JSX.Element;
1070
+
950
1071
  /**
951
1072
  * The cut a viewer holds for itself, when nobody else is holding it.
952
1073
  *
@@ -1854,4 +1975,4 @@ declare const ORBIT_TARGET_FADE_MS = 1000;
1854
1975
  */
1855
1976
  declare function orbitTargetOpacity(sinceHold: number, fade?: number): number;
1856
1977
 
1857
- export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, BANANA_GAP, BANANA_MODEL_URL, Banana, type BananaProps, type BoxStockFigure, type BoxStockInput, type BoxStockPreview, CAD_CAMERA_UP, CHAMFER, CONTROL_SCHEME_OPTIONS, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type ControlSchemeOption, type CubeZone, type CylinderStockFigure, type CylinderStockInput, type CylinderStockPreview, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, HoverCard, type HoverCardProps, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, type NamedDirection, ORBIT_TARGET_COLOR, ORBIT_TARGET_FADE_MS, ORBIT_TARGET_FLASH_MS, ORBIT_TARGET_PIXELS, ORBIT_TARGET_RING_COLOR, ORBIT_TARGET_RING_OPACITY, ORBIT_TARGET_RING_PIXELS, ORBIT_TARGET_RING_WIDTH, PERSPECTIVE_FOV, POINTS_PER_MEASUREMENT, PartModel, PartModelFeature, PartModelRegion, PartPick, PartReportFormatError, type Projection, type RankingContext, RegionHighlight, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, type SectionSample, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, Stock, type StockAllowance, type StockAppearanceProps, type StockPosition, type StockProps, type StockSource, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, type ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, ViewerToolbar, type ViewerToolbarControl, type ViewerToolbarControls, type ViewerToolbarControlsProps, type ViewerToolbarProps, ViewerToolbarProvider, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bananaFrameBounds, bananaPosition, bestOwner, boundsFromBox, boxStockBounds, buildRegionIndex, cadViewDirections, cameraLimits, capHit, clipSegments, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionHighlights, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTypeRank, fitDistance, fixedBoxStockBounds, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, pointInContour, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, sampleSection, screenLength, screenPoint, sectionContour, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useSectionStore, useTapGuard, useViewerControls, useViewerToolbar, viewKind, viewUp, viewVector, visualSurfaces };
1978
+ export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, BANANA_GAP, BANANA_MODEL_URL, Banana, type BananaProps, type BoxStockFigure, type BoxStockInput, type BoxStockPreview, CAD_CAMERA_UP, CHAMFER, CONTROL_SCHEME_OPTIONS, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type ControlSchemeOption, type CubeZone, type CylinderStockFigure, type CylinderStockInput, type CylinderStockPreview, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, type FeatureTriangles, FeatureType, Grid, type GridProps, type GridSpec, HoverCard, type HoverCardProps, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, type NamedDirection, ORBIT_TARGET_COLOR, ORBIT_TARGET_FADE_MS, ORBIT_TARGET_FLASH_MS, ORBIT_TARGET_PIXELS, ORBIT_TARGET_RING_COLOR, ORBIT_TARGET_RING_OPACITY, ORBIT_TARGET_RING_PIXELS, ORBIT_TARGET_RING_WIDTH, PERSPECTIVE_FOV, POINTS_PER_MEASUREMENT, type PartContextValue, PartModel, PartModelFeature, PartModelRegion, PartPick, PartReportFormatError, type Projection, type RankingContext, RegionHighlight, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, type SectionSample, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, Stock, type StockAllowance, type StockAppearanceProps, type StockPosition, type StockProps, type StockSource, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, type ToolMark, ToolMarks, type ToolMarksProps, type ToolProfilePoint, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, type ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, ViewerToolbar, type ViewerToolbarControl, type ViewerToolbarControls, type ViewerToolbarControlsProps, type ViewerToolbarProps, ViewerToolbarProvider, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bananaFrameBounds, bananaPosition, bestOwner, boundsFromBox, boxStockBounds, buildRegionIndex, cadViewDirections, cameraLimits, capHit, clipSegments, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionHighlights, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTriangles, featureTypeRank, fitDistance, fixedBoxStockBounds, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, pointInContour, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, sampleSection, screenLength, screenPoint, sectionContour, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, usePartContext, useRetarget, useSectionStore, useTapGuard, useViewerControls, useViewerToolbar, viewKind, viewUp, viewVector, visualSurfaces };
package/dist/index.js CHANGED
@@ -152,6 +152,7 @@ import {
152
152
  trackDoubleTaps,
153
153
  trackTaps,
154
154
  useContentBox,
155
+ usePartContext,
155
156
  useRetarget,
156
157
  useSectionStore,
157
158
  useTapGuard,
@@ -161,7 +162,50 @@ import {
161
162
  viewUp,
162
163
  viewVector,
163
164
  visualSurfaces
164
- } from "./chunk-4TBLMR4Z.js";
165
+ } from "./chunk-WTTOUG4P.js";
166
+
167
+ // src/render/feature-triangles.ts
168
+ var EMPTY = { positions: new Float32Array(), normals: new Float32Array() };
169
+ function featureTriangles(model, geometry, tag) {
170
+ const position = geometry.getAttribute("position");
171
+ if (!position) return EMPTY;
172
+ const normal = geometry.getAttribute("normal");
173
+ const index = geometry.getIndex();
174
+ const vertex = (corner) => index ? index.getX(corner) : corner;
175
+ const triangleCount = Math.floor((index ? index.count : position.count) / 3);
176
+ const positions = [];
177
+ const normals = [];
178
+ for (const region of new Set(model.regionIndex.regionsForFeature(tag))) {
179
+ const range = model.regionIndex.rangeForRegion(region);
180
+ if (!range) continue;
181
+ for (let triangle = range.start; triangle < Math.min(range.end, triangleCount); triangle++) {
182
+ const corners = [vertex(triangle * 3), vertex(triangle * 3 + 1), vertex(triangle * 3 + 2)];
183
+ const at = corners.map((corner) => [
184
+ position.getX(corner),
185
+ position.getY(corner),
186
+ position.getZ(corner)
187
+ ]);
188
+ const out = normal ? corners.reduce(
189
+ (sum, corner) => [
190
+ sum[0] + normal.getX(corner),
191
+ sum[1] + normal.getY(corner),
192
+ sum[2] + normal.getZ(corner)
193
+ ],
194
+ [0, 0, 0]
195
+ ) : winding(at);
196
+ const length = Math.hypot(out[0], out[1], out[2]);
197
+ if (length === 0) continue;
198
+ positions.push(...at[0], ...at[1], ...at[2]);
199
+ normals.push(out[0] / length, out[1] / length, out[2] / length);
200
+ }
201
+ }
202
+ return { positions: new Float32Array(positions), normals: new Float32Array(normals) };
203
+ }
204
+ var winding = ([a, b, c]) => {
205
+ const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
206
+ const v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
207
+ return [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
208
+ };
165
209
 
166
210
  // src/hover-card.tsx
167
211
  import { useEffect, useState } from "react";
@@ -222,6 +266,7 @@ var StockIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */
222
266
  var AxesIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M5 19V3m0 16h16M5 19l10-10M2 6l3-3 3 3m10 10 3 3-3 3M11 9h4v4" }) });
223
267
  var GridIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 8l9-5 9 5-9 5zM3 8v8l9 5 9-5V8M12 13v8M7.5 5.5v8M16.5 5.5v8" }) });
224
268
  var BananaIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { viewBox: "0 0 576 512", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M284.2 245.6c12.99 6.929 25.35 15.14 36.08 25.8L334.5 285.6l65.75-23.47c14.75-5.265 30.18-7.849 45.81-8.389c1.154-10.73 1.764-21.38 1.764-31.87c0-118.5-81.33-221.9-119.7-221.9c-21.01 0-40.91 17.04-40.91 39.25c0 16.18 16.74 41.9 16.74 103C303.1 170.1 300.6 203.1 284.2 245.6zM575.1 389.6c0-3.687-.8637-7.429-2.687-10.93l-15.12-29.11c-21.05-40.53-63.08-64.51-106.9-64.51c-13.43 0-27.02 2.252-40.22 6.969l-84.84 30.27l-28.59-28.41C274.4 270.9 243.7 258.1 212.8 258.1c-23.72 0-47.57 6.97-68.29 21.2L106.3 306.4c-6.732 4.631-10.35 12.07-10.35 19.63c0 14.93 12.7 23.87 24.04 23.87c4.695 0 9.443-1.376 13.61-4.26l38.13-26.23c12.43-8.525 26.71-12.69 40.91-12.69c10.64 0 21.24 2.339 30.97 6.934c-50.62 62.23-128.3 99.85-211.4 99.85C14.42 413.5 0 427.8 0 445.5v31.38c0 17.68 14.66 32.02 32.46 32.02l28.98.0009c14.15 0 34.69 1.098 59.07 1.098c93.51 0 243.4-16.15 304.8-172.4c9.021-3.22 17.95-4.712 26.53-4.712c27.65 0 51.7 15.49 63.7 38.55l15.12 29.11c3.484 6.723 11.74 12.98 21.41 12.98C564.1 413.6 575.1 403.8 575.1 389.6z" }) });
269
+ var ToolsIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M9 2h6v7H9zM9.5 9v9.5l2.5 3.5 2.5-3.5V9M9.5 12l5 2.5M9.5 15.5l5 2.5" }) });
225
270
  var DirectionsIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M7 3v18m-4-4 4 4 4-4M17 21V3m-4 4 4-4 4 4" }) });
226
271
  var HoverIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v12H9l-5 4zm4 5h8m-8 3h5" }) });
227
272
  var FocusIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v16H4zM8 8h8v8H8zM2 12h4m12 0h4M12 2v4m0 12v4" }) });
@@ -236,6 +281,7 @@ var ICONS = {
236
281
  axes: AxesIcon,
237
282
  grid: GridIcon,
238
283
  banana: BananaIcon,
284
+ tools: ToolsIcon,
239
285
  directions: DirectionsIcon,
240
286
  hover: HoverIcon,
241
287
  focus: FocusIcon,
@@ -315,6 +361,14 @@ var BananaButton = () => /* @__PURE__ */ jsx3(
315
361
  label: (pressed) => pressed ? "Banana for scale (on)" : "Banana for scale"
316
362
  }
317
363
  );
364
+ var ToolsButton = () => /* @__PURE__ */ jsx3(
365
+ ToolbarButton,
366
+ {
367
+ action: "tools",
368
+ icon: "tools",
369
+ label: (pressed) => pressed ? "Hide tools on the part" : "Show tools on the part"
370
+ }
371
+ );
318
372
  var DirectionsButton = () => /* @__PURE__ */ jsx3(
319
373
  ToolbarButton,
320
374
  {
@@ -376,7 +430,8 @@ var DefaultControls = () => {
376
430
  controls.stock ? /* @__PURE__ */ jsx3(StockButton, {}, "stock") : null,
377
431
  controls.axes ? /* @__PURE__ */ jsx3(AxesButton, {}, "axes") : null,
378
432
  controls.grid ? /* @__PURE__ */ jsx3(GridButton, {}, "grid") : null,
379
- controls.banana ? /* @__PURE__ */ jsx3(BananaButton, {}, "banana") : null
433
+ controls.banana ? /* @__PURE__ */ jsx3(BananaButton, {}, "banana") : null,
434
+ controls.tools ? /* @__PURE__ */ jsx3(ToolsButton, {}, "tools") : null
380
435
  ].filter(Boolean);
381
436
  const analysis = [
382
437
  controls.directions ? /* @__PURE__ */ jsx3(DirectionsButton, {}, "directions") : null,
@@ -417,6 +472,7 @@ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
417
472
  AxesButton,
418
473
  GridButton,
419
474
  BananaButton,
475
+ ToolsButton,
420
476
  DirectionsButton,
421
477
  HoverButton,
422
478
  FocusButton,
@@ -2168,6 +2224,145 @@ function className(extra) {
2168
2224
  return extra ? `${MEASURE_LABEL_CLASS} ${extra}` : MEASURE_LABEL_CLASS;
2169
2225
  }
2170
2226
 
2227
+ // src/tool-marks.tsx
2228
+ import { Html as Html2, Line as Line2 } from "@react-three/drei";
2229
+ import { useMemo as useMemo7 } from "react";
2230
+ import { DoubleSide as DoubleSide2 } from "three";
2231
+
2232
+ // src/render/tool-marks.ts
2233
+ import { Quaternion as Quaternion4, Vector2 as Vector23, Vector3 as Vector38 } from "three";
2234
+ var TOOL_CORNER_STEPS = 12;
2235
+ var TOOL_ROUND = 64;
2236
+ function toolProfile(radius, corner, height) {
2237
+ const rounded = Math.min(Math.max(corner, 0), radius);
2238
+ const flat = radius - rounded;
2239
+ const points = [new Vector23(0, 0)];
2240
+ if (rounded <= 0) {
2241
+ points.push(new Vector23(radius, 0));
2242
+ } else {
2243
+ for (let step = 0; step <= TOOL_CORNER_STEPS; step++) {
2244
+ const angle = step / TOOL_CORNER_STEPS * (Math.PI / 2);
2245
+ points.push(
2246
+ new Vector23(flat + Math.sin(angle) * rounded, rounded - Math.cos(angle) * rounded)
2247
+ );
2248
+ }
2249
+ }
2250
+ points.push(new Vector23(radius, Math.max(height, rounded)));
2251
+ return points;
2252
+ }
2253
+ var UP = new Vector38(0, 1, 0);
2254
+ var AXES = [new Vector38(1, 0, 0), new Vector38(0, 1, 0), new Vector38(0, 0, 1)];
2255
+ var toVector = ({ x, y, z }) => new Vector38(x, y, z);
2256
+ var squareTo = (w, along) => {
2257
+ const wanted = along ? toVector(along) : null;
2258
+ const flat = wanted?.clone().addScaledVector(w, -wanted.dot(w));
2259
+ if (flat && flat.lengthSq() > 1e-12) return flat.normalize();
2260
+ const least = AXES.reduce(
2261
+ (best, axis) => Math.abs(axis.dot(w)) < Math.abs(best.dot(w)) ? axis : best
2262
+ );
2263
+ return least.clone().addScaledVector(w, -least.dot(w)).normalize();
2264
+ };
2265
+ var circle = (centre, u, v, radius) => Array.from({ length: TOOL_ROUND + 1 }, (_, step) => {
2266
+ const angle = step / TOOL_ROUND * Math.PI * 2;
2267
+ return centre.clone().addScaledVector(u, Math.cos(angle) * radius).addScaledVector(v, Math.sin(angle) * radius);
2268
+ });
2269
+ var tickLength = (diameter) => Math.min(Math.max(diameter * 0.15, 0.3), 3);
2270
+ function toolMarkShape(mark) {
2271
+ const axis = toVector(mark.axis);
2272
+ if (!(mark.diameter > 0) || axis.lengthSq() === 0) return null;
2273
+ const w = axis.normalize();
2274
+ const u = squareTo(w, mark.across);
2275
+ const v = new Vector38().crossVectors(w, u);
2276
+ const radius = mark.diameter / 2;
2277
+ const corner = Math.min(Math.max(mark.cornerRadius ?? 0, 0), radius);
2278
+ const height = Math.max(mark.height, corner);
2279
+ const base = toVector(mark.base);
2280
+ const top = base.clone().addScaledVector(w, height);
2281
+ const dimension = [
2282
+ top.clone().addScaledVector(u, radius),
2283
+ top.clone().addScaledVector(u, -radius)
2284
+ ];
2285
+ const half = tickLength(mark.diameter) / 2;
2286
+ const tick = (end) => [
2287
+ end.clone().addScaledVector(w, -half),
2288
+ end.clone().addScaledVector(w, half)
2289
+ ];
2290
+ return {
2291
+ base,
2292
+ turn: new Quaternion4().setFromUnitVectors(UP, w),
2293
+ profile: mark.profile ? mark.profile.map(({ r, z }) => new Vector23(r, z)) : toolProfile(radius, corner, height),
2294
+ rims: [
2295
+ ...radius - corner > 1e-9 ? [circle(base, u, v, radius - corner)] : [],
2296
+ circle(top, u, v, radius)
2297
+ ],
2298
+ dimension,
2299
+ ticks: [tick(dimension[0]), tick(dimension[1])],
2300
+ labelAt: top
2301
+ };
2302
+ }
2303
+
2304
+ // src/tool-marks.tsx
2305
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2306
+ var BODY = { color: 16317180, opacity: 0.32 };
2307
+ var NO_POINTER2 = { pointerEvents: "none" };
2308
+ var ignoreRay = () => void 0;
2309
+ var ToolMarks = ({ marks, visible = true, labelClassName, theme }) => {
2310
+ const color = resolveTheme(theme).measure;
2311
+ const shapes = useMemo7(
2312
+ () => marks.flatMap((mark) => {
2313
+ const shape = toolMarkShape(mark);
2314
+ return shape ? [{ mark, shape }] : [];
2315
+ }),
2316
+ [marks]
2317
+ );
2318
+ const label = labelClassName ? `${MEASURE_LABEL_CLASS} ${labelClassName}` : MEASURE_LABEL_CLASS;
2319
+ return /* @__PURE__ */ jsx10("group", { visible, userData: { [EXCLUDE_FROM_FRAME]: true }, children: shapes.map(({ mark, shape }, index) => /* @__PURE__ */ jsxs6("group", { children: [
2320
+ /* @__PURE__ */ jsx10(ToolBody, { shape }),
2321
+ [...shape.rims, shape.dimension, ...shape.ticks].map((points, line) => /* @__PURE__ */ jsx10(
2322
+ Line2,
2323
+ {
2324
+ points,
2325
+ color,
2326
+ lineWidth: MEASURE_LINE_PIXELS.line,
2327
+ depthTest: false,
2328
+ depthWrite: false,
2329
+ renderOrder: MEASURE_RENDER_ORDER,
2330
+ raycast: ignoreRay
2331
+ },
2332
+ line
2333
+ )),
2334
+ mark.label && visible ? /* @__PURE__ */ jsx10(Html2, { position: shape.labelAt, center: true, zIndexRange: [0, 0], style: NO_POINTER2, children: /* @__PURE__ */ jsxs6("div", { className: label, "data-measure-label": "tool", children: [
2335
+ /* @__PURE__ */ jsx10("span", { "data-measure-value": true, children: mark.label }),
2336
+ mark.note ? /* @__PURE__ */ jsxs6("span", { "data-measure-note": true, children: [
2337
+ " ",
2338
+ mark.note
2339
+ ] }) : null
2340
+ ] }) }) : null
2341
+ ] }, index)) });
2342
+ };
2343
+ var ToolBody = ({ shape }) => /* @__PURE__ */ jsxs6(
2344
+ "mesh",
2345
+ {
2346
+ position: shape.base,
2347
+ quaternion: shape.turn,
2348
+ raycast: ignoreRay,
2349
+ renderOrder: MEASURE_RENDER_ORDER - 1,
2350
+ children: [
2351
+ /* @__PURE__ */ jsx10("latheGeometry", { args: [shape.profile, 64] }),
2352
+ /* @__PURE__ */ jsx10(
2353
+ "meshBasicMaterial",
2354
+ {
2355
+ color: BODY.color,
2356
+ transparent: true,
2357
+ opacity: BODY.opacity,
2358
+ depthWrite: false,
2359
+ side: DoubleSide2
2360
+ }
2361
+ )
2362
+ ]
2363
+ }
2364
+ );
2365
+
2171
2366
  // src/model/normals.ts
2172
2367
  function regionNormals(geometry, regions) {
2173
2368
  const position = geometry.getAttribute("position");
@@ -2267,6 +2462,7 @@ export {
2267
2462
  SectionView,
2268
2463
  Stock,
2269
2464
  TAP_SLOP,
2465
+ ToolMarks,
2270
2466
  UnsupportedKernelVersionError,
2271
2467
  VIEW_NAMES,
2272
2468
  VIEW_SIGNS,
@@ -2318,6 +2514,7 @@ export {
2318
2514
  distanceBetween,
2319
2515
  dragPlane,
2320
2516
  excludedFromFrame,
2517
+ featureTriangles,
2321
2518
  featureTypeRank,
2322
2519
  fitDistance,
2323
2520
  fixedBoxStockBounds,
@@ -2377,6 +2574,7 @@ export {
2377
2574
  trackDoubleTaps,
2378
2575
  trackTaps,
2379
2576
  useContentBox,
2577
+ usePartContext,
2380
2578
  useRetarget,
2381
2579
  useSectionStore,
2382
2580
  useTapGuard,
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
  import { Group, Mesh, LineSegments, Box3, Plane, BufferGeometry, Vector3, Raycaster, Object3D, Intersection, Camera } from 'three';
3
4
 
4
5
  /**
@@ -851,6 +852,11 @@ interface PartMeshProps {
851
852
  showEdges?: boolean;
852
853
  /** Wireframe shows region boundaries and any hovered/painted faces, with no triangle diagonals. */
853
854
  display?: PartDisplay;
855
+ /**
856
+ * Objects drawn on the part, such as `<ToolMarks>`. They can read its model
857
+ * and mesh with `usePartContext`.
858
+ */
859
+ children?: ReactNode;
854
860
  }
855
861
  /**
856
862
  * The part, as one mesh with one material.
@@ -862,7 +868,7 @@ interface PartMeshProps {
862
868
  * React render at all: the hovered face lives in a ref, and routing it through
863
869
  * state is what this rewrite exists to stop doing.
864
870
  */
865
- declare const PartMesh: ({ model, geometry, selection, focus, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, hover, activeDirection, section, onSectionChange, onAdjacency, focusFeature, onHover, onPick, theme, showEdges, display, }: PartMeshProps) => react.JSX.Element;
871
+ declare const PartMesh: ({ model, geometry, selection, focus, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, hover, activeDirection, section, onSectionChange, onAdjacency, focusFeature, onHover, onPick, theme, showEdges, display, children, }: PartMeshProps) => react.JSX.Element;
866
872
 
867
873
  interface EnginePartProps extends Omit<PartMeshProps, 'model' | 'geometry'> {
868
874
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/viewer",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "React Three Fiber viewer for Toolpath Engine part reports",
5
5
  "license": "MIT",
6
6
  "engines": {