@toolpath/viewer 1.4.0 → 2.0.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
@@ -76,6 +76,7 @@ What each wrapper is for:
76
76
  ├─ <EnginePart> validates the report, loads the mesh, then renders <PartMesh>
77
77
  │ └─ <PartMesh> draws the part and handles hover, click, colours, section cuts
78
78
  ├─ <DirectionArrows> arrows for the directions the part can be machined from
79
+ ├─ <Stock> translucent geometry, GLB, box or cylinder stock, included in Fit
79
80
  ├─ <SectionTool> optional: click a face or a plane to cut the part open
80
81
  ├─ <MeasureTool> optional: click two points for a distance, three for an angle
81
82
  ├─ <Grid> <Axes> reference geometry, sized to the part
@@ -95,6 +96,16 @@ What each wrapper is for:
95
96
  4. **You own the state.** The viewer tells you what was clicked (`onPick`), and you tell it what to
96
97
  highlight (`selection`, `highlights`, …). It never changes your selection by itself.
97
98
 
99
+ ### Focus the current selection
100
+
101
+ Pass `focus` to make selected features solid while the remainder of the part becomes translucent.
102
+ The default outside opacity is 15%; set `opacity` for a different X-ray strength. With no selected
103
+ features, the part stays fully opaque.
104
+
105
+ ```tsx
106
+ <EnginePart report={report} selection={selection} focus={{ opacity: 0.12 }} />
107
+ ```
108
+
98
109
  Parts are in **millimetres** with **Z pointing up**. The camera uses the same convention.
99
110
 
100
111
  ### A click usually matches several features
@@ -103,23 +114,298 @@ The same face is usually owned by **5–8 features at once**, even on a plain cu
103
114
  `face` when cut from one direction, a `wall` when cut from another, and part of every `profile`
104
115
  around it. So a click gives you every match, and you decide which to use:
105
116
 
106
- | Field | What it contains |
107
- | ---------------- | ------------------------------------------------------------------- |
108
- | `pick.best` | The most likely feature, or `null` |
109
- | `pick.ranked` | Every matching feature, most likely first |
110
- | `pick.owners` | Every matching feature, in report order |
111
- | `pick.region` | The index of the face that was clicked |
112
- | `pick.point` | Where the click hit, as `[x, y, z]` |
113
- | `pick.normal` | The direction the clicked surface faces, as `[x, y, z]` |
114
- | `pick.modifiers` | `{ alt, ctrl, meta, shift, secondary }`: keys held, and right-click |
115
- | `pick.doubled` | `true` if this click was the second half of a double-click |
117
+ | Field | What it contains |
118
+ | ---------------- | --------------------------------------------------------------------- |
119
+ | `pick.best` | The most likely feature, or `null` |
120
+ | `pick.ranked` | Every matching feature, most likely first |
121
+ | `pick.owners` | Every matching feature, in report order |
122
+ | `pick.region` | The index of the face that was clicked |
123
+ | `pick.point` | Where the click hit, as `[x, y, z]` |
124
+ | `pick.normal` | The direction the clicked surface faces, as `[x, y, z]` |
125
+ | `pick.pointer` | Browser coordinates for an application-owned hover card, when present |
126
+ | `pick.modifiers` | `{ alt, ctrl, meta, shift, secondary }`: keys held, and right-click |
127
+ | `pick.doubled` | `true` if this click was the second half of a double-click |
116
128
 
117
129
  The ranking puts specific features first: holes, then pockets and bosses, then chamfers and
118
130
  fillets, then walls and faces, then profiles. Among features of the same kind, the one whose
119
131
  machining direction points most toward the camera wins.
120
132
 
133
+ `onHover` receives the same `PartPick` shape. Use the optional `pointer` location yourself, or
134
+ wrap application content in `<HoverCard pick={hoverPick}>`; it follows the cursor while that
135
+ region remains hovered. The viewer intentionally leaves card content and actions to the application.
136
+ Hover feedback is on by default; pass `hover={false}` to disable both face feedback and `onHover`
137
+ callbacks while preserving clicks. That makes an application toolbar's feature-hover toggle
138
+ unambiguous.
139
+
140
+ The normalized model contains feature identity, type, directions, face shape, and analytic area.
141
+ An Engine DFM card can join `pick.best` to the part's detailed feature data and render its own
142
+ depth, clearance-diameter, or L/D rows inside `HoverCard`; setup labels remain application or plan
143
+ context rather than a fact of the part surface.
144
+
121
145
  ## Components
122
146
 
147
+ ### Stock and display controls
148
+
149
+ Use `<Stock>` for a box, cylinder, prepared geometry or GLB bytes. Stock and part coordinates
150
+ must use the same millimetre, Z-up frame. Stock is included in Fit and Reset while
151
+ keeping section tools, measurements, the grid, and direction arrows sized to the finished part.
152
+ Stock does not intercept clicks or get clipped by the part's section plane.
153
+
154
+ ```tsx
155
+ import { useMemo, useRef, useState } from 'react'
156
+ import {
157
+ Axes,
158
+ Stock,
159
+ DirectionArrows,
160
+ PartMesh,
161
+ Viewer,
162
+ directionHighlights,
163
+ } from '@toolpath/viewer'
164
+ import type { PartModel, ViewerHandle } from '@toolpath/viewer'
165
+ import type { BufferGeometry } from 'three'
166
+
167
+ export function StockPreview({ model, geometry }: { model: PartModel; geometry: BufferGeometry }) {
168
+ const viewer = useRef<ViewerHandle>(null)
169
+ const [stock, setStock] = useState(false)
170
+ const [axes, setAxes] = useState(true)
171
+ const [directions, setDirections] = useState(false)
172
+ const [direction, setDirection] = useState<number | null>(null)
173
+ const [wireframe, setWireframe] = useState(false)
174
+ const colors = useMemo(
175
+ () => (directions ? directionHighlights(model, direction) : []),
176
+ [model, direction, directions],
177
+ )
178
+
179
+ return (
180
+ <>
181
+ <Viewer ref={viewer} style={{ height: 500 }}>
182
+ <PartMesh
183
+ model={model}
184
+ geometry={geometry}
185
+ display={wireframe ? 'wireframe' : 'solid'}
186
+ regionHighlights={colors}
187
+ activeDirection={directions ? direction : null}
188
+ />
189
+ {stock && <Stock box={{ partGeometry: geometry, allowance: { wall: 3, floor: 3 } }} />}
190
+ {axes && <Axes />}
191
+ <DirectionArrows
192
+ directions={model.candidateDirections}
193
+ visible={directions}
194
+ shownDirection={direction}
195
+ onPickDirection={(index) => setDirection((held) => (held === index ? null : index))}
196
+ />
197
+ </Viewer>
198
+ <button aria-pressed={stock} onClick={() => setStock(!stock)}>
199
+ Stock
200
+ </button>
201
+ <button aria-pressed={axes} onClick={() => setAxes(!axes)}>
202
+ Axes
203
+ </button>
204
+ <button aria-pressed={directions} onClick={() => setDirections(!directions)}>
205
+ By direction
206
+ </button>
207
+ <button aria-pressed={wireframe} onClick={() => setWireframe(!wireframe)}>
208
+ Wireframe
209
+ </button>
210
+ <button onClick={() => viewer.current?.fit()}>Fit</button>
211
+ </>
212
+ )
213
+ }
214
+ ```
215
+
216
+ For a box preview, pass `box={{ partGeometry, allowance, offset }}`. Allowance is padding
217
+ **per side**, in millimetres. The preferred form is
218
+ `{ wall, floor }`: wall stock expands X/Y and floor stock expands Z, matching the wall/floor
219
+ roughing-stock distinction used by machining settings. A number or `{ x, y, z }` remains accepted
220
+ for uniform or axis-specific padding. It defaults to zero. `offset` translates the stock from the
221
+ part's bounding-box centre. `boxStockBounds(geometry, allowance, offset)` returns the same `Box3`
222
+ for displaying dimensions.
223
+
224
+ For fixed-box previews, pass `box={{ partGeometry, dimensions, position, positionOffset, offset }}`
225
+ instead, where `dimensions` is `{ x, y, z }`. Dimensions and allowance are alternative sizing
226
+ modes, not simultaneous options. `position` accepts
227
+ `model_centered`, `offset_from_top`, or `offset_from_bottom`, and `positionOffset` is the distance
228
+ from the selected top or bottom bound. All fixed-box values use millimetres and the part's Z-up
229
+ coordinate frame.
230
+ Negative/non-finite allowances, non-finite coordinates, and empty or non-positive stock dimensions
231
+ throw `RangeError`. The 3 mm allowance above is an example, not an automatic stock recommendation.
232
+
233
+ `<Stock cylinder={{ origin, axis, diameter, length }} />` draws round stock along any axis:
234
+ `origin` is the centre of its base and `axis` points from the base toward its far end, in part
235
+ coordinates. For a preview, pass
236
+ `cylinder={{ partGeometry, diameter, length, position, positionOffset }}` instead. It stands along
237
+ the part's Z, is centred on the part's bounding box, and is placed along Z by the same `position`
238
+ rule as fixed-box stock. The kernel centres fixed round stock on the part's
239
+ smallest enclosing circle about the first setup's cutting direction, so render the resolved stock
240
+ once a job is planned.
241
+
242
+ `<Stock box={{ frame, lower, upper }} />` draws a block whose corners are given in a frame
243
+ of `location`, `axis` and `refDirection`: a point `(x, y, z)` sits at
244
+ `location + x·refDirection + y·(axis × refDirection) + z·axis`, with `axis` normalised and
245
+ `refDirection` projected square to it and normalised. That is the shape of an Engine plan's
246
+ `stock.resolved` box, which is squared to the setup rather than to the part. Omit `frame` when
247
+ `lower` and `upper` are already in part coordinates. Resolved placement and `partGeometry`
248
+ preview options are mutually exclusive for both shapes.
249
+
250
+ Box and cylinder sources rebuild their mesh only when their figures/options change (or the
251
+ preview's `partGeometry` reference changes). Inline option objects do not trigger repeated part
252
+ scans on ordinary renders. Figures that cannot make a
253
+ solid — a zero axis, a non-positive size, a frame whose axis and reference direction are parallel —
254
+ throw `RangeError`.
255
+
256
+ `Stock` accepts exactly one of `geometry`, `glb`, `box` or `cylinder`:
257
+
258
+ ```tsx
259
+ <Stock geometry={geometry} showEdges={false} />
260
+ // Or:
261
+ <Stock glb={glbBytes} showEdges={false} />
262
+ <Stock box={resolvedBox} />
263
+ <Stock cylinder={resolvedCylinder} />
264
+ <Stock box={{ partGeometry, dimensions, position, positionOffset }} />
265
+ <Stock cylinder={{ partGeometry, diameter, length, position, positionOffset }} />
266
+ ```
267
+
268
+ All four inputs are mutually exclusive in `StockProps`. With `geometry`, the caller owns its
269
+ lifetime and disposal. With `box` or `cylinder`, `Stock` owns the generated geometry. With `glb`,
270
+ `Stock` decodes the bytes internally and disposes decoded
271
+ geometry when the bytes change or the component unmounts. Keep the same `ArrayBuffer` reference
272
+ while displaying the same artifact to avoid decoding it again on ordinary renders.
273
+ It renders nothing while decoding and throws decoding failures to the nearest error boundary.
274
+ Changing the source immediately hides the previous stock; late results from an earlier decode
275
+ are discarded and cleaned up.
276
+
277
+ The component does not fetch, cache or store artifacts. The caller supplies the bytes.
278
+ GLB node transforms are applied without centering, rescaling or changing axes:
279
+ Engine stock artifacts are already in part coordinates, millimetres and Z-up. Indexed geometry
280
+ stays indexed; missing normals are computed without requiring a part report. The parser cleans up
281
+ its temporary scene after preparing the returned geometry.
282
+
283
+ Engine stock files are plain GLB served with HTTP gzip, which the browser handles automatically;
284
+ no meshopt decoder is needed. Authentication, durable artifact copies, action selection and
285
+ missing-artifact fallback and artifact caching belong to the application. The example's
286
+ **Stock source → GLB fixture** option exercises synthetic blanks encoded by the services stock
287
+ writer. **Shape preview** exposes the separate box/cylinder shape controls.
288
+
289
+ `Stock` accepts `color`, `emissive` (default black), `flatShading` (default `false`),
290
+ `opacity` (default `0.2`), `edgeColor`, `edgeOpacity`, and `showEdges`. Emissive shading
291
+ and flat shading change only the material, not the source's vertices, indices or normals.
292
+ `renderStyle="legacy-workpiece"` reproduces legacy Workpiece rendering: white with dark emissive
293
+ shading, per-face shading, 20% opacity, two shared-geometry surface passes and black 50%-opacity
294
+ edges drawn before surfaces (rather than over them). The default `overlay` style is unchanged.
295
+
296
+ For action-to-action playback, `retainPrevious` keeps the current decoded GLB visible until
297
+ the next GLB is ready, then swaps meshes without a blank frame. Key the stock's parent by
298
+ part/calculation identity (not action) to prevent retaining geometry across unrelated runs.
299
+ The default still hides the previous source during decoding. Geometry is disposed after a
300
+ replacement commits; cancelled/late results are discarded.
301
+ The outline is built the first time it is shown, so a large stock mesh with `showEdges={false}`
302
+ costs no edge pass. All source types accept the same appearance props. Caller-provided geometry is never disposed; the
303
+ components dispose their own materials and outlines. Conditionally mount stock to toggle it.
304
+ Toggling stock preserves the camera; Fit/Reset then frames the visible stock together with the part.
305
+
306
+ **Migration from 1.x:** `BoxStock` and `BoxStockProps` have been removed. Use
307
+ `<Stock box={{ partGeometry, allowance, offset }} />` for allowance-based previews, or
308
+ `<Stock box={{ partGeometry, dimensions, position, positionOffset, offset }} />` for fixed
309
+ dimensions. No preview-builder helper is required. `Stock geometry={geometry}` remains supported.
310
+ `StockProps` is now a union type alias, so `interface X extends StockProps` becomes
311
+ `type X = StockProps & { … }`; `StockAppearanceProps` names the shared appearance props alone.
312
+
313
+ `directionHighlights(model, activeDirection?)` returns region colors in the same palette as
314
+ `DirectionArrows`. For a face with several owners, the most specific feature wins, followed by
315
+ candidate-direction order and feature tag. Unmatched directions are left unpainted. Hover and
316
+ selection still paint over this wash. Scoping by direction filters ownership; it does not hide
317
+ the rest of the model or claim that an unpainted face cannot be manufactured.
318
+
319
+ Wireframe keeps the existing semantic edges, including rear edges, and keeps faces available for
320
+ picking, measuring, and section placement. Highlighted faces and section caps remain visible.
321
+ `showEdges` controls solid-mode outlines; wireframe always shows them. The example's bottom toolbar
322
+ makes wireframe and direction coloring mutually exclusive and includes a direction legend and
323
+ contextual Section/Measure controls.
324
+
325
+ ### `<ViewerToolbar>`
326
+
327
+ `ViewerToolbar` is a composable, viewer-scoped toolbar for camera, display, section, and measurement
328
+ controls. Import its stylesheet alongside your application stylesheet:
329
+
330
+ ```tsx
331
+ import '@toolpath/viewer/toolbar.css'
332
+ import { ViewerToolbar, ViewerToolbarProvider } from '@toolpath/viewer'
333
+ ```
334
+
335
+ Give `ViewerToolbarProvider` the current values and callbacks from the host app, then place the
336
+ toolbar and viewer inside it. The toolbar owns no application state or behavior: it only reads the
337
+ provider. Omit `children` for every configured control in the standard order, or compose the
338
+ individual buttons to use only the controls and order your application needs.
339
+
340
+ ```tsx
341
+ <ViewerToolbarProvider
342
+ controls={{
343
+ stock: { pressed: showStock, onClick: () => setShowStock((shown) => !shown) },
344
+ measure: { pressed: measuring, onClick: toggleMeasuring },
345
+ fit: { onClick: () => viewer.current?.fit() },
346
+ }}
347
+ >
348
+ <ViewerToolbar>
349
+ <ViewerToolbar.Controls>
350
+ <ViewerToolbar.MeasureButton />
351
+ <ViewerToolbar.Divider />
352
+ <ViewerToolbar.StockButton />
353
+ <ViewerToolbar.FitButton />
354
+ </ViewerToolbar.Controls>
355
+ </ViewerToolbar>
356
+ <Viewer ref={viewer}>…</Viewer>
357
+ </ViewerToolbarProvider>
358
+ ```
359
+
360
+ `ViewerToolbar.Controls`, `Divider`, and the thirteen `*Button` components are available on
361
+ `ViewerToolbar`: `StockButton`, `AxesButton`, `GridButton`, `BananaButton`, `DirectionsButton`,
362
+ `HoverButton`, `FocusButton`, `WireframeButton`, `SectionButton`, `MeasureButton`, `FitButton`,
363
+ `ResetButton`, and `TopButton`. A rendered button requires its matching provider control. For an
364
+ application-specific toolbar component, `useViewerToolbar()` reads the same controls from inside
365
+ the provider.
366
+
367
+ #### Styling hooks
368
+
369
+ Every viewer, hover-card, and toolbar DOM boundary has a stable class name. Your `className` on
370
+ `<Viewer>`, `<ViewerToolbar>`, or `<HoverCard>` is appended to the component's own class; it never
371
+ replaces the hook. The toolbar stylesheet uses the same names, so import it for the default look or
372
+ override any of these selectors in your application stylesheet.
373
+
374
+ | Component | Class | Element |
375
+ | ----------------- | ------------------------- | --------------------------------- |
376
+ | `<Viewer>` | `viewer-root` | Wrapper around the canvas |
377
+ | `<Viewer>` | `viewer-canvas-container` | R3F canvas event container |
378
+ | `<Viewer>` | `viewer-canvas-frame` | R3F's canvas-sizing frame |
379
+ | `<Viewer>` | `viewer-canvas` | The `<canvas>` itself |
380
+ | `<HoverCard>` | `viewer-hover-card` | Cursor-following tooltip shell |
381
+ | `<ViewerToolbar>` | `viewer-toolbar-stack` | Outer toolbar stack |
382
+ | `<ViewerToolbar>` | `viewer-toolbar` | Controls group |
383
+ | `<ViewerToolbar>` | `viewer-toolbar-button` | Each standard control button |
384
+ | `<ViewerToolbar>` | `viewer-toolbar-icon` | SVG within a standard control |
385
+ | `<ViewerToolbar>` | `viewer-toolbar-tooltip` | Label shown on button hover/focus |
386
+ | `<ViewerToolbar>` | `viewer-toolbar-divider` | Separator between control groups |
387
+
388
+ The following `data-*` attributes identify generated elements without depending on labels, which
389
+ can change with state or localization:
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 |
404
+
405
+ Toggle buttons also carry standard `aria-pressed="true"` or `"false"`; use that to style their
406
+ current state. `Fit`, `Reset`, and `Top view` are actions rather than toggles, so they omit the
407
+ attribute.
408
+
123
409
  ### `<Viewer>`
124
410
 
125
411
  The canvas that holds everything. Import it from `@toolpath/viewer`.
@@ -127,7 +413,7 @@ The canvas that holds everything. Import it from `@toolpath/viewer`.
127
413
  | Prop | Default | What it does |
128
414
  | ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
129
415
  | `projection` | `'orthographic'` | `'orthographic'` keeps parallel lines parallel. `'perspective'` gives depth, which helps with deep pockets. |
130
- | `controls` | `'toolpath'` | Mouse mapping. See [Mouse controls](#mouse-controls). |
416
+ | `controls` | `'toolpath'` | CAD navigation preset. See [CAD controls](#cad-controls). |
131
417
  | `zoomTo` | `'cursor'` | Zoom toward the pointer, or toward `'centre'` (often easier on a trackpad). |
132
418
  | `freeOrbit` | `true` | Let the view keep rotating past straight-up and straight-down. |
133
419
  | `retargetOnDoubleClick` | `true` | Double-click the part to rotate around that point. |
@@ -142,20 +428,35 @@ Changing `projection` rebuilds the canvas and returns the camera to its starting
142
428
 
143
429
  The canvas is transparent. To set a background colour, style the wrapper or its parent.
144
430
 
145
- #### Mouse controls
431
+ #### CAD controls
432
+
433
+ `controls` changes only the navigation gestures. It does not choose a projection: the viewer stays
434
+ orthographic by default, and `projection="perspective"` remains an independent opt-in.
435
+
436
+ | Value | Label | Rotate | Pan | Zoom |
437
+ | -------------- | ---------- | ------------------------------------- | ----------------------------------------- | ----------------------------------- |
438
+ | `'toolpath'` | Toolpath | Left-drag | Right-drag | Scroll wheel |
439
+ | `'fusion'` | Fusion | Shift + middle-drag or Shift + scroll | Middle-drag, scroll, or two-finger scroll | Trackpad pinch |
440
+ | `'alias'` | Alias | Left-drag | Middle-drag | Scroll wheel |
441
+ | `'inventor'` | Inventor | Shift + middle-drag or Shift + scroll | Middle-drag, scroll, or two-finger scroll | Trackpad pinch |
442
+ | `'solidworks'` | SolidWorks | Middle-drag | Ctrl + middle-drag | Shift + middle-drag or scroll wheel |
443
+ | `'tinkercad'` | Tinkercad | Right-drag | Shift + right-drag | Scroll wheel |
444
+ | `'powermill'` | PowerMill | Middle-drag | Shift + middle-drag | Scroll wheel |
445
+ | `'onshape'` | Onshape | Right-drag | Middle-drag | Scroll wheel |
446
+
447
+ Fusion and Inventor treat a two-finger scroll as pan, Shift + two-finger scroll as orbit, and a
448
+ pinch (a wheel event with Ctrl set by the browser) as zoom. Other schemes use one-finger rotate,
449
+ two-finger pinch-and-pan, and three-finger pan on touch screens.
146
450
 
147
- | Action | `controls="toolpath"` (default) | `controls="fusion"` (like Fusion 360) |
148
- | --------------------- | ------------------------------- | ------------------------------------------ |
149
- | Rotate | Left-drag | Shift + middle-drag, or Shift + scroll |
150
- | Pan | Right-drag or middle-drag | Middle-drag, or scroll / two-finger scroll |
151
- | Zoom | Scroll wheel | Trackpad pinch |
152
- | Rotate around a point | Double-click the part | Double-click the part |
153
- | Fit whole part | Double middle-click | Double middle-click |
451
+ All schemes support double-clicking the part to rotate around that point and double-clicking the
452
+ middle mouse button to fit the whole part. Dragging never selects anything; a click only counts if
453
+ the pointer barely moved.
154
454
 
155
- In `fusion` mode, left-drag doesn't move the camera, and the scroll wheel pans instead of zooming.
156
- On touch screens, one finger rotates and two fingers pinch and pan.
455
+ The package exports `CONTROL_SCHEME_OPTIONS`, `ControlScheme`, and `ControlSchemeOption` for a host
456
+ application's settings UI. It deliberately stores no preferences: pass the selected `controls`,
457
+ `freeOrbit`, and `zoomTo` values to `<Viewer>` from your own state or persistence layer.
157
458
 
158
- Dragging never selects anything. A click only counts if the pointer barely moved.
459
+ `'toolpath'`, `freeOrbit={true}`, and `zoomTo="cursor"` are the defaults.
159
460
 
160
461
  ### `<EnginePart>`
161
462
 
@@ -176,28 +477,30 @@ Draws the part and handles clicks. You only use it directly when you
176
477
 
177
478
  **Colours**
178
479
 
179
- | Prop | Type | What it does |
180
- | ------------------- | ---------------------- | -------------------------------------------------------------- |
181
- | `selection` | `string[]` | Feature tags to highlight as selected (orange). |
182
- | `highlights` | `FeatureHighlight[]` | Your own colour per feature, e.g. difficulty or setup. |
183
- | `regionHighlights` | `RegionHighlight[]` | Your own colour per face. |
184
- | `candidates` | `string[]` | Other possible matches, faintly tinted by machining direction. |
185
- | `pickedRegions` | `number[]` | Faces to mark as just clicked. |
186
- | `hoveredFeatureIds` | `string[]` | Features to show as hovered, e.g. when hovering a list row. |
187
- | `showEdges` | `boolean` (`true`) | Draw outlines between faces. |
188
- | `theme` | `Partial<ViewerTheme>` | Override the part's colours. |
480
+ | Prop | Type | What it does |
481
+ | ------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
482
+ | `selection` | `string[]` | Feature tags to highlight as selected (orange). |
483
+ | `highlights` | `FeatureHighlight[]` | Your own colour per feature, e.g. difficulty or setup. |
484
+ | `regionHighlights` | `RegionHighlight[]` | Your own colour per face. |
485
+ | `candidates` | `string[]` | Other possible matches, faintly tinted by machining direction. |
486
+ | `pickedRegions` | `number[]` | Faces to mark as just clicked. |
487
+ | `hoveredFeatureIds` | `string[]` | Features to show as hovered, e.g. when hovering a list row. |
488
+ | `showEdges` | `boolean` (`true`) | Draw outlines between faces. |
489
+ | `display` | `'solid' \| 'wireframe'` (`'solid'`) | Wireframe draws face boundaries without triangle diagonals. Painted and hovered faces remain visible. |
490
+ | `theme` | `Partial<ViewerTheme>` | Override the part's colours. |
189
491
 
190
492
  **Interaction**
191
493
 
192
- | Prop | Type | What it does |
193
- | ----------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
194
- | `onPick` | `(pick: PartPick) => void` | Left- or right-click on the part. |
195
- | `onHover` | `(pick: PartPick \| null) => void` | The pointer moved onto a different face, or off the part (`null`). |
196
- | `activeDirection` | `number \| null` | Only match features machined from this direction (an index into `candidateDirections`). |
197
- | `focusFeature` | `string \| null` | Zoom to this feature. The camera moves each time the value changes. |
198
- | `section` | `SectionOptions` | Cut the part open. Omit it to follow the viewer's own cut. See [Section view](#section-view). |
199
- | `onSectionChange` | `(state: SectionState) => void` | Called when the cut moves or goes away. With `section`, passing it also shows the drag handle. |
200
- | `onAdjacency` | `(map) => void` | Called once per mesh with which faces touch which. |
494
+ | Prop | Type | What it does |
495
+ | ----------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
496
+ | `onPick` | `(pick: PartPick) => void` | Left- or right-click on the part. |
497
+ | `onHover` | `(pick: PartPick \| null) => void` | The pointer moved onto a different face, or off the part (`null`). |
498
+ | `hover` | `boolean` (`true`) | Paint and report faces under the pointer. `false` leaves click picking on and clears any hover. |
499
+ | `activeDirection` | `number \| null` | Only match features machined from this direction (an index into `candidateDirections`). |
500
+ | `focusFeature` | `string \| null` | Zoom to this feature. The camera moves each time the value changes. |
501
+ | `section` | `SectionOptions` | Cut the part open. Omit it to follow the viewer's own cut. See [Section view](#section-view). |
502
+ | `onSectionChange` | `(state: SectionState) => void` | Called when the cut moves or goes away. With `section`, passing it also shows the drag handle. |
503
+ | `onAdjacency` | `(map) => void` | Called once per mesh with which faces touch which. |
201
504
 
202
505
  Hovering over the part is handled for you. You only need `onHover` if you want to show the hovered
203
506
  feature elsewhere in your UI.
@@ -478,7 +781,10 @@ no `section` prop:
478
781
 
479
782
  Hovering the part previews a cut through the face under the pointer; clicking places it. Three
480
783
  coloured planes stand behind the part — click one to cut along that axis, in from your side. Once
481
- there is a cut, an arrow drags it, an outlined sheet shows the cutting plane, and Escape clears it.
784
+ there is a cut, a framed plane and two-way normal-axis handle show exactly what will move; drag the
785
+ handle to move it. Keep the canvas clear by presenting `sectionMeasurement(state)` beside the host
786
+ application's section slider; it reports the depth from a picked surface, or the distance swept
787
+ through the part bounds. Escape clears it.
482
788
  The cut belongs to the viewer: `viewer.current.setSection(null)` clears it from a button outside
483
789
  the canvas, `setSection({ enabled: true, normal, offset })` sets one, and `onSectionChange` on
484
790
  `PartMesh` still reports every move.
@@ -505,7 +811,7 @@ const [offset, setOffset] = useState(0.5)
505
811
  ```
506
812
 
507
813
  The cut surface is filled in with a hatch and outlined, so the part doesn't look hollow and the cut
508
- reads as a cut. Passing `onSectionChange` also shows an arrow handle users can drag. The handler is
814
+ reads as a cut. Passing `onSectionChange` also shows the draggable plane-frame gizmo. The handler is
509
815
  called on every drag and only when the cut actually changes, so it's safe to store the value in
510
816
  state; a cut going away is reported once, with `enabled: false`.
511
817
 
@@ -625,10 +931,11 @@ go on the cube. See `DEFAULT_THEME` for every key. The default part colours are
625
931
  default lighting, so if you change one, check the other.
626
932
 
627
933
  The section cut is themed on the part too: `sectionCap` and `sectionHatch` are the cap's fill and
628
- hatch lines, `sectionHandle` the drag arrow, and `sectionOutline` the cutting-plane sheet and
629
- preview that `<SectionTool>` draws. `measure` and `measureSnap` are `<MeasureTool>`'s lines and its
630
- snap indicator; a distance's X, Y, Z legs take `AXIS_COLORS`, the same hues as the section tool's
631
- global planes.
934
+ hatch lines, `sectionHandle` overrides the drag arrow's colour, and `sectionOutline` the
935
+ cutting-plane sheet and preview that `<SectionTool>` draws. Without a `sectionHandle` override, the
936
+ drag arrow uses the cutting plane's direction colour. `measure` and `measureSnap` are
937
+ `<MeasureTool>`'s lines and its snap indicator; a distance's X, Y, Z legs take `AXIS_COLORS`, the
938
+ same hues as the section tool's global planes.
632
939
 
633
940
  `HIGHLIGHT_COLORS` has the standard selection colours (`default`, `toolIssue`, `geometryIssue`).
634
941
  `DIRECTION_COLORS` has the 9 direction colours, which repeat for parts with more than 9 directions.
@@ -872,12 +1179,13 @@ is a complete app built this way, with no API key needed.
872
1179
 
873
1180
  ### Hooks
874
1181
 
875
- | Hook | Use inside `<Viewer>` to… |
876
- | --------------------- | -------------------------------------------------------------------------------- |
877
- | `useViewerControls()` | Get `fit`, `reset`, `setView`, `setViewDirection`, `frameBox`, and `setSection`. |
878
- | `useSectionStore()` | Read, set, or subscribe to the viewer's own cut. |
879
- | `useContentBox()` | Get the part's bounding box (a `THREE.Box3`, empty until loaded). |
880
- | `useTapGuard()` | Check whether a pointer event was a click and not a drag. |
1182
+ | Hook | Where | What it does |
1183
+ | --------------------- | -------------------------------- | -------------------------------------------------------------------------------- |
1184
+ | `useViewerControls()` | Inside `<Viewer>` | Get `fit`, `reset`, `setView`, `setViewDirection`, `frameBox`, and `setSection`. |
1185
+ | `useViewerToolbar()` | Inside `<ViewerToolbarProvider>` | Get the actions and state supplied to the provider. |
1186
+ | `useSectionStore()` | Inside `<Viewer>` | Read, set, or subscribe to the viewer's own cut. |
1187
+ | `useContentBox()` | Inside `<Viewer>` | Get the part's bounding box (a `THREE.Box3`, empty until loaded). |
1188
+ | `useTapGuard()` | Inside `<Viewer>` | Check whether a pointer event was a click and not a drag. |
881
1189
 
882
1190
  ### Helpers
883
1191
 
@@ -903,11 +1211,12 @@ use. They're listed in `dist/index.d.ts`.
903
1211
 
904
1212
  `ViewerProps`, `ViewerHandle`, `ViewerView`, `Projection`, `ControlScheme`, `EnginePartProps`,
905
1213
  `PartMeshProps`, `PartPick`, `PickModifiers`, `PartModel`, `PartModelFeature`, `PartModelRegion`,
906
- `FeatureTag`, `FeatureType`, `Vec3`, `FeatureHighlight`, `RegionHighlight`, `SectionOptions`,
1214
+ `FeatureTag`, `FeatureType`, `Vec3`, `FeatureHighlight`, `RegionHighlight`, `FocusOptions`, `SectionOptions`,
907
1215
  `SectionState`, `SectionPlacement`, `SectionToolProps`, `SectionStore`, `MeasureToolProps`,
908
1216
  `MeasureMode`, `Measurement`, `DistanceMeasurement`, `AngleMeasurement`, `Snap`, `SnapKind`,
909
1217
  `ViewerTheme`, `ViewName`, `DirectionArrowsProps`, `NamedDirection`, `GridProps`, `AxesProps`,
910
- `ViewCubeProps`.
1218
+ `ViewCubeProps`, `ViewerToolbarProps`, `ViewerToolbarControlsProps`, `ViewerToolbarControls`,
1219
+ `ViewerToolbarControl`.
911
1220
 
912
1221
  `FeatureType` and `ShapeKind` accept any string, because newer Engine versions add new values.
913
1222
  Handle values you don't recognize.
Binary file