@toolpath/viewer 0.3.0 → 0.4.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.
@@ -1,5 +1,5 @@
1
- import { P as PartModel, a as PartMeshRefs } from '../normalize-W9Lv_Mpg.js';
2
- export { E as EnginePart, b as EnginePartProps, M as MIN_KERNEL_VERSION, c as assertSupportedKernelVersion, n as normalizePartReport, s as smoothRegionNormals } from '../normalize-W9Lv_Mpg.js';
1
+ import { P as PartModel, a as PartMeshRefs } from '../normalize-BXIGoZmc.js';
2
+ export { E as EnginePart, b as EnginePartProps, M as MIN_KERNEL_VERSION, c as assertSupportedKernelVersion, n as normalizePartReport, s as smoothRegionNormals } from '../normalize-BXIGoZmc.js';
3
3
  import { BufferGeometry } from 'three';
4
4
  import 'react';
5
5
 
@@ -12,7 +12,7 @@ import {
12
12
  parsePartGeometry,
13
13
  partMeshAssets,
14
14
  smoothRegionNormals
15
- } from "../chunk-RICG6UVH.js";
15
+ } from "../chunk-ILZ2KQUK.js";
16
16
  export {
17
17
  EnginePart,
18
18
  MIN_KERNEL_VERSION,
package/dist/index.d.ts CHANGED
@@ -1,10 +1,18 @@
1
- import { V as Vec3, d as ViewerTheme, P as PartModel, e as PartModelRegion, f as ViewerCamera, g as ViewerView, h as Projection, T as TriangleRange, F as FeatureTag, R as RegionIndex, i as PartModelFeature, j as FeatureType } from './normalize-W9Lv_Mpg.js';
2
- export { B as BuildPickInput, C as CAD_CAMERA_UP, k as CANDIDATE_WEIGHT, D as DEFAULT_FIT_MARGIN, l as DEFAULT_THEME, m as DIRECTION_COLORS, o as EXCLUDE_FROM_FRAME, E as EnginePart, p as FeatureHighlight, H as HANDLE_PIXELS, q as HIGHLIGHT_COLORS, r as HIGHLIGHT_WEIGHT, t as HOVER_WEIGHT, u as HighlightLayers, K as KnownFeatureType, N as NO_MODIFIERS, v as PERSPECTIVE_FOV, w as PICKED_SURFACE_LABEL, x as PartMesh, y as PartMeshProps, a as PartMeshRefs, z as PartObject, A as PartPick, G as PickModifiers, I as REGION_ATTRIBUTE, J as RegionHighlight, L as RegionPaint, S as SECTION_RENDER_ORDER, O as SceneBounds, Q as SectionAnchor, U as SectionBounds, W as SectionOptions, X as SectionPlacement, Y as SectionState, Z as SectionView, _ as ViewportSize, $ as applyHighlightLayers, a0 as applyProjection, a1 as aspectRatio, a2 as boundsFromBox, a3 as buildPick, a4 as buildRegionAttribute, a5 as buildRegionTexels, a6 as cadViewDirections, a7 as contentBounds, a8 as createPart, a9 as currentViewDirection, aa as defaultBounds, ab as directionColor, ac as dragPlane, ad as fitDistance, ae as focusForPick, n as normalizePartReport, af as orthographicHalfHeight, ag as perspectiveFitDistance, ah as pickedStartDepth, ai as resolveSectionPlane, aj as resolveTheme, ak as screenLength, al as sectionBounds, am as sectionConstant, an as sectionDepth, ao as sectionDepthConstant, ap as sectionDepthRange, aq as sectionFromPick, ar as sectionOffset, as as sectionPlane, s as smoothRegionNormals, at as startPosition, au as themesEqual, av as viewDirection } from './normalize-W9Lv_Mpg.js';
1
+ import { P as PartModel, V as Vec3, d as ViewerTheme, e as PartModelRegion, f as ViewerCamera, g as ViewerView, h as Projection, T as TriangleRange, F as FeatureTag, R as RegionIndex, i as PartModelFeature, j as FeatureType } from './normalize-BXIGoZmc.js';
2
+ export { B as BuildPickInput, C as CAD_CAMERA_UP, k as CANDIDATE_WEIGHT, D as DEFAULT_FIT_MARGIN, l as DEFAULT_THEME, m as DIRECTION_COLORS, o as EXCLUDE_FROM_FRAME, E as EnginePart, p as FeatureHighlight, H as HANDLE_PIXELS, q as HIGHLIGHT_COLORS, r as HIGHLIGHT_WEIGHT, t as HOVER_WEIGHT, u as HighlightLayers, K as KnownFeatureType, N as NO_MODIFIERS, v as PERSPECTIVE_FOV, w as PICKED_SURFACE_LABEL, x as PartMesh, y as PartMeshProps, a as PartMeshRefs, z as PartObject, A as PartPick, G as PickModifiers, I as REGION_ATTRIBUTE, J as RegionHighlight, L as RegionPaint, S as SECTION_RENDER_ORDER, O as SceneBounds, Q as SectionAnchor, U as SectionBounds, W as SectionOptions, X as SectionPlacement, Y as SectionState, Z as SectionView, _ as ViewportSize, $ as applyHighlightLayers, a0 as applyProjection, a1 as aspectRatio, a2 as boundsFromBox, a3 as buildPick, a4 as buildRegionAttribute, a5 as buildRegionTexels, a6 as cadViewDirections, a7 as contentBounds, a8 as createPart, a9 as currentViewDirection, aa as defaultBounds, ab as directionColor, ac as dragPlane, ad as fitDistance, ae as focusForPick, n as normalizePartReport, af as orthographicHalfHeight, ag as perspectiveFitDistance, ah as pickedStartDepth, ai as resolveSectionPlane, aj as resolveTheme, ak as screenLength, al as sectionBounds, am as sectionConstant, an as sectionDepth, ao as sectionDepthConstant, ap as sectionDepthRange, aq as sectionFromPick, ar as sectionOffset, as as sectionPlane, s as smoothRegionNormals, at as startPosition, au as themesEqual, av as viewDirection } from './normalize-BXIGoZmc.js';
3
+ import { BufferGeometry, CanvasTexture, Vector3, Box3 } from 'three';
3
4
  import * as react from 'react';
4
5
  import { RefObject, ReactNode, CSSProperties } from 'react';
5
- import { BufferGeometry, CanvasTexture, Vector3, Box3 } from 'three';
6
6
  import CameraControls from 'camera-controls';
7
7
 
8
+ /**
9
+ * Every face's neighbours, keyed by region index.
10
+ *
11
+ * One pass over the triangles: each contributes three edges, each edge names
12
+ * the region that owns it, and any edge named by two regions joins them.
13
+ */
14
+ declare function regionAdjacency(model: PartModel, geometry: BufferGeometry): Map<number, Set<number>>;
15
+
8
16
  /**
9
17
  * The 26 standard views: the six faces of the cube, the twelve edges between
10
18
  * them, and its eight corners.
@@ -39,6 +47,21 @@ declare function viewVector(name: ViewName): Vec3;
39
47
  * come from here.
40
48
  */
41
49
  declare function viewUp(direction: Vec3): Vec3;
50
+ /**
51
+ * The squared up vector nearest to the one the camera already has.
52
+ *
53
+ * A view has four square orientations, not one: {@link viewUp} rolled by 0°,
54
+ * 90°, 180° and 270° about the view direction. Snapping to the canonical one
55
+ * would spin the part on the way to a view somebody asked for because it was
56
+ * already nearly in front of them — arrive at the bottom view from the right
57
+ * and the part turns a quarter of a turn for no reason they can see.
58
+ *
59
+ * So the roll is chosen, not imposed: of the four, the one closest to where the
60
+ * camera is looking from now. The view still lands square — Bottom simply lands
61
+ * square in whichever of its four ways was already nearest, which is how the
62
+ * Fusion cube behaves and what somebody arriving from it will expect.
63
+ */
64
+ declare function squaredUp(direction: Vec3, currentUp: Vec3): Vec3;
42
65
  /**
43
66
  * Half-width of a face panel, where the cube's half-extent is 1 — so the
44
67
  * chamfer taken off each edge is the remaining `1 - CHAMFER`.
@@ -134,10 +157,15 @@ interface DirectionArrowsProps {
134
157
  */
135
158
  activeDirection?: number | null;
136
159
  /**
137
- * Shows one direction's arrow without scoping anything, for "the feature
138
- * being read came from this way". Falls back to `activeDirection`.
160
+ * Which arrows are drawn, without scoping anything: `null` for all of them,
161
+ * an index for one, `-1` for none and **a list of indices for a set**, which
162
+ * is how a plan draws only the ways up it has confirmed.
163
+ *
164
+ * One prop rather than a singular and a plural, because two would need a rule
165
+ * about which wins, and a caller with an answer to give should not also have
166
+ * to say where to put it. Falls back to `activeDirection`.
139
167
  */
140
- shownDirection?: number | null;
168
+ shownDirection?: number | readonly number[] | null;
141
169
  /**
142
170
  * A direction being named, drawn while it is aimed. Not a candidate and not a
143
171
  * selection: a way up that does not exist yet, so it is drawn over the part —
@@ -227,6 +255,37 @@ interface TapTracker {
227
255
  * and a listener that runs after `stopPropagation` runs never.
228
256
  */
229
257
  declare function trackTaps(element: HTMLElement): TapTracker;
258
+ /**
259
+ * How long between two presses still reads as one gesture, in milliseconds.
260
+ *
261
+ * The browser's own `dblclick` threshold is a platform setting and is not
262
+ * readable from script, so a hand-paired gesture has to pick a number. 400ms is
263
+ * inside every default and outside a deliberate pair of separate clicks.
264
+ */
265
+ declare const DOUBLE_TAP_MS = 400;
266
+ interface DoubleTapPoint extends TapPoint {
267
+ readonly timeStamp: number;
268
+ }
269
+ interface DoubleTapTracker {
270
+ /** Whether this press completes a double tap begun by the last one. */
271
+ isDouble(event: DoubleTapPoint): boolean;
272
+ }
273
+ /**
274
+ * Pairing presses into a double tap by hand.
275
+ *
276
+ * Needed because `dblclick` fires for the **primary button only** — there is no
277
+ * such event for the middle button, however many times it is pressed. So a
278
+ * middle-button gesture has to be assembled from single presses.
279
+ *
280
+ * Position matters as well as time: two presses at opposite corners inside the
281
+ * window are two clicks that happened to be quick, not one gesture, and
282
+ * treating them as a double would re-frame the view out from under somebody
283
+ * who was doing something else. The same {@link TAP_SLOP} the click guard uses.
284
+ *
285
+ * A completed double clears the state rather than leaving it, so three presses
286
+ * are one double and one single rather than two overlapping doubles.
287
+ */
288
+ declare function trackDoubleTaps(within?: number, slop?: number): DoubleTapTracker;
230
289
 
231
290
  /**
232
291
  * A click that is a click, for anything inside the canvas.
@@ -368,6 +427,15 @@ interface CadCameraControlsProps {
368
427
  * about an orbit control.
369
428
  */
370
429
  freeOrbit?: boolean;
430
+ /**
431
+ * What the wheel zooms toward: the pointer, or the middle of the view.
432
+ *
433
+ * Fusion and SolidWorks zoom to the cursor and most people expect it — you
434
+ * point at the corner you want and lean in. It is not universally liked: on a
435
+ * trackpad it can walk the model off screen, which is why the other one
436
+ * stays.
437
+ */
438
+ zoomTo?: 'cursor' | 'centre';
371
439
  }
372
440
  /**
373
441
  * Mounts `camera-controls` against the R3F camera and canvas.
@@ -378,7 +446,7 @@ interface CadCameraControlsProps {
378
446
  * also registered as R3F's default `controls`, which is how anything else in
379
447
  * the scene reaches the orbit target.
380
448
  */
381
- declare const CadCameraControls: ({ controlsRef, scheme, freeOrbit, }: CadCameraControlsProps) => null;
449
+ declare const CadCameraControls: ({ controlsRef, scheme, freeOrbit, zoomTo, }: CadCameraControlsProps) => null;
382
450
 
383
451
  interface ViewerControls {
384
452
  /** Frames the part without changing the current viewing direction. */
@@ -430,6 +498,20 @@ interface ViewerProps {
430
498
  controls?: ControlScheme;
431
499
  /** Orbit past the poles instead of stopping there. On by default. */
432
500
  freeOrbit?: boolean;
501
+ /**
502
+ * What the wheel zooms toward: the pointer, or the middle of the view.
503
+ *
504
+ * `cursor` by default, which is what Fusion does and what most people reach
505
+ * for. It is a preference rather than a right answer — on a trackpad it can
506
+ * walk the model off screen.
507
+ */
508
+ zoomTo?: 'cursor' | 'centre';
509
+ /**
510
+ * Whether a double **middle** click re-frames the part. On by default — it is
511
+ * the way back from having zoomed into a corner, which zooming to the cursor
512
+ * makes easy to do.
513
+ */
514
+ recentreOnDoubleClick?: boolean;
433
515
  /** Lighting and background. The part's own colours are tuned against this rig. */
434
516
  theme?: Partial<ViewerTheme>;
435
517
  /**
@@ -602,4 +684,4 @@ declare function bestOwner(model: PartModel, owners: readonly FeatureTag[], cont
602
684
  */
603
685
  declare function cycleOwner(owners: readonly FeatureTag[], current: FeatureTag | null): FeatureTag | null;
604
686
 
605
- export { type ArrowPlacement, Axes, type AxesProps, CHAMFER, CadCameraControls, type CadCameraControlsProps, type ControlScheme, type CubeZone, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, type NamedDirection, PartModel, PartModelFeature, PartModelRegion, PartReportFormatError, Projection, type RankingContext, RegionIndex, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, ViewerView, arrowPlacement, bestOwner, buildRegionIndex, cubeOutlineGeometry, cubeZones, cycleOwner, directionIndexOf, directionLabel, featureTypeRank, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, movedFar, panelGeometry, rankOwners, regionEdgesGeometry, regionNormals, sameDirection, trackTaps, useContentBox, useTapGuard, useViewerControls, viewKind, viewUp, viewVector, visualSurfaces };
687
+ export { type ArrowPlacement, Axes, type AxesProps, CHAMFER, CadCameraControls, type CadCameraControlsProps, type ControlScheme, type CubeZone, DOUBLE_TAP_MS, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DoubleTapPoint, type DoubleTapTracker, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, type NamedDirection, PartModel, PartModelFeature, PartModelRegion, PartReportFormatError, Projection, type RankingContext, RegionIndex, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, ViewerView, arrowPlacement, bestOwner, buildRegionIndex, cubeOutlineGeometry, cubeZones, cycleOwner, directionIndexOf, directionLabel, featureTypeRank, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, movedFar, panelGeometry, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, sameDirection, squaredUp, trackDoubleTaps, trackTaps, useContentBox, useTapGuard, useViewerControls, viewKind, viewUp, viewVector, visualSurfaces };
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  CAD_CAMERA_UP,
3
3
  CANDIDATE_WEIGHT,
4
+ CHAMFER,
4
5
  CONE_AXIS,
5
6
  CadCameraControls,
6
7
  DEFAULT_FIT_MARGIN,
7
8
  DEFAULT_THEME,
8
9
  DIRECTION_COLORS,
10
+ DOUBLE_TAP_MS,
9
11
  EXCLUDE_FROM_FRAME,
10
12
  EnginePart,
11
13
  ExtendedCameraControls,
@@ -27,6 +29,8 @@ import {
27
29
  SectionView,
28
30
  TAP_SLOP,
29
31
  UnsupportedKernelVersionError,
32
+ VIEW_NAMES,
33
+ VIEW_SIGNS,
30
34
  Viewer,
31
35
  applyHighlightLayers,
32
36
  applyProjection,
@@ -41,6 +45,8 @@ import {
41
45
  cadViewDirections,
42
46
  contentBounds,
43
47
  createPart,
48
+ cubeOutlineGeometry,
49
+ cubeZones,
44
50
  currentViewDirection,
45
51
  cycleOwner,
46
52
  defaultBounds,
@@ -52,12 +58,16 @@ import {
52
58
  fitDistance,
53
59
  focusForPick,
54
60
  groupByDirection,
61
+ labelGeometry,
62
+ labelTexture,
55
63
  movedFar,
56
64
  normalizePartReport,
57
65
  orthographicHalfHeight,
66
+ panelGeometry,
58
67
  perspectiveFitDistance,
59
68
  pickedStartDepth,
60
69
  rankOwners,
70
+ regionAdjacency,
61
71
  regionEdgesGeometry,
62
72
  resolveSectionPlane,
63
73
  resolveTheme,
@@ -72,15 +82,20 @@ import {
72
82
  sectionOffset,
73
83
  sectionPlane,
74
84
  smoothRegionNormals,
85
+ squaredUp,
75
86
  startPosition,
76
87
  themesEqual,
88
+ trackDoubleTaps,
77
89
  trackTaps,
78
90
  useContentBox,
79
91
  useTapGuard,
80
92
  useViewerControls,
81
93
  viewDirection,
94
+ viewKind,
95
+ viewUp,
96
+ viewVector,
82
97
  visualSurfaces
83
- } from "./chunk-RICG6UVH.js";
98
+ } from "./chunk-ILZ2KQUK.js";
84
99
 
85
100
  // src/primitives.tsx
86
101
  import { GizmoHelper } from "@react-three/drei";
@@ -125,233 +140,6 @@ function gridGeometry(spec) {
125
140
  return geometry;
126
141
  }
127
142
 
128
- // src/render/view-cube.ts
129
- import {
130
- BufferGeometry as BufferGeometry2,
131
- CanvasTexture,
132
- Float32BufferAttribute as Float32BufferAttribute2,
133
- SRGBColorSpace,
134
- Vector3 as Vector32
135
- } from "three";
136
- var VIEW_NAMES = [
137
- // The six faces.
138
- "top",
139
- "bottom",
140
- "front",
141
- "back",
142
- "left",
143
- "right",
144
- // The twelve edges.
145
- "top-front",
146
- "top-back",
147
- "top-left",
148
- "top-right",
149
- "bottom-front",
150
- "bottom-back",
151
- "bottom-left",
152
- "bottom-right",
153
- "front-left",
154
- "front-right",
155
- "back-left",
156
- "back-right",
157
- // The eight corners.
158
- "top-front-left",
159
- "top-front-right",
160
- "top-back-left",
161
- "top-back-right",
162
- "bottom-front-left",
163
- "bottom-front-right",
164
- "bottom-back-left",
165
- "bottom-back-right"
166
- ];
167
- var VIEW_SIGNS = {
168
- top: [0, 0, 1],
169
- bottom: [0, 0, -1],
170
- front: [0, -1, 0],
171
- back: [0, 1, 0],
172
- left: [-1, 0, 0],
173
- right: [1, 0, 0],
174
- "top-front": [0, -1, 1],
175
- "top-back": [0, 1, 1],
176
- "top-left": [-1, 0, 1],
177
- "top-right": [1, 0, 1],
178
- "bottom-front": [0, -1, -1],
179
- "bottom-back": [0, 1, -1],
180
- "bottom-left": [-1, 0, -1],
181
- "bottom-right": [1, 0, -1],
182
- "front-left": [-1, -1, 0],
183
- "front-right": [1, -1, 0],
184
- "back-left": [-1, 1, 0],
185
- "back-right": [1, 1, 0],
186
- "top-front-left": [-1, -1, 1],
187
- "top-front-right": [1, -1, 1],
188
- "top-back-left": [-1, 1, 1],
189
- "top-back-right": [1, 1, 1],
190
- "bottom-front-left": [-1, -1, -1],
191
- "bottom-front-right": [1, -1, -1],
192
- "bottom-back-left": [-1, 1, -1],
193
- "bottom-back-right": [1, 1, -1]
194
- };
195
- function viewKind(name) {
196
- const off = VIEW_SIGNS[name].filter((sign) => sign !== 0).length;
197
- if (off === 1) return "face";
198
- if (off === 2) return "edge";
199
- return "corner";
200
- }
201
- var DIRECTIONS = new Map(
202
- VIEW_NAMES.map((name) => {
203
- const [x, y, z] = VIEW_SIGNS[name];
204
- const length = Math.hypot(x, y, z);
205
- return [name, { x: x / length, y: y / length, z: z / length }];
206
- })
207
- );
208
- function viewVector(name) {
209
- return DIRECTIONS.get(name) ?? { x: 0, y: 0, z: 1 };
210
- }
211
- function viewUp(direction) {
212
- if (direction.x === 0 && direction.y === 0) {
213
- return { x: 0, y: direction.z > 0 ? 1 : -1, z: 0 };
214
- }
215
- return { x: 0, y: 0, z: 1 };
216
- }
217
- var CHAMFER = 0.5834;
218
- function vec(v) {
219
- return { x: v.x, y: v.y, z: v.z };
220
- }
221
- function cubeZones(chamfer = CHAMFER) {
222
- const AXES = [0, 1, 2];
223
- return VIEW_NAMES.map((name) => {
224
- const kind = viewKind(name);
225
- const direction = viewVector(name);
226
- const [sx, sy, sz] = VIEW_SIGNS[name];
227
- const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
228
- const normal = new Vector32(direction.x, direction.y, direction.z);
229
- let polygon;
230
- if (kind === "face") {
231
- const up = viewUp(direction);
232
- const upVector = new Vector32(up.x, up.y, up.z);
233
- const right = normal.clone().negate().cross(upVector).normalize();
234
- const square = [
235
- [-1, -1],
236
- [1, -1],
237
- [1, 1],
238
- [-1, 1]
239
- ];
240
- polygon = square.map(
241
- ([u, v]) => normal.clone().addScaledVector(right, u * chamfer).addScaledVector(upVector, v * chamfer)
242
- );
243
- } else if (kind === "edge") {
244
- const a = AXES.find((axis) => sign(axis) !== 0) ?? 0;
245
- const b = [...AXES].reverse().find((axis) => sign(axis) !== 0) ?? 0;
246
- const c = AXES.find((axis) => sign(axis) === 0) ?? 0;
247
- const rectangle = [
248
- [1, chamfer, -chamfer],
249
- [1, chamfer, chamfer],
250
- [chamfer, 1, chamfer],
251
- [chamfer, 1, -chamfer]
252
- ];
253
- polygon = rectangle.map(
254
- ([onA, onB, onC]) => new Vector32().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
255
- );
256
- } else {
257
- const triangle = [
258
- [1, chamfer, chamfer],
259
- [chamfer, 1, chamfer],
260
- [chamfer, chamfer, 1]
261
- ];
262
- polygon = triangle.map(([onX, onY, onZ]) => new Vector32(sx * onX, sy * onY, sz * onZ));
263
- }
264
- const [first, second, third] = polygon;
265
- if (first && second && third) {
266
- const facing = new Vector32().subVectors(second, first).cross(new Vector32().subVectors(third, first));
267
- if (facing.dot(normal) < 0) polygon.reverse();
268
- }
269
- return { name, kind, direction, polygon: polygon.map(vec) };
270
- });
271
- }
272
- function panelGeometry(zone) {
273
- const positions = [];
274
- const normals = [];
275
- const [first] = zone.polygon;
276
- const geometry = new BufferGeometry2();
277
- if (!first) return geometry;
278
- for (let index = 1; index + 1 < zone.polygon.length; index += 1) {
279
- const b = zone.polygon[index];
280
- const c = zone.polygon[index + 1];
281
- if (!b || !c) continue;
282
- positions.push(first.x, first.y, first.z, b.x, b.y, b.z, c.x, c.y, c.z);
283
- for (let vertex = 0; vertex < 3; vertex += 1) {
284
- normals.push(zone.direction.x, zone.direction.y, zone.direction.z);
285
- }
286
- }
287
- geometry.setAttribute("position", new Float32BufferAttribute2(positions, 3));
288
- geometry.setAttribute("normal", new Float32BufferAttribute2(normals, 3));
289
- return geometry;
290
- }
291
- function cubeOutlineGeometry(zones) {
292
- const positions = [];
293
- for (const zone of zones) {
294
- for (let index = 0; index < zone.polygon.length; index += 1) {
295
- const from = zone.polygon[index];
296
- const to = zone.polygon[(index + 1) % zone.polygon.length];
297
- if (!from || !to) continue;
298
- positions.push(from.x, from.y, from.z, to.x, to.y, to.z);
299
- }
300
- }
301
- const geometry = new BufferGeometry2();
302
- geometry.setAttribute("position", new Float32BufferAttribute2(positions, 3));
303
- return geometry;
304
- }
305
- function labelGeometry(zone) {
306
- const positions = [];
307
- const uvs = [];
308
- const corners = [0, 1, 2, 0, 2, 3];
309
- const corner = [
310
- [0, 0],
311
- [1, 0],
312
- [1, 1],
313
- [0, 1]
314
- ];
315
- for (const index of corners) {
316
- const point = zone.polygon[index];
317
- const uv = corner[index];
318
- if (!point || !uv) continue;
319
- positions.push(
320
- point.x + zone.direction.x * 4e-3,
321
- point.y + zone.direction.y * 4e-3,
322
- point.z + zone.direction.z * 4e-3
323
- );
324
- uvs.push(uv[0] ?? 0, uv[1] ?? 0);
325
- }
326
- const geometry = new BufferGeometry2();
327
- geometry.setAttribute("position", new Float32BufferAttribute2(positions, 3));
328
- geometry.setAttribute("uv", new Float32BufferAttribute2(uvs, 2));
329
- return geometry;
330
- }
331
- function labelTexture(label, color) {
332
- const size = 128;
333
- const canvas = document.createElement("canvas");
334
- canvas.width = size;
335
- canvas.height = size;
336
- const context = canvas.getContext("2d");
337
- if (context) {
338
- const text = label.toUpperCase();
339
- const hex = `#${color.toString(16).padStart(6, "0")}`;
340
- context.font = `600 ${size / 4}px system-ui, sans-serif`;
341
- const width = context.measureText("BOTTOM").width || 1;
342
- const scale = Math.min(1, size * 0.78 / width);
343
- context.font = `600 ${size / 4 * scale}px system-ui, sans-serif`;
344
- context.textAlign = "center";
345
- context.textBaseline = "middle";
346
- context.fillStyle = hex;
347
- context.fillText(text, size / 2, size / 2);
348
- }
349
- const texture = new CanvasTexture(canvas);
350
- texture.colorSpace = SRGBColorSpace;
351
- texture.needsUpdate = true;
352
- return texture;
353
- }
354
-
355
143
  // src/primitives.tsx
356
144
  import { jsx, jsxs } from "react/jsx-runtime";
357
145
  var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
@@ -460,7 +248,7 @@ var CubePanels = ({
460
248
  // src/direction-arrows.tsx
461
249
  import { useThree as useThree2 } from "@react-three/fiber";
462
250
  import { useMemo as useMemo2 } from "react";
463
- import { Quaternion, Vector3 as Vector33 } from "three";
251
+ import { Quaternion, Vector3 as Vector32 } from "three";
464
252
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
465
253
  var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
466
254
  var DirectionArrows = ({
@@ -476,6 +264,11 @@ var DirectionArrows = ({
476
264
  const box = useContentBox();
477
265
  const resolved = useMemo2(() => resolveTheme(theme), [theme]);
478
266
  const shown = shownDirection === void 0 ? activeDirection : shownDirection;
267
+ const drawn = (index) => {
268
+ if (shown === null) return true;
269
+ if (typeof shown === "number") return shown === index;
270
+ return shown.includes(index);
271
+ };
479
272
  if (!visible) return null;
480
273
  return (
481
274
  // Excluded from framing: the arrows are deliberately placed outside the
@@ -483,7 +276,7 @@ var DirectionArrows = ({
483
276
  // part small in the middle of them.
484
277
  /* @__PURE__ */ jsxs2("group", { userData: FURNITURE2, children: [
485
278
  directions.map((direction, index) => {
486
- if (shown !== null && shown !== index) return null;
279
+ if (!drawn(index)) return null;
487
280
  return /* @__PURE__ */ jsx2(
488
281
  Arrow,
489
282
  {
@@ -517,7 +310,7 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
517
310
  const placement = arrowPlacement(direction, box);
518
311
  const aim = new Quaternion().setFromUnitVectors(
519
312
  CONE_AXIS,
520
- new Vector33(direction.x, direction.y, direction.z).normalize()
313
+ new Vector32(direction.x, direction.y, direction.z).normalize()
521
314
  );
522
315
  return { tip: placement.tip, length: placement.length, quaternion: aim };
523
316
  }, [box, direction]);
@@ -584,6 +377,7 @@ export {
584
377
  DEFAULT_FIT_MARGIN,
585
378
  DEFAULT_THEME,
586
379
  DIRECTION_COLORS,
380
+ DOUBLE_TAP_MS,
587
381
  DirectionArrows,
588
382
  EXCLUDE_FROM_FRAME,
589
383
  EnginePart,
@@ -645,6 +439,7 @@ export {
645
439
  perspectiveFitDistance,
646
440
  pickedStartDepth,
647
441
  rankOwners,
442
+ regionAdjacency,
648
443
  regionEdgesGeometry,
649
444
  regionNormals,
650
445
  resolveSectionPlane,
@@ -660,8 +455,10 @@ export {
660
455
  sectionOffset,
661
456
  sectionPlane,
662
457
  smoothRegionNormals,
458
+ squaredUp,
663
459
  startPosition,
664
460
  themesEqual,
461
+ trackDoubleTaps,
665
462
  trackTaps,
666
463
  useContentBox,
667
464
  useTapGuard,
@@ -5,7 +5,7 @@ import { Group, Mesh, LineSegments, Box3, Plane, BufferGeometry, Camera, Vector3
5
5
  * The normalized part — the only thing the renderer consumes.
6
6
  *
7
7
  * `engine/` produces this; nothing under `model/` or `render/` sees a
8
- * `PartReportResponse`. The renderer therefore survives an API change, and the
8
+ * `PartResponse`. The renderer therefore survives an API change, and the
9
9
  * viewer can be driven from a local file with no API at all, which is also how
10
10
  * it gets tested.
11
11
  *
@@ -215,7 +215,7 @@ declare const HIGHLIGHT_COLORS: {
215
215
  * of the palette — it is an identity, not a ranking, which is why the colors
216
216
  * are unordered and deliberately not a scale.
217
217
  */
218
- declare const DIRECTION_COLORS: readonly [3900150, 1357990, 14239471, 440020, 6660877, 15485081, 6583435, 1096065, 6514417];
218
+ declare const DIRECTION_COLORS: readonly [3900150, 889992, 14239471, 6809849, 6660877, 15485081, 9741240, 4906624, 1981066];
219
219
  declare const DEFAULT_THEME: ViewerTheme;
220
220
  declare function resolveTheme(overrides?: Partial<ViewerTheme>): ViewerTheme;
221
221
  /**
@@ -303,11 +303,15 @@ declare function buildRegionAttribute(model: RegionTable, texels: Map<number, nu
303
303
  /**
304
304
  * Builds the renderable part.
305
305
  *
306
- * Takes exclusive use of `geometry`: it adds {@link REGION_ATTRIBUTE} to it and
306
+ * Takes working use of `geometry`: it adds {@link REGION_ATTRIBUTE} to it and
307
307
  * removes that again on dispose, but never disposes the geometry itself — the
308
308
  * loader's caller owns it, and a viewer destroying an object it was handed is a
309
- * good way to break a cache that is legitimately sharing it. One geometry backs
310
- * one part; sharing it between two parts at once is unsupported.
309
+ * good way to break a cache that is legitimately sharing it.
310
+ *
311
+ * Two parts on one geometry is still not a thing to *render*, but it is a state
312
+ * that happens: a report changing identity rebuilds the part against the same
313
+ * cached mesh, and for a moment both exist. So dispose removes the attribute
314
+ * only if it is still the one this part set — see the note where it is built.
311
315
  */
312
316
  declare function createPart(model: PartModel, geometry: BufferGeometry, theme: ViewerTheme): PartObject;
313
317
 
@@ -772,6 +776,15 @@ interface PartMeshProps {
772
776
  * real change, so echoing it into state is safe.
773
777
  */
774
778
  onSectionChange?: (state: SectionState) => void;
779
+ /**
780
+ * Which faces touch which, once the mesh is in.
781
+ *
782
+ * The consumer cannot work this out: it has a region table with areas and
783
+ * shape kinds, and the topology only exists in the geometry. Handed over
784
+ * rather than queried, because it is computed once per mesh and the consumer
785
+ * needs it to answer questions about faces it has not clicked yet.
786
+ */
787
+ onAdjacency?: (adjacency: ReadonlyMap<number, ReadonlySet<number>>) => void;
775
788
  /**
776
789
  * A feature to frame. Framed when it changes, so setting it to the feature
777
790
  * already framed does nothing — a zoom is a request, not a state to hold.
@@ -801,7 +814,7 @@ interface PartMeshProps {
801
814
  * React render at all: the hovered face lives in a ref, and routing it through
802
815
  * state is what this rewrite exists to stop doing.
803
816
  */
804
- declare const PartMesh: ({ model, geometry, selection, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, activeDirection, section, onSectionChange, focusFeature, onHover, onPick, theme, showEdges, }: PartMeshProps) => react.JSX.Element;
817
+ declare const PartMesh: ({ model, geometry, selection, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, activeDirection, section, onSectionChange, onAdjacency, focusFeature, onHover, onPick, theme, showEdges, }: PartMeshProps) => react.JSX.Element;
805
818
 
806
819
  interface EnginePartProps extends Omit<PartMeshProps, 'model' | 'geometry'> {
807
820
  /**
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@toolpath/viewer",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React Three Fiber viewer for Toolpath Engine part reports",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/toolpath/toolpath.git",
8
+ "url": "https://github.com/toolpath/ui-packages.git",
9
9
  "directory": "packages/viewer"
10
10
  },
11
11
  "homepage": "https://developers.toolpath.com",
12
12
  "bugs": {
13
- "url": "https://github.com/toolpath/toolpath/issues"
13
+ "url": "https://github.com/toolpath/ui-packages/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public",