@toolpath/viewer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toolpath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # `@toolpath/viewer`
2
+
3
+ React Three Fiber components for exploring Toolpath Engine part meshes. The package is client-side:
4
+ it can be imported by an SSR application, but render `<Viewer>` from a client component.
5
+
6
+ ```bash
7
+ npm install @toolpath/viewer react react-dom three @react-three/fiber @react-three/drei
8
+ ```
9
+
10
+ ## Toolpath Engine reports
11
+
12
+ ```tsx
13
+ import { Axes, DirectionArrows, Grid, ViewCube, Viewer } from '@toolpath/viewer'
14
+ import { EnginePart } from '@toolpath/viewer/engine'
15
+ ;<Suspense fallback={<p>Loading mesh…</p>}>
16
+ <Viewer style={{ height: 500 }}>
17
+ <EnginePart report={report} selection={selection} onPick={(pick) => setPick(pick)} />
18
+ <Grid />
19
+ <Axes />
20
+ <ViewCube />
21
+ </Viewer>
22
+ </Suspense>
23
+ ```
24
+
25
+ `EnginePart` takes a `PartReportResponse` exactly as `@toolpath/api` returns it and validates it:
26
+ a malformed report throws `PartReportFormatError` carrying every problem it found, and one from a
27
+ kernel older than `0.3.0` — before `regions[]` and `featureTag` existed — throws
28
+ `UnsupportedKernelVersionError`. It fetches `meshGlbUrl`, falls back to `meshStlUrl`, and refuses a
29
+ mesh whose triangle count does not match the report, because region ranges index that buffer
30
+ directly and a mismatch would quietly highlight the wrong surface.
31
+
32
+ Wrap the tree in an error boundary: an expired mesh URL, a malformed report, and an old kernel all
33
+ arrive as thrown errors rather than as empty states. The viewer does not parse STEP — the Engine
34
+ analyzes STEP and emits tessellated GLB/STL for display.
35
+
36
+ ## A click means several things at once
37
+
38
+ A region on the mesh is owned by **five to eight features at once** — measured on a cube, not
39
+ estimated. The same physical face is a `face` cut from one direction and a `wall` from others, and
40
+ every direction's `profile` overlaps the surfaces it traces. Nothing can reduce that to one answer,
41
+ so `onPick` hands over the whole set:
42
+
43
+ ```tsx
44
+ <Viewer onPointerMissed={clearSelection}>
45
+ <EnginePart
46
+ report={report}
47
+ onPick={(pick) => {
48
+ setCandidates(pick.ranked) // every reading, best first
49
+ setFocused(focusForPick(pick, lastRegion, focused)) // clicking again walks them
50
+ setLastRegion(pick.region)
51
+ }}
52
+ />
53
+ </Viewer>
54
+ ```
55
+
56
+ Putting the selection down belongs to `<Viewer onPointerMissed>` rather than to
57
+ `onPick`: a mesh's own missed event fires whenever _that mesh_ was not hit,
58
+ including when the click landed on a direction arrow or a section handle, so
59
+ reporting it from the part made pressing an arrow clear the selection.
60
+
61
+ `ranked` orders the owners by type specificity — a hole beats the wall it is bored through, and a
62
+ `profile` never wins the surface it traces — then by which reading faces the camera. `best` is the
63
+ first of them. Pass `activeDirection` to scope a pick to one machining direction, which **filters**
64
+ rather than reorders: a face that direction cannot reach picks to nothing, which is a real answer
65
+ rather than a missed click.
66
+
67
+ ## Colouring the part
68
+
69
+ A face can only be one colour, so the layers are painted weakest first and each overwrites what is
70
+ under it:
71
+
72
+ | Layer | Weight | What it says |
73
+ | ------------------- | ------ | --------------------------------------------- |
74
+ | `highlights` | 0.7 | your own meaning — a difficulty band, a setup |
75
+ | `regionHighlights` | 0.7 | the same, on named faces rather than features |
76
+ | `candidates` | 0.4 | what a click could have meant, per direction |
77
+ | `selection` | 1.0 | the features being read |
78
+ | `hoveredFeatureIds` | 0.85 | a list row under the pointer |
79
+ | the hovered face | 0.85 | what the pointer is on, tracked for you |
80
+
81
+ Highlighting is a texture write rather than a material change, so lighting every feature of a part
82
+ costs one mesh, one material, and one draw call.
83
+
84
+ ## Sectioning
85
+
86
+ ```tsx
87
+ <EnginePart
88
+ report={report}
89
+ section={{ enabled: true, normal: { x: 0, y: 0, z: 1 }, offset }}
90
+ onSectionChange={(state) => setOffset(state.offset)}
91
+ />
92
+ ```
93
+
94
+ `normal` points into the half that stays and `offset` runs 0 (whole) to 1 (gone). To cut from a
95
+ surface instead, turn a pick into a placement with `sectionFromPick(pick)` and pass it as `plane`,
96
+ moving it with `depth` in model units. The cut is capped rather than left hollow, and its arrow can
97
+ be dragged; `onSectionChange` reports every move, including the drag's.
98
+
99
+ ## Camera and controls
100
+
101
+ Use a ref or `useViewerControls()` beneath a viewer to call `fit`, `reset`, `setView`, and
102
+ `setViewDirection`. The part data is Z-up millimetres and the camera says so.
103
+
104
+ - `controls="toolpath"` (default) — left-drag orbits, right-drag pans.
105
+ - `controls="fusion"` — middle-drag and two-finger scroll pan, shift orbits, pinch zooms.
106
+ - `projection="orthographic"` — parallel edges stay parallel, so two features the same size
107
+ measure the same size wherever they sit.
108
+ - `freeOrbit` (default on) — orbiting past a pole keeps going instead of sticking there.
109
+
110
+ `<ViewCube>` offers all 26 standard views: six faces, twelve edge chamfers, and eight corners, so
111
+ an isometric is a click rather than a drag. `<Grid>` sizes itself from the part — 5 mm cells under a
112
+ 50 mm cube, 50 mm under a 900 mm plate — and sits on the part's underside. `<DirectionArrows>`
113
+ draws the directions a part can be held in, each in its own colour, aimed inward because a
114
+ machining direction is the direction the tool comes from.
115
+
116
+ ## Driving it without the Engine
117
+
118
+ `PartMesh` takes a `PartModel` and a `BufferGeometry` directly, so the viewer can be driven from a
119
+ file, a fixture, or geometry you built yourself — see `examples/react-viewer`. The mesh must be
120
+ non-indexed: highlighting is a per-vertex region attribute, and a vertex shared between two regions
121
+ has no single value to carry.