@toolpath/viewer 2.3.1 → 2.3.3
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 +781 -1171
- package/dist/{chunk-N54FHMTG.js → chunk-M2B3CQPQ.js} +40 -2
- package/dist/engine/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,45 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Toolpath Viewer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Toolpath Engine. You can also give it your own geometry.
|
|
3
|
+
Display CAD meshes in a React application with feature picking, camera controls,
|
|
4
|
+
stock, section cuts, and measurements. Use `EnginePart` for an analyzed Toolpath
|
|
5
|
+
report, or `PartMesh` for geometry and region/feature data you supply yourself.
|
|
6
|
+
The viewer does not read STEP files, run analysis, or require an API key to render
|
|
7
|
+
local geometry.
|
|
9
8
|
|
|
10
9
|
- [Install](#install)
|
|
11
|
-
- [Quick start](#quick-start)
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [Using it without the Engine](#using-it-without-the-engine)
|
|
10
|
+
- [Quick start: Engine report](#quick-start)
|
|
11
|
+
- [Use your own geometry](#using-it-without-the-engine)
|
|
12
|
+
- [Selection and hover](#selection-and-hover)
|
|
13
|
+
- [Components and camera controls](#components)
|
|
14
|
+
- [Toolbar, stock, section, and measurement recipes](#recipes)
|
|
15
|
+
- [Engine loading, caching, and errors](#working-with-the-toolpath-api)
|
|
18
16
|
- [Reference](#reference)
|
|
17
|
+
- [Work on the package](#working-on-this-package)
|
|
19
18
|
|
|
20
19
|
## Install
|
|
21
20
|
|
|
22
|
-
```
|
|
23
|
-
npm install @toolpath/viewer
|
|
21
|
+
```sh
|
|
22
|
+
npm install @toolpath/viewer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Your application must provide compatible peers:
|
|
26
|
+
|
|
27
|
+
| Dependency | Declared range |
|
|
28
|
+
| -------------------- | -------------- |
|
|
29
|
+
| `react`, `react-dom` | `^19.0.0` |
|
|
30
|
+
| `three` | `>=0.159.0` |
|
|
31
|
+
| `@react-three/fiber` | `^9.7.0` |
|
|
32
|
+
| `@react-three/drei` | `^10.7.8` |
|
|
33
|
+
|
|
34
|
+
For a new application, this is the combination used by the repository's example:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install @toolpath/viewer react@19.2.0 react-dom@19.2.0 three@0.185.1 @react-three/fiber@9.7.0 @react-three/drei@10.7.8
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
TypeScript applications also need React and three.js types, for example
|
|
41
|
+
`@types/react`, `@types/react-dom`, and `@types/three`. Match the three.js types to
|
|
42
|
+
your installed three.js release. Keep one compatible copy of each renderer/peer
|
|
43
|
+
in your application; React Three Fiber 9 uses React 19.
|
|
44
|
+
|
|
45
|
+
This is an ESM package with included TypeScript declarations. Its declared Node
|
|
46
|
+
floor is 20; the tested three.js release needs a browser with WebGL2 support. Use your framework's
|
|
47
|
+
client component boundary for interactive React code and its normal bundler setup.
|
|
48
|
+
See the [React Three Fiber installation guide](https://r3f.docs.pmnd.rs/getting-started/installation)
|
|
49
|
+
for framework-specific setup.
|
|
50
|
+
|
|
51
|
+
**Current packaging limitation:** direct, unbundled Node ESM imports of both
|
|
52
|
+
JavaScript entries fail with `CameraControls.install is not a function`. Use an
|
|
53
|
+
application bundler; for SSR, verify the framework's server dependency bundling
|
|
54
|
+
as well as its client component boundary. The declared Node floor does not make
|
|
55
|
+
these entries standalone Node utilities.
|
|
56
|
+
|
|
57
|
+
### Styles
|
|
58
|
+
|
|
59
|
+
The canvas does not need Tailwind or `@toolpath/ui`. Give the viewer a nonzero
|
|
60
|
+
height. It fills its wrapper, and its transparent canvas shows the background you
|
|
61
|
+
set on the wrapper or parent.
|
|
62
|
+
|
|
63
|
+
For the optional toolbar, import its plain CSS through your application's CSS
|
|
64
|
+
entry point or client entry:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
@import '@toolpath/viewer/toolbar.css';
|
|
24
68
|
```
|
|
25
69
|
|
|
70
|
+
Measurement labels and hover-card content need your own styling; the toolbar
|
|
71
|
+
stylesheet does not style them. See [Measuring](#measuring) and [Theming](#theming).
|
|
72
|
+
|
|
26
73
|
## Quick start
|
|
27
74
|
|
|
28
|
-
Pass a
|
|
75
|
+
Pass a completed analysis report to `EnginePart`. It validates the report, loads
|
|
76
|
+
its mesh, and renders the result. It accepts `unknown`, so the viewer does not
|
|
77
|
+
require an SDK dependency just to display a report.
|
|
78
|
+
|
|
79
|
+
This example uses [react-error-boundary](https://github.com/bvaughn/react-error-boundary):
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npm install react-error-boundary
|
|
83
|
+
```
|
|
29
84
|
|
|
30
85
|
```tsx
|
|
31
86
|
'use client'
|
|
32
87
|
|
|
33
88
|
import { Suspense, useState } from 'react'
|
|
34
|
-
import {
|
|
89
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
90
|
+
import { Axes, Grid, ViewCube, Viewer, type FeatureTag } from '@toolpath/viewer'
|
|
35
91
|
import { EnginePart } from '@toolpath/viewer/engine'
|
|
36
|
-
import type { PartResponse } from '@toolpath/api'
|
|
37
92
|
|
|
38
|
-
export
|
|
39
|
-
const [selection, setSelection] = useState<
|
|
93
|
+
export const PartViewer = ({ report }: { report: unknown }) => {
|
|
94
|
+
const [selection, setSelection] = useState<Array<FeatureTag>>([])
|
|
40
95
|
|
|
41
96
|
return (
|
|
42
|
-
<ErrorBoundary
|
|
97
|
+
<ErrorBoundary
|
|
98
|
+
fallback={<p role="alert">This part could not be displayed.</p>}
|
|
99
|
+
resetKeys={[report]}
|
|
100
|
+
>
|
|
43
101
|
<Suspense fallback={<p>Loading part…</p>}>
|
|
44
102
|
<Viewer style={{ height: 500 }} onPointerMissed={() => setSelection([])}>
|
|
45
103
|
<EnginePart
|
|
@@ -57,930 +115,537 @@ export function PartViewer({ report }: { report: PartResponse }) {
|
|
|
57
115
|
}
|
|
58
116
|
```
|
|
59
117
|
|
|
60
|
-
|
|
61
|
-
|
|
118
|
+
Suspense supplies loading UI; the error boundary handles invalid report data.
|
|
119
|
+
An accepted analysis job is not a report: finish the API workflow
|
|
120
|
+
before passing its result here. When changing parts/results, key `PartViewer` by
|
|
121
|
+
analysis-result identity to reset selection, the camera, and one-time overlay
|
|
122
|
+
bounds. Refreshing URLs for the same artifact does not require a new key.
|
|
62
123
|
|
|
63
|
-
|
|
124
|
+
## How it works
|
|
64
125
|
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
126
|
+
- Geometry and tool dimensions are **millimetres**, with **Z up**. Vectors and
|
|
127
|
+
stock must use the same coordinate frame; converting display units does not
|
|
128
|
+
change mesh coordinates.
|
|
129
|
+
- Regions identify surfaces and cover half-open triangle ranges `[start, end)`.
|
|
130
|
+
A feature refers to one or more regions. Several direction-dependent features
|
|
131
|
+
can share one surface, so a click can have several valid owners.
|
|
132
|
+
- `PartMesh` emits picks and hover information. Your application owns selection,
|
|
133
|
+
filtering, DFM rules, annotations, and persistence.
|
|
134
|
+
- The viewer frames its content when it first appears and renders on demand.
|
|
135
|
+
It still renders during interaction and transitions; this is not a zero-cost
|
|
136
|
+
guarantee for an idle mounted scene. Call `fit()` after later content changes
|
|
137
|
+
when you want to reframe, or remount the viewer to start a new view.
|
|
71
138
|
|
|
72
|
-
##
|
|
139
|
+
## Using it without the Engine
|
|
73
140
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
│ └─ <PartMesh> draws the part and handles hover, click, colours, section cuts
|
|
78
|
-
├─ <DirectionArrows> arrows for the directions the part can be machined from
|
|
79
|
-
├─ <Stock> translucent geometry, GLB, box or cylinder stock, included in Fit
|
|
80
|
-
├─ <SectionTool> optional: click a face or a plane to cut the part open
|
|
81
|
-
├─ <MeasureTool> optional: click two points for a distance, three for an angle
|
|
82
|
-
├─ <Grid> <Axes> reference geometry, sized to the part
|
|
83
|
-
└─ <ViewCube> the clickable orientation cube in the corner
|
|
84
|
-
```
|
|
141
|
+
Supply a `PartModel` and a **non-indexed** three.js `BufferGeometry`. Region ranges
|
|
142
|
+
must cover every triangle without gaps or overlaps; `buildRegionIndex` validates
|
|
143
|
+
that mapping. Preserve triangle order when preparing geometry.
|
|
85
144
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
a viewer that isn't being used costs nothing.
|
|
89
|
-
2. **`<EnginePart>`** checks the report, downloads the mesh (GLB first, STL if there is no GLB),
|
|
90
|
-
and makes sure the mesh matches the report. Meshes are cached, so showing the same part twice
|
|
91
|
-
downloads it once.
|
|
92
|
-
3. **`<PartMesh>`** draws the part as one mesh. The Engine splits the part's surface into
|
|
93
|
-
**regions** (faces) and groups regions into **features** (holes, pockets, walls, and so on).
|
|
94
|
-
When the pointer is over the part, `PartMesh` works out which region it is on and which
|
|
95
|
-
features own that region.
|
|
96
|
-
4. **You own the state.** The viewer tells you what was clicked (`onPick`), and you tell it what to
|
|
97
|
-
highlight (`selection`, `highlights`, …). It never changes your selection by itself.
|
|
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.
|
|
145
|
+
This complete example renders a 20 mm cube and disposes the caller-owned geometry
|
|
146
|
+
on unmount:
|
|
104
147
|
|
|
105
148
|
```tsx
|
|
106
|
-
|
|
149
|
+
import { useEffect, useMemo } from 'react'
|
|
150
|
+
import { BoxGeometry } from 'three'
|
|
151
|
+
import { PartMesh, Viewer, ViewCube, buildRegionIndex, type PartModel } from '@toolpath/viewer'
|
|
152
|
+
|
|
153
|
+
export const Cube = () => {
|
|
154
|
+
const { model, geometry } = useMemo(() => {
|
|
155
|
+
const indexed = new BoxGeometry(20, 20, 20)
|
|
156
|
+
const geometry = indexed.toNonIndexed()
|
|
157
|
+
indexed.dispose()
|
|
158
|
+
const regions = Array.from({ length: 6 }, (_, idx) => ({
|
|
159
|
+
idx,
|
|
160
|
+
splitOrigin: idx,
|
|
161
|
+
shapeKind: 'Plane',
|
|
162
|
+
area: 400,
|
|
163
|
+
triangles: { start: idx * 2, end: idx * 2 + 2 },
|
|
164
|
+
}))
|
|
165
|
+
const features = [
|
|
166
|
+
{
|
|
167
|
+
tag: 'top',
|
|
168
|
+
featureType: 'face',
|
|
169
|
+
machiningDirection: { x: 0, y: 0, z: 1 },
|
|
170
|
+
axis: null,
|
|
171
|
+
regionIdxs: [4],
|
|
172
|
+
},
|
|
173
|
+
]
|
|
174
|
+
const model: PartModel = {
|
|
175
|
+
partId: 'local-cube',
|
|
176
|
+
kernelVersion: '',
|
|
177
|
+
regions,
|
|
178
|
+
features,
|
|
179
|
+
candidateDirections: [{ x: 0, y: 0, z: 1 }],
|
|
180
|
+
mesh: { pointCount: 36, triangleCount: 12, glbUrl: null, stlUrl: null, thumbnailUrl: null },
|
|
181
|
+
regionIndex: buildRegionIndex({ regions, features, triangleCount: 12 }),
|
|
182
|
+
warnings: [],
|
|
183
|
+
}
|
|
184
|
+
return { model, geometry }
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
useEffect(() => () => geometry.dispose(), [geometry])
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<Viewer style={{ height: 400 }}>
|
|
191
|
+
<PartMesh model={model} geometry={geometry} selection={['top']} />
|
|
192
|
+
<ViewCube />
|
|
193
|
+
</Viewer>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
107
196
|
```
|
|
108
197
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`face` when cut from one direction, a `wall` when cut from another, and part of every `profile`
|
|
115
|
-
around it. So a click gives you every match, and you decide which to use:
|
|
116
|
-
|
|
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 |
|
|
128
|
-
|
|
129
|
-
The ranking puts specific features first: holes, then pockets and bosses, then chamfers and
|
|
130
|
-
fillets, then walls and faces, then profiles. Among features of the same kind, the one whose
|
|
131
|
-
machining direction points most toward the camera wins.
|
|
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.
|
|
198
|
+
The feature covers only the top face; surfaces without a feature remain drawable
|
|
199
|
+
and pickable, but have no `pick.best`. An application-supplied model does not go
|
|
200
|
+
through the Engine kernel-version gate. Keep geometry/model references stable
|
|
201
|
+
between renders. `PartMesh` manages its rendering resources, but never disposes
|
|
202
|
+
the input geometry; the caller owns that lifetime.
|
|
144
203
|
|
|
145
|
-
##
|
|
204
|
+
## Selection and hover
|
|
146
205
|
|
|
147
|
-
|
|
206
|
+
Viewer identity is `FeatureTag` (`model.features[i].tag`, sourced from an Engine
|
|
207
|
+
report's `featureTag`). Do not replace it with an array index or an API
|
|
208
|
+
`featureId`. Region indices are local to their report and cannot join unrelated
|
|
209
|
+
results. `FeatureType` and `ShapeKind` allow unknown strings for newer data.
|
|
210
|
+
|
|
211
|
+
| `PartPick` field | Meaning |
|
|
212
|
+
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
213
|
+
| `best` | Highest-ranked feature tag, or `null`. |
|
|
214
|
+
| `ranked` | Owners ranked for the camera and active direction. |
|
|
215
|
+
| `owners` | All owners in report order, before active-direction filtering. |
|
|
216
|
+
| `region`, `triangleIndex` | Surface index and triangle hit. |
|
|
217
|
+
| `point`, `normal` | Hit position and outward normal; three-number tuples. The normal is in world space. |
|
|
218
|
+
| `pointer` | Optional browser `clientX`/`clientY` for DOM overlays. |
|
|
219
|
+
| `modifiers` | `alt`, `ctrl`, `meta`, `shift`, and `secondary` (right button). |
|
|
220
|
+
| `doubled` | This pick completed a double-click. |
|
|
221
|
+
| `sharpCorner` | Optional corner hit when sharp-corner visualization is enabled. |
|
|
222
|
+
|
|
223
|
+
`onPick` reports primary and secondary clicks. Decide whether secondary clicks,
|
|
224
|
+
double-clicks, or modifier keys should change selection. Double-click retargeting
|
|
225
|
+
still works independently. `onHover` receives a pick or `null`; `hover={false}`
|
|
226
|
+
disables hover feedback and callbacks while leaving click picking enabled.
|
|
148
227
|
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
228
|
+
This inspector shares selection between a list and the mesh, cycles overlapping
|
|
229
|
+
owners on repeated clicks, and draws application-owned hover text:
|
|
153
230
|
|
|
154
231
|
```tsx
|
|
155
|
-
import {
|
|
232
|
+
import { useState } from 'react'
|
|
233
|
+
import type { BufferGeometry } from 'three'
|
|
156
234
|
import {
|
|
157
|
-
|
|
158
|
-
Stock,
|
|
159
|
-
DirectionArrows,
|
|
235
|
+
HoverCard,
|
|
160
236
|
PartMesh,
|
|
161
237
|
Viewer,
|
|
162
|
-
|
|
238
|
+
focusForPick,
|
|
239
|
+
type PartModel,
|
|
240
|
+
type PartPick,
|
|
163
241
|
} from '@toolpath/viewer'
|
|
164
|
-
import type { PartModel, ViewerHandle } from '@toolpath/viewer'
|
|
165
|
-
import type { BufferGeometry } from 'three'
|
|
166
242
|
|
|
167
|
-
export
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
243
|
+
export const FeatureInspector = ({
|
|
244
|
+
model,
|
|
245
|
+
geometry,
|
|
246
|
+
}: {
|
|
247
|
+
model: PartModel
|
|
248
|
+
geometry: BufferGeometry
|
|
249
|
+
}) => {
|
|
250
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
251
|
+
const [hovered, setHovered] = useState<string | null>(null)
|
|
252
|
+
const [pick, setPick] = useState<PartPick | null>(null)
|
|
253
|
+
const [lastRegion, setLastRegion] = useState<number | null>(null)
|
|
178
254
|
|
|
179
255
|
return (
|
|
180
256
|
<>
|
|
181
|
-
<
|
|
257
|
+
<ul>
|
|
258
|
+
{model.features.map((feature) => (
|
|
259
|
+
<li key={feature.tag}>
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={() => setSelected(feature.tag)}
|
|
263
|
+
onMouseEnter={() => setHovered(feature.tag)}
|
|
264
|
+
onMouseLeave={() => setHovered(null)}
|
|
265
|
+
>
|
|
266
|
+
{feature.featureType}: {feature.tag}
|
|
267
|
+
</button>
|
|
268
|
+
</li>
|
|
269
|
+
))}
|
|
270
|
+
</ul>
|
|
271
|
+
<Viewer style={{ height: 400 }} onPointerMissed={() => setSelected(null)}>
|
|
182
272
|
<PartMesh
|
|
183
273
|
model={model}
|
|
184
274
|
geometry={geometry}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
275
|
+
selection={selected ? [selected] : []}
|
|
276
|
+
hoveredFeatureIds={hovered ? [hovered] : []}
|
|
277
|
+
focusFeature={selected}
|
|
278
|
+
onHover={setPick}
|
|
279
|
+
onPick={(next) => {
|
|
280
|
+
if (next.modifiers.secondary || next.doubled) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
setSelected(focusForPick(next, lastRegion, selected))
|
|
284
|
+
setLastRegion(next.region)
|
|
285
|
+
}}
|
|
196
286
|
/>
|
|
197
287
|
</Viewer>
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
</
|
|
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>
|
|
288
|
+
<HoverCard pick={pick}>
|
|
289
|
+
{(current) => <span>{current.best ?? `Region ${current.region}`}</span>}
|
|
290
|
+
</HoverCard>
|
|
211
291
|
</>
|
|
212
292
|
)
|
|
213
293
|
}
|
|
214
294
|
```
|
|
215
295
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`{
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
296
|
+
`HoverCard` is a DOM element; keep it outside the canvas and style its contents.
|
|
297
|
+
`focusFeature` frames the feature when its value changes. The `focus` prop is
|
|
298
|
+
different: `focus={{ opacity: 0.12 }}` makes regions outside `selection`
|
|
299
|
+
translucent. Its default outside opacity is 0.15, and an empty selection leaves
|
|
300
|
+
the part opaque. For multiselection, update your own tag array and avoid duplicates.
|
|
339
301
|
|
|
340
|
-
|
|
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
|
-
```
|
|
302
|
+
## Components
|
|
359
303
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
`
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
|
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`, `tools`, `directions`, `dfm`, `sharpCorners`, `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.
|
|
304
|
+
| Component | Purpose |
|
|
305
|
+
| ---------------------------- | ------------------------------------------------------------------------------------ |
|
|
306
|
+
| `Viewer` | Canvas, camera, lighting, controls, initial framing, and viewer-owned section state. |
|
|
307
|
+
| `EnginePart` | Validated Engine report plus cached mesh loading; imports from `/engine`. |
|
|
308
|
+
| `PartMesh` | Your normalized model and prepared geometry; selection, paint, picking, sections. |
|
|
309
|
+
| `ViewCube` | 26 orientation targets: faces, edges, and corners. |
|
|
310
|
+
| `Grid`, `Axes` | Reference geometry excluded from camera framing. |
|
|
311
|
+
| `DirectionArrows` | Candidate and optional named/preview machining directions. |
|
|
312
|
+
| `Stock` | Geometry, GLB bytes, box, or cylinder stock. |
|
|
313
|
+
| `SectionTool`, `SectionView` | Interactive section placement or a supplied section visualization. |
|
|
314
|
+
| `MeasureTool` | Distance/angle picking and labels. |
|
|
315
|
+
| `ToolMarks` | Supplied cutter/profile geometry and labels; no datasheet inference. |
|
|
316
|
+
| `Banana` | Optional bundled 154 mm scale reference; loads its GLB on mount and suspends. |
|
|
317
|
+
| `CadCameraControls` | Lower-level camera controls for custom R3F scenes. `Viewer` already installs them. |
|
|
318
|
+
| `ViewerToolbar` | DOM controls configured through `ViewerToolbarProvider`. |
|
|
319
|
+
| `HoverCard` | Cursor-following DOM content for a pick. |
|
|
408
320
|
|
|
409
321
|
### `<Viewer>`
|
|
410
322
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
|
414
|
-
|
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
323
|
+
| Prop | Default | Behavior |
|
|
324
|
+
| ----------------------- | ----------------- | ------------------------------------------------------------------------------------- |
|
|
325
|
+
| `projection` | `orthographic` | `perspective` is also supported. Changing it remounts the canvas and resets the view. |
|
|
326
|
+
| `controls` | `toolpath` | Navigation preset, independent of projection. |
|
|
327
|
+
| `zoomTo` | `cursor` | Zoom toward the pointer or `centre`. |
|
|
328
|
+
| `freeOrbit` | `true` | Allow rotation through the poles. |
|
|
329
|
+
| `retargetOnDoubleClick` | `true` | Double-click a part to move the orbit target there. |
|
|
330
|
+
| `recentreOnDoubleClick` | `true` | Double middle-click to fit content. |
|
|
331
|
+
| `showOrbitTarget` | `false` | Show the pivot marker. |
|
|
332
|
+
| `theme` | Default lighting | Override lighting; part/cube/tool colors have their own theme props. |
|
|
333
|
+
| `onPointerMissed` | — | Empty-space primary click, with drag guarding; use it to clear selection. |
|
|
334
|
+
| `style`, `className` | Full-size wrapper | Set dimensions and background on the wrapper or parent. |
|
|
335
|
+
| `ref` | — | `ViewerHandle` for controls outside the canvas. |
|
|
336
|
+
|
|
337
|
+
`ViewerHandle` exposes `fit()`, `reset()`, `setView(name)`,
|
|
338
|
+
`setViewDirection({ x, y, z })`, `frameBox(box)`, and `setSection(options | null)`.
|
|
339
|
+
`fit` and `frameBox` preserve view direction; `reset` returns to the opening view.
|
|
340
|
+
Named views are `top`, `bottom`, `front`, `back`, `left`, `right`, and `isometric`.
|
|
341
|
+
Direction vectors point from the part toward the camera.
|
|
430
342
|
|
|
431
343
|
#### CAD controls
|
|
432
344
|
|
|
433
|
-
|
|
434
|
-
|
|
345
|
+
| Preset | Rotate | Pan | Zoom |
|
|
346
|
+
| -------------------- | ----------------------------------------------- | ------------------------------- | --------------------------- |
|
|
347
|
+
| `toolpath` | Left-drag | Right-drag | Wheel |
|
|
348
|
+
| `fusion`, `inventor` | Shift + middle-drag / Shift + two-finger scroll | Middle-drag / two-finger scroll | Trackpad pinch |
|
|
349
|
+
| `alias` | Left-drag | Middle-drag | Wheel |
|
|
350
|
+
| `solidworks` | Middle-drag | Ctrl + middle-drag | Shift + middle-drag / wheel |
|
|
351
|
+
| `tinkercad` | Right-drag | Shift + right-drag | Wheel |
|
|
352
|
+
| `powermill` | Middle-drag | Shift + middle-drag | Wheel |
|
|
353
|
+
| `onshape` | Right-drag | Middle-drag | Wheel |
|
|
435
354
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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.
|
|
450
|
-
|
|
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.
|
|
454
|
-
|
|
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.
|
|
458
|
-
|
|
459
|
-
`'toolpath'`, `freeOrbit={true}`, and `zoomTo="cursor"` are the defaults.
|
|
355
|
+
`CONTROL_SCHEME_OPTIONS` supplies labels/values for a settings UI. Preferences are
|
|
356
|
+
application-owned. Default touch controls use one-finger rotation, two-finger
|
|
357
|
+
pinch/pan, and three-finger pan; Fusion/Inventor use their CAD scroll behavior.
|
|
460
358
|
|
|
461
359
|
### `<EnginePart>`
|
|
462
360
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
other props are the same as `<PartMesh>`'s, except `model` and `geometry`, which it creates for
|
|
467
|
-
you.
|
|
468
|
-
|
|
469
|
-
Mesh URLs in a report expire **15 minutes** after the report was created. If a part may be shown
|
|
470
|
-
later than that, fetch a fresh report first. Don't save the URLs.
|
|
361
|
+
`report` is its only additional prop. It inherits `PartMeshProps` except `model`
|
|
362
|
+
and `geometry`, which it creates. Mesh loading prefers GLB and tries STL if GLB
|
|
363
|
+
fails or is absent. It does not fetch a report or start an Engine job.
|
|
471
364
|
|
|
472
365
|
### `<PartMesh>`
|
|
473
366
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
|
481
|
-
|
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
| `section` | `SectionOptions` | Cut the part open. Omit it to follow the viewer's own cut. See [Section view](#section-view). |
|
|
503
|
-
| `onSectionChange` | `(state: SectionState) => void` | Called when the cut moves or goes away. With `section`, passing it also shows the drag handle. |
|
|
504
|
-
| `onAdjacency` | `(map) => void` | Called once per mesh with which faces touch which. |
|
|
505
|
-
| `children` | `ReactNode` | Objects drawn on the part. They read its model and mesh with `usePartContext()`. |
|
|
506
|
-
|
|
507
|
-
Hovering over the part is handled for you. You only need `onHover` if you want to show the hovered
|
|
508
|
-
feature elsewhere in your UI.
|
|
509
|
-
|
|
510
|
-
### `<ViewCube>`
|
|
511
|
-
|
|
512
|
-
The orientation cube in the corner. It has 26 clickable areas: 6 faces, 12 edges, and 8 corners,
|
|
513
|
-
so you can get an isometric view with one click.
|
|
514
|
-
|
|
515
|
-
```tsx
|
|
516
|
-
<ViewCube alignment="bottom-right" margin={[60, 60]} onViewChange={(view) => track(view)} />
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
| Prop | Default | |
|
|
520
|
-
| -------------- | ------------- | -------------------------------------------------------------- |
|
|
521
|
-
| `alignment` | `'top-right'` | `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` |
|
|
522
|
-
| `margin` | `[80, 80]` | Distance from the corner, in pixels |
|
|
523
|
-
| `onViewChange` | — | Called with the view name, e.g. `'top-front-right'` |
|
|
524
|
-
| `theme` | — | Cube colours |
|
|
525
|
-
|
|
526
|
-
### `<DirectionArrows>`
|
|
527
|
-
|
|
528
|
-
An arrow for each direction the part can be machined from. Each direction has its own colour, and
|
|
529
|
-
features machined from that direction use the same colour. The arrows point **at** the part,
|
|
530
|
-
because that is the direction the tool comes from.
|
|
531
|
-
|
|
532
|
-
```tsx
|
|
533
|
-
<DirectionArrows
|
|
534
|
-
directions={model.candidateDirections}
|
|
535
|
-
activeDirection={direction}
|
|
536
|
-
onPickDirection={(index) => setDirection(index === direction ? null : index)}
|
|
537
|
-
/>
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
| Prop | What it does |
|
|
541
|
-
| ------------------ | ------------------------------------------------------------------------------------ |
|
|
542
|
-
| `directions` | The report's `candidateDirections`. |
|
|
543
|
-
| `activeDirection` | Show only this arrow. |
|
|
544
|
-
| `shownDirection` | Which arrows to show: `null` for all, an index, a list of indices, or `-1` for none. |
|
|
545
|
-
| `previewDirection` | An extra arrow drawn on top of the part, e.g. while the user aims a new direction. |
|
|
546
|
-
| `namedDirections` | Extra directions that aren't candidates, each with its own colour. |
|
|
547
|
-
| `onPickDirection` | Called with an arrow's index when it is clicked. |
|
|
548
|
-
| `visible` | Hide all arrows. |
|
|
549
|
-
|
|
550
|
-
Clicking an arrow does not trigger `onPointerMissed`, so it won't clear your selection.
|
|
551
|
-
|
|
552
|
-
With the Engine, get the directions from the model:
|
|
553
|
-
`const model = useMemo(() => normalizePartReport(report), [report])`.
|
|
554
|
-
|
|
555
|
-
### `<SectionTool>`
|
|
556
|
-
|
|
557
|
-
Cut the part open from inside the viewport. Optional: mount it next to the part when the user
|
|
558
|
-
enters section mode, and unmount it when they leave.
|
|
559
|
-
|
|
560
|
-
```tsx
|
|
561
|
-
{
|
|
562
|
-
sectioning ? <SectionTool /> : null
|
|
563
|
-
}
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
While it is up, hovering the part previews a cut through the face under the pointer and a click
|
|
567
|
-
places it; three coloured planes behind the part cut along an axis instead. Once there is a cut,
|
|
568
|
-
the part's own arrow drags it, an outlined sheet shows the cutting plane, and Escape clears it.
|
|
569
|
-
|
|
570
|
-
| Prop | What it does |
|
|
571
|
-
| ------- | ---------------------------------------------------------------------- |
|
|
572
|
-
| `theme` | Override its colours — `sectionOutline` for the sheet and the preview. |
|
|
573
|
-
|
|
574
|
-
The cut it places is the viewer's own, and `<PartMesh>` follows it when given no `section` prop.
|
|
575
|
-
While the tool is mounted the part reports no hovers or picks. Details and the controlled
|
|
576
|
-
alternative are under [Section view](#section-view).
|
|
577
|
-
|
|
578
|
-
### `<MeasureTool>`
|
|
579
|
-
|
|
580
|
-
Measure the part from inside the viewport. Optional: mount it next to the part when the user
|
|
581
|
-
enters measure mode, and unmount it when they leave.
|
|
582
|
-
|
|
583
|
-
```tsx
|
|
584
|
-
{
|
|
585
|
-
measuring ? <MeasureTool mode="distance" onChange={setMeasurements} /> : null
|
|
586
|
-
}
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
The pointer snaps to what is under it — a corner first, then an edge's midpoint, then the edge,
|
|
590
|
-
then the face — and a marker shows where the click will land. Two clicks measure a distance, three
|
|
591
|
-
an angle (one arm's end, the vertex, the other arm's end), with a live readout between clicks.
|
|
592
|
-
Finished measurements stay drawn over the part, each with a label, until Delete removes the last
|
|
593
|
-
one or the tool is unmounted. Escape drops the points of one in progress, and Shift holds the next
|
|
594
|
-
point to an axis through the last.
|
|
595
|
-
|
|
596
|
-
| Prop | What it does |
|
|
597
|
-
| ---------------- | --------------------------------------------------------------------------------------------- |
|
|
598
|
-
| `mode` | `'distance'` (default) or `'angle'`. Changing it drops any points already placed. |
|
|
599
|
-
| `measurements` | Own the list. Omit it and the tool keeps its own. |
|
|
600
|
-
| `onChange` | The list changed: a measurement finished, or Delete removed the last one. |
|
|
601
|
-
| `showDeltas` | Show a distance's X, Y and Z parts as dashed legs in the axis colours. On by default. |
|
|
602
|
-
| `format` | Write a length. Millimetres to two places by default; pass your own to show inches. |
|
|
603
|
-
| `labelClassName` | Added to every label, beside `toolpath-measure-label`. |
|
|
604
|
-
| `theme` | Override its colours — `measure` for lines and markers, `measureSnap` for the snap indicator. |
|
|
605
|
-
|
|
606
|
-
Labels are DOM elements laid over the canvas and come **unstyled**: the tool is headless, and
|
|
607
|
-
what a label looks like is your stylesheet's, through the `toolpath-measure-label` class and the
|
|
608
|
-
data attributes on each one — see [Measuring](#measuring) for a starting point. While the tool is
|
|
609
|
-
mounted the part reports no hovers or picks, as with `<SectionTool>`.
|
|
610
|
-
|
|
611
|
-
### `<ToolMarks>`
|
|
612
|
-
|
|
613
|
-
Cutting tools drawn on the part, each to scale where it stands: a see-through end mill — flat,
|
|
614
|
-
bull nose or ball — or any tool given by its profile, rimmed at its bottom and top, with a dimension
|
|
615
|
-
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.
|
|
616
|
-
Nothing here reads a datasheet.
|
|
617
|
-
|
|
618
|
-
```tsx
|
|
619
|
-
<EnginePart report={report}>
|
|
620
|
-
<ToolMarks
|
|
621
|
-
marks={[
|
|
622
|
-
{
|
|
623
|
-
base: { x: 12, y: 4, z: -8 },
|
|
624
|
-
axis: { x: 0, y: 0, z: 1 },
|
|
625
|
-
diameter: 6,
|
|
626
|
-
height: 8,
|
|
627
|
-
cornerRadius: 0.5,
|
|
628
|
-
label: '⌀ 6.000 mm',
|
|
629
|
-
note: 'bull nose R 0.5',
|
|
630
|
-
},
|
|
631
|
-
]}
|
|
632
|
-
/>
|
|
633
|
-
</EnginePart>
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
| Prop | What it does |
|
|
637
|
-
| ---------------- | ---------------------------------------------------------------------------------- |
|
|
638
|
-
| `marks` | The tools, in the coordinates the part is drawn in. See `ToolMark` below. |
|
|
639
|
-
| `visible` | `false` hides them without unmounting. On by default. |
|
|
640
|
-
| `labelClassName` | Added to every label, beside `toolpath-measure-label`. |
|
|
641
|
-
| `theme` | `measure` colours the rims and the dimension, as it does the measure tool's lines. |
|
|
642
|
-
|
|
643
|
-
A `ToolMark` is `base` (the centre of the tool's bottom), `axis` (up the tool), `diameter`,
|
|
644
|
-
`height`, and optionally `cornerRadius` (0 flat, half the diameter a ball), `across` (which way the
|
|
645
|
-
dimension runs; any direction square to the axis otherwise), `label` and `note`. Any other tool —
|
|
646
|
-
a drill, a chamfer mill, a tool in its holder — takes `profile`: its side as `{ r, z }` points from
|
|
647
|
-
the tip up, such as `@toolpath/tool-drawing`'s outline, turned about the axis in place of the end
|
|
648
|
-
mill.
|
|
649
|
-
|
|
650
|
-
Show and hide them with `visible`, from the toolbar's `ToolsButton` and its `tools` control.
|
|
651
|
-
|
|
652
|
-
It draws as `<MeasureTool>` draws: the lines show through the part, and each label is an unstyled
|
|
653
|
-
`<div class="toolpath-measure-label" data-measure-label="tool">` with its text in a
|
|
654
|
-
`data-measure-value` span and its note in a `data-measure-note` one. The marks take no pointer
|
|
655
|
-
events and are left out of framing.
|
|
656
|
-
|
|
657
|
-
Inside `<PartMesh>` or `<EnginePart>`, `usePartContext()` gives the part's `model` and `geometry`,
|
|
658
|
-
and `featureTriangles(model, geometry, tag)` a feature's triangles, for working out where a tool
|
|
659
|
-
stands.
|
|
660
|
-
|
|
661
|
-
### `<Grid>` and `<Axes>`
|
|
662
|
-
|
|
663
|
-
Reference geometry. The camera ignores both when it frames the part.
|
|
664
|
-
|
|
665
|
-
- **`<Grid>`** sits under the part and sizes itself to it: 5 mm squares under a 50 mm cube, 50 mm
|
|
666
|
-
squares under a 900 mm plate. Props: `step` (square size in mm), `extent`, `color`,
|
|
667
|
-
`opacity` (default `0.35`).
|
|
668
|
-
- **`<Axes>`** draws X (red), Y (green), and Z (blue) from the origin. Prop: `size` (default `25`).
|
|
367
|
+
| Props | Behavior |
|
|
368
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
369
|
+
| `model`, `geometry` | Required normalized data and non-indexed geometry. |
|
|
370
|
+
| `selection`, `candidates` | Selected feature tags and secondary candidates. |
|
|
371
|
+
| `highlights`, `regionHighlights` | Feature/region colors. Hover and selection overlay the direction wash. |
|
|
372
|
+
| `pickedRegions`, `hoveredFeatureIds` | Application-driven region/feature feedback. |
|
|
373
|
+
| `hover`, `onHover`, `onPick` | Hover enablement and interaction callbacks. |
|
|
374
|
+
| `focus`, `focusFeature` | Selection X-ray rendering versus camera framing. |
|
|
375
|
+
| `activeDirection` | Candidate-direction index; `null`/omitted means all directions. |
|
|
376
|
+
| `display`, `showEdges` | `solid` or `wireframe`; outlines default on. Wireframe always shows semantic face boundaries, not triangle diagonals. |
|
|
377
|
+
| `sharpCorners` | `{ features: tags }` for externally flagged features, or `null`. |
|
|
378
|
+
| `section`, `onSectionChange` | Controlled section options and change reporting. Omit `section` to follow the viewer's section store. |
|
|
379
|
+
| `onAdjacency` | Receive touching-region sets for the prepared mesh. |
|
|
380
|
+
| `theme` | Partial `ViewerTheme` for the part. |
|
|
381
|
+
| `children` | R3F overlays that can read `usePartContext()`. |
|
|
382
|
+
|
|
383
|
+
With an active direction, picking ranks only reachable owners; a face with none
|
|
384
|
+
has `best === null`. `owners` still carries all owners. `directionHighlights(model,
|
|
385
|
+
index)` produces region paint matching `DirectionArrows`; it does not hide geometry
|
|
386
|
+
or establish that an unpainted face is impossible to machine.
|
|
387
|
+
|
|
388
|
+
### `<ViewCube>`, `<Grid>`, and `<Axes>`
|
|
389
|
+
|
|
390
|
+
`ViewCube` defaults to `alignment="top-right"` and `margin={[80, 80]}` (pixels).
|
|
391
|
+
`onViewChange` receives a `ViewName`, including diagonal names. Its theme controls
|
|
392
|
+
cube colors. Grid chooses a step from part size unless you supply `step`/`extent`;
|
|
393
|
+
it accepts `color` and `opacity` (default 0.35). Axes has a `size` in millimetres
|
|
394
|
+
(default 25). These controls/reference objects do not enlarge normal part framing.
|
|
669
395
|
|
|
670
396
|
## Recipes
|
|
671
397
|
|
|
672
|
-
###
|
|
398
|
+
### Toolbar and camera buttons
|
|
673
399
|
|
|
674
|
-
|
|
400
|
+
This scene uses caller-owned geometry. The provider supplies actions; it does not
|
|
401
|
+
implement toggling, persistence, measurement state, or stock creation for you.
|
|
402
|
+
The containing DOM element is positioned so the toolbar sits over this viewer.
|
|
675
403
|
|
|
676
404
|
```tsx
|
|
677
|
-
import { useRef } from 'react'
|
|
678
|
-
import {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
| `setViewDirection({x,y,z})` | Look at the part from any direction (a vector pointing toward the camera). |
|
|
696
|
-
| `frameBox(box)` | Zoom to a `THREE.Box3`, keeping the current angle. |
|
|
697
|
-
| `setSection(options)` | Set or clear (`null`) the viewer's own cut. See [Section view](#section-view). |
|
|
698
|
-
|
|
699
|
-
Components rendered **inside** `<Viewer>` can get the same methods with `useViewerControls()`.
|
|
700
|
-
|
|
701
|
-
### Selecting from a list
|
|
405
|
+
import { useRef, useState } from 'react'
|
|
406
|
+
import type { BufferGeometry } from 'three'
|
|
407
|
+
import {
|
|
408
|
+
Axes,
|
|
409
|
+
Grid,
|
|
410
|
+
MeasureTool,
|
|
411
|
+
PartMesh,
|
|
412
|
+
SectionTool,
|
|
413
|
+
Stock,
|
|
414
|
+
ViewCube,
|
|
415
|
+
Viewer,
|
|
416
|
+
ViewerToolbar,
|
|
417
|
+
ViewerToolbarProvider,
|
|
418
|
+
type Measurement,
|
|
419
|
+
type PartModel,
|
|
420
|
+
type ViewerHandle,
|
|
421
|
+
} from '@toolpath/viewer'
|
|
422
|
+
import '@toolpath/viewer/toolbar.css'
|
|
702
423
|
|
|
703
|
-
|
|
704
|
-
|
|
424
|
+
export const Workbench = ({ model, geometry }: { model: PartModel; geometry: BufferGeometry }) => {
|
|
425
|
+
const viewer = useRef<ViewerHandle>(null)
|
|
426
|
+
const [stock, setStock] = useState(false)
|
|
427
|
+
const [mode, setMode] = useState<'section' | 'measure' | null>(null)
|
|
428
|
+
const [measurements, setMeasurements] = useState<readonly Measurement[]>([])
|
|
429
|
+
const toggle = (next: 'section' | 'measure') =>
|
|
430
|
+
setMode((current) => (current === next ? null : next))
|
|
705
431
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
onClick={() => setSelected(feature.tag)}
|
|
432
|
+
return (
|
|
433
|
+
<ViewerToolbarProvider
|
|
434
|
+
controls={{
|
|
435
|
+
stock: { pressed: stock, onClick: () => setStock((shown) => !shown) },
|
|
436
|
+
section: { pressed: mode === 'section', onClick: () => toggle('section') },
|
|
437
|
+
measure: { pressed: mode === 'measure', onClick: () => toggle('measure') },
|
|
438
|
+
fit: { onClick: () => viewer.current?.fit() },
|
|
439
|
+
reset: { onClick: () => viewer.current?.reset() },
|
|
440
|
+
top: { onClick: () => viewer.current?.setView('top') },
|
|
441
|
+
}}
|
|
717
442
|
>
|
|
718
|
-
{
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
</
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
position in the array.
|
|
736
|
-
|
|
737
|
-
### Clicking the same face again to go through its matches
|
|
738
|
-
|
|
739
|
-
Clicking the same face repeatedly can step through its matching features, like most CAD tools do.
|
|
740
|
-
`focusForPick` handles this:
|
|
741
|
-
|
|
742
|
-
```tsx
|
|
743
|
-
const [focused, setFocused] = useState<string | null>(null)
|
|
744
|
-
const [lastRegion, setLastRegion] = useState<number | null>(null)
|
|
745
|
-
|
|
746
|
-
<EnginePart
|
|
747
|
-
report={report}
|
|
748
|
-
selection={focused ? [focused] : []}
|
|
749
|
-
onPick={(pick) => {
|
|
750
|
-
setFocused(focusForPick(pick, lastRegion, focused))
|
|
751
|
-
setLastRegion(pick.region)
|
|
752
|
-
}}
|
|
753
|
-
/>
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
A click on a different face starts again from that face's best match. To show the other matches
|
|
757
|
-
faintly, pass `pick.ranked` as `candidates`.
|
|
758
|
-
|
|
759
|
-
### Selecting several features with a modifier key
|
|
760
|
-
|
|
761
|
-
The viewer tells you which keys were held and leaves the behaviour to you:
|
|
762
|
-
|
|
763
|
-
```tsx
|
|
764
|
-
onPick={(pick) => {
|
|
765
|
-
const tag = pick.best
|
|
766
|
-
if (!tag) return
|
|
767
|
-
if (pick.modifiers.shift) setSelection((current) => [...current, tag])
|
|
768
|
-
else setSelection([tag])
|
|
769
|
-
}}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
A double-click on the part also re-centres the view. The second click still calls `onPick`, with
|
|
773
|
-
`pick.doubled` set to `true`. Ignore those clicks if a double-click shouldn't change your selection.
|
|
774
|
-
|
|
775
|
-
### Showing only one machining direction
|
|
776
|
-
|
|
777
|
-
Pass the same index to the arrows and the part. Clicks then only match features machined from that
|
|
778
|
-
direction. A face that can't be reached from that direction gives `pick.best === null`.
|
|
779
|
-
|
|
780
|
-
```tsx
|
|
781
|
-
<EnginePart report={report} activeDirection={direction} onPick={onPick} />
|
|
782
|
-
<DirectionArrows
|
|
783
|
-
directions={model.candidateDirections}
|
|
784
|
-
activeDirection={direction}
|
|
785
|
-
onPickDirection={setDirection}
|
|
786
|
-
/>
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
To list features by direction, use `groupByDirection(model)`. To label a direction, use
|
|
790
|
-
`directionLabel(direction)`, which gives `+Z`, `−X`, or `0.707, 0, 0.707`. To colour a direction
|
|
791
|
-
the same way as its arrow, use `directionColor(index)`.
|
|
792
|
-
|
|
793
|
-
### Showing sharp corners
|
|
794
|
-
|
|
795
|
-
`sharpCorners` draws a part's sharp corners as red bands painted on the two faces that meet there: strong where you can see them, faint
|
|
796
|
-
where the part is in front of them. **Which corners is the DFM rules' call, not the viewer's.** Pass
|
|
797
|
-
the features the rules flag with a sharp corner — those whose `featureSheet(datasheet).sharpCorners`
|
|
798
|
-
is set, from `@toolpath/dfm/model` — and the viewer draws each one's inside corners that run along
|
|
799
|
-
its machining direction: the vertical corners between its walls that no end mill makes. Nothing else
|
|
800
|
-
is drawn — not a hole's rim, not an outside edge, not a floor meeting a wall.
|
|
801
|
-
|
|
802
|
-
The mesh only says where those corners are. It needs the closed mesh the Engine produces (every edge
|
|
803
|
-
shared by exactly two triangles) and the report's regions, which tell a real edge from tessellation.
|
|
804
|
-
|
|
805
|
-
```tsx
|
|
806
|
-
const flagged = Object.entries(sheets)
|
|
807
|
-
.filter(([, sheet]) => sheet.sharpCorners)
|
|
808
|
-
.map(([tag]) => tag)
|
|
809
|
-
|
|
810
|
-
<ViewerToolbarProvider
|
|
811
|
-
controls={{ sharpCorners: { pressed: shown, onClick: () => setShown((on) => !on) } }}
|
|
812
|
-
>
|
|
813
|
-
<ViewerToolbar />
|
|
814
|
-
<Viewer>
|
|
815
|
-
<EnginePart report={report} sharpCorners={shown ? { features: flagged } : null} />
|
|
816
|
-
</Viewer>
|
|
817
|
-
</ViewerToolbarProvider>
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
`ViewerToolbar.SharpCornersButton` turns them on and off; there is nothing else to offer users.
|
|
821
|
-
Tags are matched without regard to case, so the lower-cased keys an application keeps its sheets
|
|
822
|
-
under work as they are.
|
|
823
|
-
|
|
824
|
-
With `hover` on, the pointer on a corner is on the corner: it takes the theme's `hover` colour at
|
|
825
|
-
the same width, the face under it is not painted as hovered, and the pick `onHover` reports carries it as
|
|
826
|
-
`pick.sharpCorner`. That is a `SharpCorner`: the flagged `feature` it belongs to, its `start` and `end`,
|
|
827
|
-
its `length` (how deep a tool would have to reach, mm), and the `angle` between its faces (90 for a
|
|
828
|
-
square corner). What a hover card says about it is the application's call; with the feature's sheet,
|
|
829
|
-
the widest tool that fits it (`pinchDiameter`) says what milling would leave there — that tool's radius.
|
|
830
|
-
|
|
831
|
-
```tsx
|
|
832
|
-
<HoverCard pick={pick}>
|
|
833
|
-
{(pick) =>
|
|
834
|
-
pick.sharpCorner ? (
|
|
835
|
-
<SharpCornerDetails corner={pick.sharpCorner} />
|
|
836
|
-
) : (
|
|
837
|
-
<FeatureDetails pick={pick} />
|
|
838
|
-
)
|
|
839
|
-
}
|
|
840
|
-
</HoverCard>
|
|
443
|
+
<div style={{ position: 'relative', height: 500 }}>
|
|
444
|
+
<Viewer ref={viewer}>
|
|
445
|
+
<PartMesh model={model} geometry={geometry} />
|
|
446
|
+
{stock && <Stock box={{ partGeometry: geometry, allowance: { wall: 3, floor: 3 } }} />}
|
|
447
|
+
{mode === 'section' && <SectionTool />}
|
|
448
|
+
{mode === 'measure' && (
|
|
449
|
+
<MeasureTool measurements={measurements} onChange={setMeasurements} />
|
|
450
|
+
)}
|
|
451
|
+
<Grid />
|
|
452
|
+
<Axes />
|
|
453
|
+
<ViewCube />
|
|
454
|
+
</Viewer>
|
|
455
|
+
<ViewerToolbar />
|
|
456
|
+
</div>
|
|
457
|
+
</ViewerToolbarProvider>
|
|
458
|
+
)
|
|
459
|
+
}
|
|
841
460
|
```
|
|
842
461
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
matter, and there is nothing left worth tuning. The colour is the `sharpCorner` theme key. To count
|
|
848
|
-
or measure the corners yourself, use `surfaceCreases(geometry, model)` and
|
|
849
|
-
`sharpCornerPositions(creases, model, options)`.
|
|
462
|
+
Call Fit after toggling stock if you want to frame the blank. When switching from
|
|
463
|
+
Section to Measure, the viewer's existing cut remains until cleared with Escape
|
|
464
|
+
while the section tool is active or `viewer.current?.setSection(null)`. The example
|
|
465
|
+
retains measurements in React state when the tool is unmounted.
|
|
850
466
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
467
|
+
With no children, `ViewerToolbar` renders configured controls in standard order.
|
|
468
|
+
For custom composition use `ViewerToolbar.Controls`, `ViewerToolbar.Divider`, and
|
|
469
|
+
its `*Button` components. A button without a matching provider control throws.
|
|
470
|
+
The supported action names are `stock`, `axes`, `grid`, `banana`, `tools`,
|
|
471
|
+
`directions`, `dfm`, `sharpCorners`, `hover`, `focus`, `wireframe`, `section`,
|
|
472
|
+
`measure`, `fit`, `reset`, and `top`; button names capitalize the action, such as
|
|
473
|
+
`ViewerToolbar.SharpCornersButton`. `useViewerToolbar()` reads those controls.
|
|
854
474
|
|
|
855
|
-
###
|
|
475
|
+
### Stock and display controls
|
|
856
476
|
|
|
857
|
-
|
|
858
|
-
|
|
477
|
+
`Stock` takes exactly one source. Stock stays in part coordinates, is included in
|
|
478
|
+
Fit/Reset, and does not intercept part picks or follow its section cut.
|
|
479
|
+
|
|
480
|
+
| Source | Input |
|
|
481
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
482
|
+
| `geometry` | A caller-owned `BufferGeometry`; dispose it yourself after use. |
|
|
483
|
+
| `glb` | An `ArrayBuffer` of GLB bytes you fetched. Keep the buffer reference stable. Stock decodes/disposes its own geometry. |
|
|
484
|
+
| `box` preview | `{ partGeometry, allowance, offset? }` or `{ partGeometry, dimensions, position?, positionOffset?, offset? }`. |
|
|
485
|
+
| `box` resolved | `{ lower, upper, frame? }`. |
|
|
486
|
+
| `cylinder` preview | `{ partGeometry, diameter, length, position?, positionOffset? }`. |
|
|
487
|
+
| `cylinder` resolved | `{ origin, axis, diameter, length }`; origin is the base center, axis points along its length. |
|
|
488
|
+
|
|
489
|
+
Allowances are **per side**: `{ wall, floor }` expands X/Y and Z respectively;
|
|
490
|
+
a number or `{ x, y, z }` is also accepted. The Workbench's 3 mm allowance is an
|
|
491
|
+
example, not a stock recommendation. Fixed preview `dimensions` are `{ x, y, z }`.
|
|
492
|
+
Positions are `model_centered`, `offset_from_top`, and `offset_from_bottom`, with
|
|
493
|
+
`positionOffset` in millimetres. Preview options and resolved figures are mutually
|
|
494
|
+
exclusive. Use `boxStockBounds` / `fixedBoxStockBounds` for corresponding dimensions.
|
|
495
|
+
|
|
496
|
+
Centered stock is centered on the part's bounds. For top positioning its upper Z
|
|
497
|
+
is `part.max.z + positionOffset`; for bottom positioning its lower Z is
|
|
498
|
+
`part.min.z - positionOffset`. These rules also position cylinder previews along Z.
|
|
499
|
+
|
|
500
|
+
A box's optional frame has `location`, `axis`, and `refDirection`. Its local point
|
|
501
|
+
maps to `location + x·refDirection + y·(axis × refDirection) + z·axis`; the axis is
|
|
502
|
+
normalized and the reference direction projected perpendicular and normalized.
|
|
503
|
+
Without a frame, corners are already in part coordinates. A cylinder preview is
|
|
504
|
+
Z-aligned and centered on the part's bounding box. Treat previews as visualization;
|
|
505
|
+
prefer the operation's resolved stock when the selected API contract supplies it.
|
|
506
|
+
|
|
507
|
+
Negative/non-finite allowances, invalid coordinates, degenerate frames/axes, and
|
|
508
|
+
non-positive dimensions throw `RangeError`. GLB node transforms are applied without
|
|
509
|
+
centering, rescaling, or axis conversion. Stock does not fetch, cache, decompress
|
|
510
|
+
HTTP responses itself, or need an analysis report; the browser handles HTTP gzip.
|
|
511
|
+
|
|
512
|
+
Appearance props are `color`, `emissive`, `flatShading`, `opacity` (default 0.2),
|
|
513
|
+
`edgeColor`, `edgeOpacity`, and `showEdges`. `renderStyle="legacy-workpiece"` selects
|
|
514
|
+
the legacy two-pass material/edge appearance. With `showEdges={false}`, outlines
|
|
515
|
+
are not built until needed. `retainPrevious` applies to GLB playback: keep the
|
|
516
|
+
previous decode visible until the replacement is ready. Key the stock subtree by
|
|
517
|
+
part/calculation identity to prevent carrying geometry into a different run.
|
|
518
|
+
Without it, changing GLB bytes hides the previous geometry during decoding.
|
|
519
|
+
|
|
520
|
+
**Migration from 1.x:** replace removed `BoxStock` with `Stock`'s `box` source.
|
|
521
|
+
`StockProps` is a union; use an intersection type rather than extending it with an
|
|
522
|
+
interface. `StockAppearanceProps` describes just the shared appearance options.
|
|
523
|
+
|
|
524
|
+
### Direction coloring
|
|
525
|
+
|
|
526
|
+
Keep the same candidate index in `PartMesh.activeDirection` and the arrows. Use
|
|
527
|
+
`shownDirection={null}` for all arrows, an index/list to choose arrows, or `-1`
|
|
528
|
+
for none. `activeDirection` also scopes arrows when `shownDirection` is omitted.
|
|
529
|
+
`previewDirection` and `namedDirections` add non-candidate directions.
|
|
859
530
|
|
|
860
531
|
```tsx
|
|
861
|
-
|
|
532
|
+
import { useMemo, useState } from 'react'
|
|
533
|
+
import type { BufferGeometry } from 'three'
|
|
534
|
+
import {
|
|
535
|
+
DirectionArrows,
|
|
536
|
+
PartMesh,
|
|
537
|
+
Viewer,
|
|
538
|
+
directionHighlights,
|
|
539
|
+
type PartModel,
|
|
540
|
+
} from '@toolpath/viewer'
|
|
862
541
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
542
|
+
export const DirectionViewer = ({
|
|
543
|
+
model,
|
|
544
|
+
geometry,
|
|
545
|
+
}: {
|
|
546
|
+
model: PartModel
|
|
547
|
+
geometry: BufferGeometry
|
|
548
|
+
}) => {
|
|
549
|
+
const [direction, setDirection] = useState<number | null>(null)
|
|
550
|
+
const colors = useMemo(() => directionHighlights(model, direction), [model, direction])
|
|
551
|
+
return (
|
|
552
|
+
<Viewer style={{ height: 400 }}>
|
|
553
|
+
<PartMesh
|
|
554
|
+
model={model}
|
|
555
|
+
geometry={geometry}
|
|
556
|
+
activeDirection={direction}
|
|
557
|
+
regionHighlights={colors}
|
|
558
|
+
/>
|
|
559
|
+
<DirectionArrows
|
|
560
|
+
directions={model.candidateDirections}
|
|
561
|
+
shownDirection={direction}
|
|
562
|
+
onPickDirection={(index) => setDirection((current) => (current === index ? null : index))}
|
|
563
|
+
/>
|
|
564
|
+
</Viewer>
|
|
565
|
+
)
|
|
566
|
+
}
|
|
868
567
|
```
|
|
869
568
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
3. `candidates` (faint, per direction)
|
|
875
|
-
4. `selection` (orange)
|
|
876
|
-
5. `pickedRegions`
|
|
877
|
-
6. `hoveredFeatureIds`
|
|
878
|
-
7. the face under the pointer
|
|
879
|
-
|
|
880
|
-
`weight` (`0`–`1`, default `0.7`) sets how strongly a highlight covers the part.
|
|
881
|
-
|
|
882
|
-
Changing colours is fast, even when every feature on a large part is highlighted.
|
|
569
|
+
Arrows point toward the part. Picking an arrow does not clear selection through
|
|
570
|
+
`onPointerMissed`. Use `groupByDirection`, `directionLabel`, and `directionColor`
|
|
571
|
+
for a matching list/legend. Wireframe and direction coloring are independent props;
|
|
572
|
+
any mutual exclusion in the example app is application behavior.
|
|
883
573
|
|
|
884
574
|
### Section view
|
|
885
575
|
|
|
886
|
-
|
|
887
|
-
|
|
576
|
+
Mount `SectionTool` inside `Viewer` beside a part with no explicit `section` prop.
|
|
577
|
+
It previews the surface under the pointer, places a cut on click, and offers X/Y/Z
|
|
578
|
+
planes. A drag handle moves the cut; Escape clears it while the tool is mounted.
|
|
579
|
+
It writes the viewer-owned section store and suppresses normal part hover/picks
|
|
580
|
+
while active. Unmounting the tool restores part interactions, not the uncut solid.
|
|
888
581
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
<EnginePart report={report} />
|
|
892
|
-
{sectioning ? <SectionTool /> : null}
|
|
893
|
-
</Viewer>
|
|
894
|
-
```
|
|
895
|
-
|
|
896
|
-
Hovering the part previews a cut through the face under the pointer; clicking places it. Three
|
|
897
|
-
coloured planes stand behind the part — click one to cut along that axis, in from your side. Once
|
|
898
|
-
there is a cut, a framed plane and two-way normal-axis handle show exactly what will move; drag the
|
|
899
|
-
handle to move it. Keep the canvas clear by presenting `sectionMeasurement(state)` beside the host
|
|
900
|
-
application's section slider; it reports the depth from a picked surface, or the distance swept
|
|
901
|
-
through the part bounds. Escape clears it.
|
|
902
|
-
The cut belongs to the viewer: `viewer.current.setSection(null)` clears it from a button outside
|
|
903
|
-
the canvas, `setSection({ enabled: true, normal, offset })` sets one, and `onSectionChange` on
|
|
904
|
-
`PartMesh` still reports every move.
|
|
905
|
-
|
|
906
|
-
While the tool is mounted, the part reports no hovers or picks — clicking a face cuts through it
|
|
907
|
-
without also selecting it. Unmount the tool, as above, and they come back.
|
|
908
|
-
|
|
909
|
-
To drive the cut yourself instead, pass `section`. `normal` points toward the half you keep.
|
|
910
|
-
`offset` goes from `0` (whole part) to `1` (fully cut away).
|
|
582
|
+
For application-controlled cuts, supply `section` directly. Here `normal` points
|
|
583
|
+
toward the retained half; `offset` ranges from 0 (whole part) to 1 (cut away).
|
|
911
584
|
|
|
912
585
|
```tsx
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
586
|
+
import { useState } from 'react'
|
|
587
|
+
import type { BufferGeometry } from 'three'
|
|
588
|
+
import { PartMesh, Viewer, type PartModel } from '@toolpath/viewer'
|
|
589
|
+
|
|
590
|
+
export const SectionViewer = ({
|
|
591
|
+
model,
|
|
592
|
+
geometry,
|
|
593
|
+
}: {
|
|
594
|
+
model: PartModel
|
|
595
|
+
geometry: BufferGeometry
|
|
596
|
+
}) => {
|
|
597
|
+
const [offset, setOffset] = useState(0.5)
|
|
598
|
+
return (
|
|
599
|
+
<>
|
|
600
|
+
<label>
|
|
601
|
+
Section offset
|
|
602
|
+
<input
|
|
603
|
+
type="range"
|
|
604
|
+
min="0"
|
|
605
|
+
max="1"
|
|
606
|
+
step="0.01"
|
|
607
|
+
value={offset}
|
|
608
|
+
onChange={(event) => setOffset(Number(event.target.value))}
|
|
609
|
+
/>
|
|
610
|
+
</label>
|
|
611
|
+
<Viewer style={{ height: 400 }}>
|
|
612
|
+
<PartMesh
|
|
613
|
+
model={model}
|
|
614
|
+
geometry={geometry}
|
|
615
|
+
section={{ enabled: true, normal: { x: 0, y: 0, z: 1 }, offset }}
|
|
616
|
+
onSectionChange={(state) => setOffset(state.offset)}
|
|
617
|
+
/>
|
|
618
|
+
</Viewer>
|
|
619
|
+
</>
|
|
620
|
+
)
|
|
621
|
+
}
|
|
925
622
|
```
|
|
926
623
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
`depth` moves it into the part, in millimetres:
|
|
934
|
-
|
|
935
|
-
```tsx
|
|
936
|
-
const [plane, setPlane] = useState<SectionPlacement | null>(null)
|
|
937
|
-
|
|
938
|
-
<EnginePart
|
|
939
|
-
report={report}
|
|
940
|
-
section={{ enabled: plane !== null, plane, depth: 1 }}
|
|
941
|
-
onPick={({ point: [x, y, z], normal: [nx, ny, nz] }) =>
|
|
942
|
-
setPlane(sectionFromPick({ point: { x, y, z }, normal: { x: nx, y: ny, z: nz } }))
|
|
943
|
-
}
|
|
944
|
-
/>
|
|
945
|
-
```
|
|
624
|
+
Passing `onSectionChange` also enables the controlled cut's drag handle. The callback
|
|
625
|
+
reports `SectionState`, including `enabled`, `normal`, `offset`, `plane`, `depth`,
|
|
626
|
+
`constant`, and depth bounds. Use `sectionFromPick({ point, normal })` to create a
|
|
627
|
+
surface placement, then `{ enabled: true, plane, depth }` to cut inward by a depth
|
|
628
|
+
in millimetres. Pick tuples must be converted to `{ x, y, z }` for that helper.
|
|
629
|
+
`sectionMeasurement(state)` supplies a readout for your own slider or label.
|
|
946
630
|
|
|
947
631
|
### Measuring
|
|
948
632
|
|
|
949
|
-
|
|
633
|
+
`MeasureTool` mounts inside `Viewer`. Two clicks measure distance; three measure an
|
|
634
|
+
angle, with the vertex as the middle point. Snapping prefers corners, edge
|
|
635
|
+
midpoints, edges, then faces. `mode` is `distance` or `angle`; `showDeltas={false}`
|
|
636
|
+
hides a distance's X/Y/Z legs. `onChange` reports `readonly Measurement[]`; pass
|
|
637
|
+
`measurements` too to control and retain the list, as the Workbench does.
|
|
950
638
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
</Viewer>
|
|
960
|
-
```
|
|
639
|
+
Shift constrains the next point to an axis. Escape cancels points in progress;
|
|
640
|
+
Delete/Backspace removes the last measurement unless focus is in a form field.
|
|
641
|
+
Normal part hover/picks are suppressed while measuring. Without controlled state,
|
|
642
|
+
unmounting the tool drops its measurements.
|
|
643
|
+
|
|
644
|
+
The tool measures the visible sectioned geometry, including the cap and clipped
|
|
645
|
+
edges. If a mounted SectionTool has no cut yet, measuring waits so its first
|
|
646
|
+
placement click is not also a measurement. Existing measurements remain visible.
|
|
961
647
|
|
|
962
|
-
|
|
963
|
-
on an edge, or a point on the face, in that order of preference. It snaps to the edges the part
|
|
964
|
-
draws — the boundaries between analytic surfaces — so a corner is a corner of the part, not of a
|
|
965
|
-
mesh triangle. Click twice for a distance or three times for an angle. The line follows the pointer
|
|
966
|
-
between clicks with a live readout.
|
|
967
|
-
|
|
968
|
-
A distance is labelled with its length and, unless it already runs along an axis, broken into its
|
|
969
|
-
X, Y and Z parts as dashed legs in the axis colours with a label each. `showDeltas={false}` turns
|
|
970
|
-
the legs off. An angle draws its two arms and an arc, labelled in degrees.
|
|
971
|
-
|
|
972
|
-
`onChange` reports the list whenever it changes. Each entry is a `Measurement`: `kind`, an `id`,
|
|
973
|
-
and its `points` — two for a distance, three for an angle with the vertex in the middle. To own the
|
|
974
|
-
list, pass `measurements` too; to clear it, unmount the tool or pass `[]`. `measurementLabel(m)`
|
|
975
|
-
writes an entry the way the tool does, and `distanceBetween`, `deltaBetween` and `angleAt` are the
|
|
976
|
-
arithmetic behind it.
|
|
977
|
-
|
|
978
|
-
**Styling the labels.** Every label is a `<div class="toolpath-measure-label">` with
|
|
979
|
-
`data-measure-label` set to `distance`, `angle`, `delta` (one leg of a distance's X, Y, Z breakdown,
|
|
980
|
-
with `data-axis`) or `live` (the readout that follows the pointer), and `data-measurement-id` on
|
|
981
|
-
anything belonging to a finished measurement. Nothing else is set: no font, no colour, no
|
|
982
|
-
background. `labelClassName` adds your own class, for Tailwind or a CSS module. A stylesheet to
|
|
983
|
-
start from, which is what the example uses:
|
|
648
|
+
Labels are unstyled DOM overlays with class `toolpath-measure-label`; add CSS such as:
|
|
984
649
|
|
|
985
650
|
```css
|
|
986
651
|
.toolpath-measure-label {
|
|
@@ -988,355 +653,300 @@ start from, which is what the example uses:
|
|
|
988
653
|
font:
|
|
989
654
|
600 12px/1.2 system-ui,
|
|
990
655
|
sans-serif;
|
|
991
|
-
color:
|
|
992
|
-
background:
|
|
656
|
+
color: white;
|
|
657
|
+
background: #141721dd;
|
|
993
658
|
border: 1px solid #4f8ef7;
|
|
994
659
|
border-radius: 4px;
|
|
995
660
|
padding: 2px 6px;
|
|
996
|
-
transform: translateY(-16px);
|
|
997
661
|
}
|
|
998
662
|
.toolpath-measure-label[data-measure-label='delta'] {
|
|
999
663
|
font-size: 11px;
|
|
1000
|
-
transform: translateY(14px);
|
|
1001
|
-
}
|
|
1002
|
-
.toolpath-measure-label[data-axis='x'] {
|
|
1003
|
-
border-color: #ff6b6b;
|
|
1004
664
|
}
|
|
1005
665
|
```
|
|
1006
666
|
|
|
1007
|
-
|
|
1008
|
-
|
|
667
|
+
`data-measure-label` identifies `distance`, `angle`, `delta`, `live`, or tool labels;
|
|
668
|
+
axis legs also have `data-axis`, and finished labels have `data-measurement-id`.
|
|
669
|
+
`labelClassName` adds a class. Labels do not take pointer events. Lengths default to
|
|
670
|
+
millimetres; the `format(mm)` callback changes their displayed string, not geometry
|
|
671
|
+
units or angles (which remain degrees).
|
|
1009
672
|
|
|
1010
|
-
|
|
1011
|
-
convert, so pass `format` to show anything else:
|
|
673
|
+
### Showing sharp corners and tool marks
|
|
1012
674
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
is an edge with corners of its own, and the capped face is a surface a point can land on. The
|
|
1021
|
-
sample follows the cut — drag the handle or set a depth and the next pointer move sees the new cut.
|
|
1022
|
-
It reads the plane off the part's own material, so a controlled `section` prop works the same way.
|
|
1023
|
-
Beside a `<SectionTool>` that has no cut yet, the measure tool waits: it offers no snap and places
|
|
1024
|
-
no point until a cut is chosen or the section tool is unmounted, so the click that picks the cut is
|
|
1025
|
-
not also the first point of a measurement. Measurements already made stay up meanwhile.
|
|
1026
|
-
|
|
1027
|
-
Hold Shift to hold the next point to the X, Y or Z line through the last one — whichever axis the
|
|
1028
|
-
pointer is furthest along — so a length along an edge is measured square rather than slightly across
|
|
1029
|
-
it. The line wears that axis's colour while it is held, and the finished distance has no legs to
|
|
1030
|
-
show. Delete or Backspace removes the last measurement, unless focus is in a field. Escape drops the
|
|
1031
|
-
points of a measurement in progress. While the tool is mounted the part reports no hovers or picks;
|
|
1032
|
-
unmounting it hands the pointer back.
|
|
1033
|
-
|
|
1034
|
-
### Theming
|
|
1035
|
-
|
|
1036
|
-
```tsx
|
|
1037
|
-
<Viewer theme={{ ambientIntensity: 1.5 }}>
|
|
1038
|
-
<EnginePart report={report} theme={{ part: 0xe5e7eb, highlight: 0x2563eb, hover: 0x93c5fd }} />
|
|
1039
|
-
<ViewCube theme={{ cube: 0x1f2937, cubeLabel: 0xffffff }} />
|
|
1040
|
-
</Viewer>
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
Colours are hex numbers. Lighting goes on `<Viewer>`, part colours go on the part, and cube colours
|
|
1044
|
-
go on the cube. See `DEFAULT_THEME` for every key. The default part colours are tuned for the
|
|
1045
|
-
default lighting, so if you change one, check the other.
|
|
1046
|
-
|
|
1047
|
-
The section cut is themed on the part too: `sectionCap` and `sectionHatch` are the cap's fill and
|
|
1048
|
-
hatch lines, `sectionHandle` overrides the drag arrow's colour, and `sectionOutline` the
|
|
1049
|
-
cutting-plane sheet and preview that `<SectionTool>` draws. Without a `sectionHandle` override, the
|
|
1050
|
-
drag arrow uses the cutting plane's direction colour. `measure` and `measureSnap` are
|
|
1051
|
-
`<MeasureTool>`'s lines and its snap indicator; a distance's X, Y, Z legs take `AXIS_COLORS`, the
|
|
1052
|
-
same hues as the section tool's global planes. `sharpCorner` is the colour of
|
|
1053
|
-
[sharp corners](#showing-sharp-corners).
|
|
1054
|
-
|
|
1055
|
-
`HIGHLIGHT_COLORS` has the standard selection colours (`default`, `toolIssue`, `geometryIssue`).
|
|
1056
|
-
`DIRECTION_COLORS` has the 9 direction colours, which repeat for parts with more than 9 directions.
|
|
1057
|
-
|
|
1058
|
-
### Drawing your own objects in the scene
|
|
1059
|
-
|
|
1060
|
-
Anything React Three Fiber can draw can go inside `<Viewer>`, and the camera includes it when it
|
|
1061
|
-
frames the scene. To leave an object out of framing, as `<Grid>` does, set
|
|
1062
|
-
`userData={{ [EXCLUDE_FROM_FRAME]: true }}`.
|
|
1063
|
-
|
|
1064
|
-
Use `useContentBox()` inside the viewer to get the part's bounding box, for example to size your own
|
|
1065
|
-
object.
|
|
1066
|
-
|
|
1067
|
-
## Errors
|
|
1068
|
-
|
|
1069
|
-
These are all thrown, so an error boundary catches them.
|
|
1070
|
-
|
|
1071
|
-
| Error | Import from | When |
|
|
1072
|
-
| ------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
1073
|
-
| `PartReportFormatError` | `@toolpath/viewer` | The report is malformed. `error.issues` lists every problem. |
|
|
1074
|
-
| `UnsupportedKernelVersionError` | `@toolpath/viewer` | The report is from an Engine kernel older than `0.3.0`. Re-analyze the part. |
|
|
1075
|
-
| `PartMeshError` | `@toolpath/viewer/engine` | The mesh download failed (usually an expired URL: fetch the report again), or the mesh doesn't match the report. |
|
|
1076
|
-
| `AggregateError` | — | Both the GLB and the STL failed. `error.errors` has one error per file. |
|
|
675
|
+
DFM decisions belong to the application. Pass flagged feature tags as
|
|
676
|
+
`sharpCorners={{ features: tags }}`; the viewer draws qualifying inside corners
|
|
677
|
+
along those features' machining directions. It does not flag features or guess
|
|
678
|
+
missing manufacturing data. Use a closed mesh with report region boundaries;
|
|
679
|
+
`findSharpCorners`/`sharpCornerPositions` operate on `surfaceCreases` for custom
|
|
680
|
+
inspection. A hovered corner appears as `pick.sharpCorner` with its feature,
|
|
681
|
+
endpoints, length, and angle. Feature tag matching for these flags is case-insensitive.
|
|
1077
682
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
function PartError({ error }: { error: Error }) {
|
|
1082
|
-
if (error instanceof UnsupportedKernelVersionError) return <p>Re-analyze this part to view it.</p>
|
|
1083
|
-
if (error instanceof PartReportFormatError)
|
|
1084
|
-
return <p>Invalid report: {error.issues.join(', ')}</p>
|
|
1085
|
-
return <p>Could not load this part. Refresh to try again.</p>
|
|
1086
|
-
}
|
|
1087
|
-
```
|
|
1088
|
-
|
|
1089
|
-
The viewer does not read STEP files. The Engine analyzes STEP files and produces the mesh the
|
|
1090
|
-
viewer displays.
|
|
1091
|
-
|
|
1092
|
-
## Working with the Toolpath API
|
|
1093
|
-
|
|
1094
|
-
Use [`@toolpath/api`](https://www.npmjs.com/package/@toolpath/api) to get parts. Call it **from your
|
|
1095
|
-
server**, so your API key stays private, and pass the result to the browser.
|
|
1096
|
-
|
|
1097
|
-
The API can give you two things to show:
|
|
1098
|
-
|
|
1099
|
-
- **An analyzed part** (`getPart`): faces and features. Use it with `<EnginePart>`.
|
|
1100
|
-
- **A display mesh** (`getPartMesh`): faces only, but much faster, because the part isn't
|
|
1101
|
-
analyzed. Use it with `<PartMesh>`.
|
|
1102
|
-
|
|
1103
|
-
Both need a part whose CAD file is already uploaded. Their mesh URLs expire after 15 minutes, so
|
|
1104
|
-
don't cache the responses.
|
|
1105
|
-
|
|
1106
|
-
### Analyzed part
|
|
683
|
+
Mount `ToolMarks` under a part to illustrate supplied cutters or profiles. For
|
|
684
|
+
example, this is a 6 mm bull-nose cutter standing along +Z:
|
|
1107
685
|
|
|
1108
686
|
```ts
|
|
1109
|
-
|
|
1110
|
-
import { createToolpathClient } from '@toolpath/api'
|
|
1111
|
-
|
|
1112
|
-
const api = createToolpathClient({ apiKey: process.env.TOOLPATH_API_KEY! })
|
|
687
|
+
import type { ToolMark } from '@toolpath/viewer'
|
|
1113
688
|
|
|
1114
|
-
export const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
689
|
+
export const toolMarks: Array<ToolMark> = [
|
|
690
|
+
{
|
|
691
|
+
base: { x: 12, y: 4, z: -8 },
|
|
692
|
+
axis: { x: 0, y: 0, z: 1 },
|
|
693
|
+
diameter: 6,
|
|
694
|
+
height: 8,
|
|
695
|
+
cornerRadius: 0.5,
|
|
696
|
+
label: '⌀ 6 mm',
|
|
697
|
+
note: 'Bull nose R 0.5',
|
|
698
|
+
},
|
|
699
|
+
]
|
|
1122
700
|
```
|
|
1123
701
|
|
|
1124
|
-
|
|
702
|
+
Use `ToolMarks`'s `marks` prop for that array. A `ToolMark` can instead supply a
|
|
703
|
+
rotational `profile` of `{ r, z }` points. `visible` toggles display;
|
|
704
|
+
`labelClassName` styles labels. Marks use the measure-label CSS hook with
|
|
705
|
+
`data-measure-label="tool"`, optional value/note spans, take no pointer events,
|
|
706
|
+
and are excluded from camera framing. They do not read datasheets themselves.
|
|
1125
707
|
|
|
1126
|
-
|
|
708
|
+
### Theming
|
|
1127
709
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
710
|
+
Colors in `ViewerTheme` are numeric hex values. Set lighting on `Viewer`, part
|
|
711
|
+
colors on `PartMesh`/`EnginePart`, and overlay colors on their components; passing
|
|
712
|
+
part colors only to `Viewer` does not theme its children automatically.
|
|
713
|
+
`DEFAULT_THEME` lists the keys. Selection palettes are `HIGHLIGHT_COLORS`; direction
|
|
714
|
+
colors cycle through `DIRECTION_COLORS`.
|
|
715
|
+
|
|
716
|
+
Part keys include `part`, `partEmissive`, `hover`, `highlight`, `picked`, `edge`,
|
|
717
|
+
`edgeOpacity`, and `sharpCorner`. Section keys include `sectionCap`, `sectionHatch`,
|
|
718
|
+
`sectionOutline`, `sectionHandle`, and `sectionHandleOutline`. Measurement tools
|
|
719
|
+
use `measure`/`measureSnap`; cube keys are `cube`, `cubeEdge`, and `cubeLabel`.
|
|
720
|
+
The canvas background comes from the wrapper/parent, independently of those colors.
|
|
721
|
+
|
|
722
|
+
Stable DOM classes include `viewer-root`, `viewer-canvas-container`,
|
|
723
|
+
`viewer-canvas-frame`, `viewer-canvas`, `viewer-hover-card`, `viewer-toolbar-stack`,
|
|
724
|
+
`viewer-toolbar`, `viewer-toolbar-button`, `viewer-toolbar-icon`,
|
|
725
|
+
`viewer-toolbar-tooltip`, and `viewer-toolbar-divider`. Caller `className` values
|
|
726
|
+
append to the component's hook. Corresponding `data-viewer-*` attributes identify
|
|
727
|
+
the DOM boundaries; `data-viewer-toolbar-action` contains the control's action name.
|
|
728
|
+
Style the hover card yourself; it is a pointer-events-free fixed-position shell.
|
|
729
|
+
|
|
730
|
+
Custom R3F objects can go inside `Viewer` and participate in framing. Set
|
|
731
|
+
`userData={{ [EXCLUDE_FROM_FRAME]: true }}` to exclude an object. Use the exported
|
|
732
|
+
`EXCLUDE_FROM_FRAME` constant, not a guessed string.
|
|
1143
733
|
|
|
1144
|
-
|
|
734
|
+
## Working with the Toolpath API
|
|
1145
735
|
|
|
1146
|
-
|
|
1147
|
-
|
|
736
|
+
Keep analysis/job orchestration outside the viewer. A server-side
|
|
737
|
+
[`@toolpath/api`](https://www.npmjs.com/package/@toolpath/api) integration can fetch a
|
|
738
|
+
completed report and pass it to the browser. Deliberate browser API access needs
|
|
739
|
+
an appropriately scoped, allowed-origin key; a browser-delivered key is observable
|
|
740
|
+
and is not made secret by CORS. See the [authentication guide](https://developers.toolpath.com/authentication).
|
|
741
|
+
The viewer's mesh loader does not attach an Engine API key to artifact requests.
|
|
742
|
+
|
|
743
|
+
### Analyzed part versus display mesh
|
|
744
|
+
|
|
745
|
+
A completed analysis report includes features and regions for `EnginePart`.
|
|
746
|
+
A display-mesh response has geometry and face counts without analysis features;
|
|
747
|
+
it is not an `EnginePart` report. Use `PartMesh` with a model whose region ranges
|
|
748
|
+
come from that display mesh's `faceTriangleCounts`. Without them, a single region
|
|
749
|
+
can cover all triangles. Such a model has no feature owners, so `pick.best` is
|
|
750
|
+
`null`; use `pickedRegions` and `regionHighlights` for face feedback. Its face
|
|
751
|
+
indices do not identify analysis-report regions, and synthetic area values do not
|
|
752
|
+
establish analytic measurements or DFM results.
|
|
753
|
+
|
|
754
|
+
### Mesh loading and ownership
|
|
755
|
+
|
|
756
|
+
For custom fetching, backend proxies, or local files, use `/engine` loaders and
|
|
757
|
+
render `PartMesh`. `EnginePart` itself has no custom-fetch prop.
|
|
758
|
+
`loadPartMesh(mesh, { fetch?, signal? })` tries GLB then STL;
|
|
759
|
+
`loadPartGeometry(url, counts, { format?, fetch?, signal? })` loads one artifact;
|
|
760
|
+
`parsePartGeometry(bytes, counts, format?)` parses bytes without fetching.
|
|
761
|
+
All produce caller-owned, non-indexed geometry, preserve triangle ordering and
|
|
762
|
+
coordinates, and validate counts against the supplied expectation. Call
|
|
763
|
+
`smoothRegionNormals(geometry, model.regions)` to smooth curved regions while
|
|
764
|
+
preserving their boundaries.
|
|
765
|
+
|
|
766
|
+
The counts argument is `{ pointCount, triangleCount }`, as in `model.mesh`; raw
|
|
767
|
+
API fields named `meshPointCount`/`meshTriangleCount` must be mapped deliberately.
|
|
768
|
+
|
|
769
|
+
This loader handles cancellation, failures, changed reports, and owned geometry:
|
|
1148
770
|
|
|
1149
771
|
```tsx
|
|
1150
|
-
// browser
|
|
1151
772
|
import { useEffect, useMemo, useState } from 'react'
|
|
1152
773
|
import type { BufferGeometry } from 'three'
|
|
1153
|
-
import type
|
|
1154
|
-
import {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const triangles = { start, end: start + count }
|
|
1161
|
-
start += count
|
|
1162
|
-
return { idx, splitOrigin: idx, shapeKind: 'Unknown', area: 0, triangles }
|
|
1163
|
-
})
|
|
1164
|
-
const triangleCount = mesh.meshTriangleCount
|
|
1165
|
-
|
|
1166
|
-
return {
|
|
1167
|
-
partId: mesh.partId,
|
|
1168
|
-
kernelVersion: '',
|
|
1169
|
-
features: [],
|
|
1170
|
-
regions,
|
|
1171
|
-
candidateDirections: [],
|
|
1172
|
-
mesh: {
|
|
1173
|
-
pointCount: mesh.meshPointCount,
|
|
1174
|
-
triangleCount,
|
|
1175
|
-
glbUrl: mesh.meshGlbUrl,
|
|
1176
|
-
stlUrl: null,
|
|
1177
|
-
thumbnailUrl: null,
|
|
1178
|
-
},
|
|
1179
|
-
regionIndex: buildRegionIndex({ regions, features: [], triangleCount }),
|
|
1180
|
-
warnings: [],
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
export function MeshViewer({ mesh }: { mesh: PartMeshResponse }) {
|
|
1185
|
-
const model = useMemo(() => toModel(mesh), [mesh])
|
|
1186
|
-
const [geometry, setGeometry] = useState<BufferGeometry | null>(null)
|
|
774
|
+
import { PartMesh, Viewer, type PartModel } from '@toolpath/viewer'
|
|
775
|
+
import {
|
|
776
|
+
loadPartMesh,
|
|
777
|
+
normalizePartReport,
|
|
778
|
+
smoothRegionNormals,
|
|
779
|
+
type FetchLike,
|
|
780
|
+
} from '@toolpath/viewer/engine'
|
|
1187
781
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}, [model])
|
|
782
|
+
type Loaded = { model: PartModel; request: FetchLike | undefined; geometry: BufferGeometry }
|
|
783
|
+
type Failure = { model: PartModel; request: FetchLike | undefined; error: Error }
|
|
1191
784
|
|
|
1192
|
-
|
|
785
|
+
export const ManagedPart = ({ report, request }: { report: unknown; request?: FetchLike }) => {
|
|
786
|
+
const model = useMemo(() => normalizePartReport(report), [report])
|
|
787
|
+
const [loaded, setLoaded] = useState<Loaded | null>(null)
|
|
788
|
+
const [failure, setFailure] = useState<Failure | null>(null)
|
|
1193
789
|
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
const controller = new AbortController()
|
|
792
|
+
let active = true
|
|
793
|
+
let owned: BufferGeometry | undefined
|
|
794
|
+
const load = async () => {
|
|
795
|
+
const geometry = await loadPartMesh(model.mesh, { fetch: request, signal: controller.signal })
|
|
796
|
+
if (!active) {
|
|
797
|
+
geometry.dispose()
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
smoothRegionNormals(geometry, model.regions)
|
|
802
|
+
} catch (error) {
|
|
803
|
+
geometry.dispose()
|
|
804
|
+
throw error
|
|
805
|
+
}
|
|
806
|
+
owned = geometry
|
|
807
|
+
setLoaded({ model, request, geometry })
|
|
808
|
+
}
|
|
809
|
+
void load().catch((error: unknown) => {
|
|
810
|
+
if (active) {
|
|
811
|
+
setFailure({
|
|
812
|
+
model,
|
|
813
|
+
request,
|
|
814
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
815
|
+
})
|
|
816
|
+
}
|
|
817
|
+
})
|
|
818
|
+
return () => {
|
|
819
|
+
active = false
|
|
820
|
+
controller.abort()
|
|
821
|
+
owned?.dispose()
|
|
822
|
+
}
|
|
823
|
+
}, [model, request])
|
|
824
|
+
|
|
825
|
+
if (failure?.model === model && failure.request === request) throw failure.error
|
|
826
|
+
if (loaded?.model !== model || loaded.request !== request) return <p>Loading mesh…</p>
|
|
1194
827
|
return (
|
|
1195
|
-
<Viewer style={{ height:
|
|
1196
|
-
<PartMesh model={model} geometry={geometry}
|
|
828
|
+
<Viewer style={{ height: 400 }}>
|
|
829
|
+
<PartMesh model={model} geometry={loaded.geometry} />
|
|
1197
830
|
</Viewer>
|
|
1198
831
|
)
|
|
1199
832
|
}
|
|
1200
833
|
```
|
|
1201
834
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
## Using it without the Engine
|
|
1209
|
-
|
|
1210
|
-
`<PartMesh>` takes a `PartModel` (the part's faces and features) and a three.js `BufferGeometry`
|
|
1211
|
-
(the mesh). Use it to show a local file, a test fixture, or geometry you created yourself.
|
|
1212
|
-
|
|
1213
|
-
```tsx
|
|
1214
|
-
import * as THREE from 'three'
|
|
1215
|
-
import { PartMesh, Viewer, buildRegionIndex, type PartModel } from '@toolpath/viewer'
|
|
835
|
+
Use an error boundary around `ManagedPart`, as in the quick start. Keep an injected
|
|
836
|
+
`request` function stable; changing it starts a new load. The component hides the
|
|
837
|
+
previous model while the replacement loads, aborts on cleanup, and disposes a
|
|
838
|
+
late result if the loader completed after cancellation. It does not use the shared
|
|
839
|
+
Engine geometry cache.
|
|
1216
840
|
|
|
1217
|
-
|
|
1218
|
-
const geometry = new THREE.BoxGeometry(20, 20, 20).toNonIndexed()
|
|
841
|
+
### Expiring URLs and cache
|
|
1219
842
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
triangles: { start: idx * 2, end: idx * 2 + 2 }, // [start, end)
|
|
1226
|
-
}))
|
|
843
|
+
Artifact URLs are short-lived. Fetch promptly and obtain fresh URLs through the
|
|
844
|
+
owning API when needed; do not store them as durable identifiers or log them. URL
|
|
845
|
+
expiry belongs to the signed URL, not the age of the immutable analysis result.
|
|
846
|
+
An error-boundary reset alone cannot renew an expired URL. Browser artifact fetches
|
|
847
|
+
also need the artifact host's CORS permission, independently of API-key origins.
|
|
1227
848
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
tag: 'sides',
|
|
1238
|
-
featureType: 'wall',
|
|
1239
|
-
machiningDirection: { x: 0, y: 0, z: 1 },
|
|
1240
|
-
axis: null,
|
|
1241
|
-
regionIdxs: [0, 1, 2, 3],
|
|
1242
|
-
},
|
|
1243
|
-
]
|
|
1244
|
-
|
|
1245
|
-
const model: PartModel = {
|
|
1246
|
-
partId: 'cube',
|
|
1247
|
-
kernelVersion: '0.3.0',
|
|
1248
|
-
features,
|
|
1249
|
-
regions,
|
|
1250
|
-
candidateDirections: [{ x: 0, y: 0, z: 1 }],
|
|
1251
|
-
mesh: { pointCount: 36, triangleCount: 12, glbUrl: null, stlUrl: null, thumbnailUrl: null },
|
|
1252
|
-
regionIndex: buildRegionIndex({ regions, features, triangleCount: 12 }),
|
|
1253
|
-
warnings: [],
|
|
1254
|
-
}
|
|
849
|
+
`EnginePart` uses a shared reference-aware geometry cache with a default capacity
|
|
850
|
+
of eight entries. Mounted parts retain their resources; released entries are
|
|
851
|
+
LRU-evicted/disposed when capacity is exceeded. The key uses GLB/STL URLs without query strings so renewed signatures reuse geometry. Proxy paths therefore
|
|
852
|
+
need distinct artifact identities; putting all identities only in query parameters
|
|
853
|
+
can collide. Keep the report's region mapping compatible with the cached artifact.
|
|
854
|
+
Use `engineGeometryCache.clear()` only after consumers have unmounted: it disposes
|
|
855
|
+
cached geometry, including retained resources.
|
|
1255
856
|
|
|
1256
|
-
|
|
1257
|
-
<Viewer style={{ height: 400 }}>
|
|
1258
|
-
<PartMesh model={model} geometry={geometry} onPick={(pick) => console.log(pick.ranked)} />
|
|
1259
|
-
</Viewer>
|
|
1260
|
-
)
|
|
1261
|
-
```
|
|
1262
|
-
|
|
1263
|
-
Requirements:
|
|
1264
|
-
|
|
1265
|
-
- **The mesh must be non-indexed** (call `.toNonIndexed()`). Each vertex can belong to only one
|
|
1266
|
-
face.
|
|
1267
|
-
- **Regions must cover every triangle, with no gaps or overlaps.** `buildRegionIndex` throws if
|
|
1268
|
-
they don't.
|
|
1269
|
-
- **Create the geometry once** (with `useMemo` or at module level). `PartMesh` doesn't dispose of
|
|
1270
|
-
it, so you're responsible for that.
|
|
1271
|
-
|
|
1272
|
-
If you have a report and want to load the mesh yourself, for example through your own backend,
|
|
1273
|
-
use the functions in `@toolpath/viewer/engine`:
|
|
1274
|
-
|
|
1275
|
-
```ts
|
|
1276
|
-
import { loadPartMesh, normalizePartReport, smoothRegionNormals } from '@toolpath/viewer/engine'
|
|
857
|
+
## Errors
|
|
1277
858
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
859
|
+
| Error | Export | Handling |
|
|
860
|
+
| ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- |
|
|
861
|
+
| `PartReportFormatError` | Root | Inspect `issues`; reject malformed reports or mismatched region/triangle mappings. |
|
|
862
|
+
| `UnsupportedKernelVersionError` | Root | `kernelVersion` is below `MIN_KERNEL_VERSION` (currently `0.3.0`); request compatible analysis data. |
|
|
863
|
+
| `PartMeshError` | `/engine` | No artifact, HTTP failure, unreadable mesh, or count/shape mismatch. |
|
|
864
|
+
| `AggregateError` | Built-in | All available GLB/STL attempts failed; inspect `errors` for individual causes. |
|
|
865
|
+
| `RangeError` | Built-in | Invalid stock dimensions, frames, axes, or coordinates. |
|
|
1282
866
|
|
|
1283
|
-
|
|
1284
|
-
|
|
867
|
+
Wrap `EnginePart` in Suspense and an error boundary. A custom async loader must
|
|
868
|
+
catch a rejected promise or rethrow it during render, as `ManagedPart` does; React
|
|
869
|
+
boundaries do not catch arbitrary async callback errors. Recover by correcting
|
|
870
|
+
input or fetching a fresh report/URL as appropriate, then reset the boundary.
|
|
871
|
+
Do not disable validation to render an unrelated mesh against a report's regions.
|
|
1285
872
|
|
|
1286
873
|
## Reference
|
|
1287
874
|
|
|
1288
875
|
### Entry points
|
|
1289
876
|
|
|
1290
|
-
| Import
|
|
1291
|
-
|
|
|
1292
|
-
| `@toolpath/viewer`
|
|
1293
|
-
| `@toolpath/viewer/engine`
|
|
877
|
+
| Import | Contents |
|
|
878
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
879
|
+
| `@toolpath/viewer` | Components, hooks, models, geometry/camera/selection helpers, and types. Also re-exports `EnginePart`, `normalizePartReport`, and `smoothRegionNormals`. |
|
|
880
|
+
| `@toolpath/viewer/engine` | Engine loading/normalization, geometry cache, `PartMeshError`, and adapter types. |
|
|
881
|
+
| `@toolpath/viewer/toolbar.css` | Optional toolbar stylesheet. |
|
|
882
|
+
|
|
883
|
+
Other source/dist subpaths are not public imports. The complete signatures for an
|
|
884
|
+
installed version are in its `dist/index.d.ts`, `dist/engine/index.d.ts`, and
|
|
885
|
+
referenced declaration chunks. The public source export lists are
|
|
886
|
+
[root](https://github.com/toolpath/ui-packages/blob/main/packages/viewer/src/index.ts)
|
|
887
|
+
and [Engine adapter](https://github.com/toolpath/ui-packages/blob/main/packages/viewer/src/engine/index.ts).
|
|
1294
888
|
|
|
1295
889
|
### Hooks
|
|
1296
890
|
|
|
1297
|
-
| Hook
|
|
1298
|
-
|
|
|
1299
|
-
| `useViewerControls
|
|
1300
|
-
| `useViewerToolbar
|
|
1301
|
-
| `useSectionStore
|
|
1302
|
-
| `useContentBox
|
|
1303
|
-
| `usePartContext
|
|
1304
|
-
| `
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
891
|
+
| Hook | Required context | Use |
|
|
892
|
+
| ------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
893
|
+
| `useViewerControls` | Inside `Viewer` | Camera controls and `setSection`. |
|
|
894
|
+
| `useViewerToolbar` | Inside `ViewerToolbarProvider` | Configured host actions/state. |
|
|
895
|
+
| `useSectionStore` | Inside `Viewer` | Read, set, or subscribe to viewer-owned section state. |
|
|
896
|
+
| `useContentBox` | Inside the scene | Finished-part bounds, excluding stock/furniture; empty until found and measured once per hook mount. |
|
|
897
|
+
| `usePartContext` | Child of `PartMesh`/`EnginePart` | Part model and prepared input geometry. |
|
|
898
|
+
| `useRetarget` | Inside viewer scene | Retarget callback, or `null` when disabled/outside context. |
|
|
899
|
+
| `useTapGuard` | Inside viewer scene | Distinguish taps from drags. |
|
|
900
|
+
|
|
901
|
+
### Helpers and types
|
|
902
|
+
|
|
903
|
+
Common model/selection helpers include `buildRegionIndex`, `focusForPick`,
|
|
904
|
+
`rankOwners`, `bestOwner`, `groupByDirection`, `directionLabel`, `directionColor`,
|
|
905
|
+
`directionHighlights`, `regionAdjacency`, and `featureTriangles`. The region index
|
|
906
|
+
provides `featuresForRegion(idx)` and `regionsForFeature(tag)`.
|
|
907
|
+
|
|
908
|
+
For custom tools, section helpers include `sectionFromPick`, `sectionMeasurement`,
|
|
909
|
+
`resolveSectionPlane`, and `createSectionStore`; measurement helpers include
|
|
910
|
+
`distanceBetween`, `deltaBetween`, `angleAt`, `measurementLabel`, and `snapAt`.
|
|
911
|
+
Lower-level camera, view-cube, painting, and geometry functions are also exported;
|
|
912
|
+
consult their declarations rather than importing implementation files.
|
|
913
|
+
|
|
914
|
+
Start with `ViewerProps`, `ViewerHandle`, `PartMeshProps`, `EnginePartProps`,
|
|
915
|
+
`PartModel`, `PartModelRegion`, `PartModelFeature`, `FeatureTag`, `PartPick`,
|
|
916
|
+
`FeatureHighlight`, `RegionHighlight`, `FocusOptions`, `ViewerTheme`, `StockProps`,
|
|
917
|
+
`SectionOptions`, `SectionState`, `Measurement`, `MeasureMode`, and `ToolMark`.
|
|
918
|
+
`EnginePartProps` and the loader/cache types come from `/engine`; scene/model
|
|
919
|
+
types come from the root. Use the types belonging to your installed version; a newer source checkout does
|
|
920
|
+
not add exports to a published package.
|
|
921
|
+
|
|
922
|
+
## Working on this package
|
|
923
|
+
|
|
924
|
+
The [React viewer example](https://github.com/toolpath/ui-packages/tree/main/examples/react-viewer)
|
|
925
|
+
loads procedural parts without credentials. It also opens local report/mesh files;
|
|
926
|
+
its [README](https://github.com/toolpath/ui-packages/blob/main/examples/react-viewer/README.md)
|
|
927
|
+
describes the controls and browser tests.
|
|
928
|
+
|
|
929
|
+
From the repository root with its pinned toolchain:
|
|
930
|
+
|
|
931
|
+
```sh
|
|
932
|
+
pnpm --filter @toolpath/viewer build
|
|
933
|
+
pnpm --filter @toolpath/viewer check-types
|
|
934
|
+
pnpm --filter @toolpath/viewer test
|
|
935
|
+
pnpm --filter @toolpath/example-react-viewer... build
|
|
936
|
+
pnpm --filter @toolpath/example-react-viewer test
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
The example imports built packages. `build:watch` on `@toolpath/viewer` refreshes
|
|
940
|
+
that output during development. Browser tests need Chromium installed. When
|
|
941
|
+
changing exports/assets, pack the viewer and test that artifact in a clean consumer;
|
|
942
|
+
check both JS entries, declarations, toolbar CSS, and the bundled banana asset.
|
|
943
|
+
Fixture URLs are redacted test data, not working download links. Keep retained
|
|
944
|
+
fixtures unchanged and use a fixture copy when adapting URLs for a local test.
|
|
945
|
+
|
|
946
|
+
See the [contributor guide](https://github.com/toolpath/ui-packages/blob/main/AGENTS.md)
|
|
947
|
+
and [changelog](https://github.com/toolpath/ui-packages/blob/main/packages/viewer/CHANGELOG.md)
|
|
948
|
+
for repo workflows and published changes.
|
|
949
|
+
|
|
950
|
+
## License
|
|
951
|
+
|
|
952
|
+
[MIT](https://github.com/toolpath/ui-packages/blob/main/packages/viewer/LICENSE).
|