@toolpath/viewer 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { P as PartModel, a as PartMeshRefs } from '../normalize-Bzzwwxt4.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-Bzzwwxt4.js';
1
+ import { P as PartModel, a as PartMeshRefs } from '../normalize-DsoIyM8B.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-DsoIyM8B.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-4BU4QZCL.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-Bzzwwxt4.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-Bzzwwxt4.js';
1
+ import { P as PartModel, V as Vec3, d as ViewerTheme, e as PartModelRegion, T as TriangleRange, F as FeatureTag, R as RegionIndex, f as PartModelFeature, g as FeatureType } from './normalize-DsoIyM8B.js';
2
+ export { B as BuildPickInput, C as CANDIDATE_WEIGHT, D as DEFAULT_THEME, h as DIRECTION_COLORS, E as EnginePart, i as FeatureHighlight, H as HANDLE_PIXELS, j as HIGHLIGHT_COLORS, k as HIGHLIGHT_WEIGHT, l as HOVER_WEIGHT, m as HighlightLayers, K as KnownFeatureType, N as NO_MODIFIERS, o as PICKED_SURFACE_LABEL, p as PartMesh, q as PartMeshProps, a as PartMeshRefs, r as PartObject, t as PartPick, u as PickModifiers, v as REGION_ATTRIBUTE, w as RegionHighlight, x as RegionPaint, S as SECTION_RENDER_ORDER, y as SectionAnchor, z as SectionBounds, A as SectionOptions, G as SectionPlacement, I as SectionState, J as SectionView, L as applyHighlightLayers, O as buildPick, Q as buildRegionAttribute, U as buildRegionTexels, W as createPart, X as directionColor, Y as dragPlane, Z as focusForPick, n as normalizePartReport, _ as pickedStartDepth, $ as resolveSectionPlane, a0 as resolveTheme, a1 as sectionBounds, a2 as sectionConstant, a3 as sectionDepth, a4 as sectionDepthConstant, a5 as sectionDepthRange, a6 as sectionFromPick, a7 as sectionOffset, a8 as sectionPlane, s as smoothRegionNormals, a9 as themesEqual, aa as viewDirection } from './normalize-DsoIyM8B.js';
3
+ import { BufferGeometry, CanvasTexture, Vector3, Box3, OrthographicCamera, PerspectiveCamera, Object3D } 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,14 +255,67 @@ 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
+ * Forgets the press being waited on, so the next one starts a fresh pair.
274
+ *
275
+ * For the caller that knows the gesture was interrupted by something this
276
+ * has no way to see — the pointer leaving the thing being clicked, most of
277
+ * all. Two presses either side of a trip to a panel are two clicks that
278
+ * happen to be quick, however close together the clock puts them.
279
+ */
280
+ reset(): void;
281
+ }
282
+ /**
283
+ * Pairing presses into a double tap by hand.
284
+ *
285
+ * Needed because `dblclick` fires for the **primary button only** — there is no
286
+ * such event for the middle button, however many times it is pressed. So a
287
+ * middle-button gesture has to be assembled from single presses.
288
+ *
289
+ * Position matters as well as time: two presses at opposite corners inside the
290
+ * window are two clicks that happened to be quick, not one gesture, and
291
+ * treating them as a double would re-frame the view out from under somebody
292
+ * who was doing something else. The same {@link TAP_SLOP} the click guard uses.
293
+ *
294
+ * A completed double clears the state rather than leaving it, so three presses
295
+ * are one double and one single rather than two overlapping doubles.
296
+ *
297
+ * Time and position are all this can judge on, which is not enough on its own:
298
+ * clicking a face, pressing something in a panel and clicking the same face
299
+ * again is three gestures the clock cannot tell from two, and it is quick
300
+ * enough to land inside the window. {@link DoubleTapTracker.reset} is how the
301
+ * caller says the pair was broken by something it could see and this could not.
302
+ */
303
+ declare function trackDoubleTaps(within?: number, slop?: number): DoubleTapTracker;
230
304
 
305
+ /** Whether the gesture ending at this event was a click rather than a drag. */
306
+ type TapGuard = (event: TapPoint) => boolean;
231
307
  /**
232
308
  * A click that is a click, for anything inside the canvas.
233
309
  *
234
310
  * Bound to the canvas element rather than to the object, because the gesture
235
311
  * being judged started before any object knew about it.
312
+ *
313
+ * Under a `<Viewer>` this returns the tracker the scene already owns and
314
+ * attaches nothing. Called outside one — it is exported, so a consumer may use
315
+ * it in a scene of their own — it owns a tracker of its own instead. The effect
316
+ * is what differs, not the hooks: they run in the same order either way.
236
317
  */
237
- declare function useTapGuard(): (event: TapPoint) => boolean;
318
+ declare function useTapGuard(): TapGuard;
238
319
 
239
320
  interface GridSpec {
240
321
  /** Cell size in part units (millimetres). */
@@ -305,6 +386,200 @@ type SurfaceOf = ReadonlyMap<number, number>;
305
386
  */
306
387
  declare function visualSurfaces(regions: readonly PartModelRegion[]): SurfaceOf;
307
388
 
389
+ type Projection = 'orthographic' | 'perspective';
390
+ type ViewerCamera = OrthographicCamera | PerspectiveCamera;
391
+ interface ViewportSize {
392
+ readonly width: number;
393
+ readonly height: number;
394
+ }
395
+ /** Vertical field of view, in degrees, for the perspective camera. */
396
+ declare const PERSPECTIVE_FOV = 30;
397
+ /** Padding around the framed bounds, as a multiple of its radius. */
398
+ declare const DEFAULT_FIT_MARGIN = 1.2;
399
+ /** Marks scene furniture — grid, axes — that the camera should not frame. */
400
+ declare const EXCLUDE_FROM_FRAME = "viewerExcludeFromFrame";
401
+ /**
402
+ * What the camera frames: a bounding *sphere*, not a box.
403
+ *
404
+ * A sphere makes framing rotation-invariant — the part stays fully visible from
405
+ * every angle, and a resize never needs to know the current pose. Framing
406
+ * per-axis box dimensions instead means recomputing the camera distance on
407
+ * every resize, and still clipping on some orientations.
408
+ */
409
+ interface SceneBounds {
410
+ readonly center: Vector3;
411
+ readonly radius: number;
412
+ }
413
+ /**
414
+ * Bounds for a scene with nothing in it. A viewer is mounted before it has a
415
+ * part, and a camera with a zero-size frustum renders nothing at all.
416
+ */
417
+ declare function defaultBounds(): SceneBounds;
418
+ /**
419
+ * A container can be laid out at zero size, and a degenerate aspect ratio
420
+ * produces a `NaN` projection matrix that never recovers. Fall back to square.
421
+ */
422
+ declare function aspectRatio(size: ViewportSize): number;
423
+ /**
424
+ * A world length that covers `pixels` on screen at `point`.
425
+ *
426
+ * Anything sized in world units is a thumbnail on a plate and a wall on an
427
+ * insert; the whole reason a control is measured this way is that it is a
428
+ * control rather than part of the model. The section handle and the orbit
429
+ * target marker both size themselves with it, which is why it lives here with
430
+ * the camera rather than beside either of them.
431
+ *
432
+ * The two projections answer differently on purpose. An orthographic frustum
433
+ * covers the same world height wherever the point is, so distance does not
434
+ * enter; a perspective one covers more the further away it looks, so it does.
435
+ */
436
+ declare function screenLength(camera: ViewerCamera, point: Vector3, viewport: ViewportSize, pixels: number): number;
437
+ declare function boundsFromBox(box: Box3): SceneBounds;
438
+ /**
439
+ * The bounds of everything worth framing under `root`.
440
+ *
441
+ * Skips objects flagged with {@link EXCLUDE_FROM_FRAME} and their descendants:
442
+ * a 100 mm grid around a 6 mm part would otherwise frame the grid, and the part
443
+ * would be a speck.
444
+ */
445
+ declare function contentBounds(root: Object3D, into: Box3): SceneBounds;
446
+ /**
447
+ * The distance at which a sphere of `radius` fits inside a perspective frustum.
448
+ * Uses the narrower of the two field-of-view angles, so a portrait viewport
449
+ * frames on width and a landscape one on height.
450
+ */
451
+ declare function perspectiveFitDistance(fovDegrees: number, aspect: number, radius: number): number;
452
+ /**
453
+ * Half-height of an orthographic frustum that fits a sphere of `radius`. A
454
+ * portrait viewport grows the height so the width still clears the sphere.
455
+ */
456
+ declare function orthographicHalfHeight(aspect: number, radius: number): number;
457
+ declare function fitDistance(projection: Projection, size: ViewportSize, bounds: SceneBounds, margin?: number): number;
458
+ declare function startPosition(projection: Projection, size: ViewportSize, bounds: SceneBounds, margin?: number): Vector3;
459
+ /**
460
+ * How far out the wheel may pull, as a fraction of the fitted framing: the part
461
+ * may shrink to a quarter of the view and no further. Past that it is a speck
462
+ * with no cue that Fit is the way back.
463
+ */
464
+ declare const MIN_FRAME_RATIO = 0.25;
465
+ /**
466
+ * How far in the wheel may push, as a multiple of the fitted framing.
467
+ *
468
+ * Both ratios are the legacy viewer's `minZoom` 0.25 / `maxZoom` 10
469
+ * (`three-object.tsx:289-311`), which transfer almost exactly: legacy's
470
+ * orthographic frustum was `largestDimension * 2` tall and this one is the
471
+ * fitted bounding sphere, which for a cube agree within a few percent. So the
472
+ * numbers are the ones that shipped, and here they carry a meaning they did not
473
+ * have there — `zoom` 1 *is* the fitted framing, so 0.25 and 10 say exactly
474
+ * what a reader thinks they say.
475
+ */
476
+ declare const MAX_FRAME_RATIO = 10;
477
+ /** How far the camera may travel, in the two units the two cameras travel in. */
478
+ interface CameraLimits {
479
+ readonly minZoom: number;
480
+ readonly maxZoom: number;
481
+ readonly minDistance: number;
482
+ readonly maxDistance: number;
483
+ }
484
+ /**
485
+ * The clamps for a scene, so the wheel cannot leave the viewport empty.
486
+ *
487
+ * Without them it can, from either end, and both cameras can do it: an
488
+ * orthographic `camera.zoom` reaches 1e30 in sixty notches, and a perspective
489
+ * camera dives inside the part in eight, because `minDistance` defaults to
490
+ * `Number.EPSILON`. Fit recovers from both — but Fit is a double middle click
491
+ * and nothing on screen says so, which makes it a way out rather than an
492
+ * answer.
493
+ *
494
+ * One rule for both cameras: the wheel may take the part from a quarter of its
495
+ * fitted size to ten times it. Under an orthographic camera that scale *is* the
496
+ * frustum, so the ratios land on `zoom`; under a perspective camera apparent
497
+ * size is the inverse of distance, so they land on `distance` — which is why
498
+ * they swap sides between the two.
499
+ *
500
+ * Pure, and derived from `bounds`, so it re-derives wherever the scene is
501
+ * re-measured rather than being sampled once at mount.
502
+ *
503
+ * `framed` is what the view has been framed *on*, when that is not the whole
504
+ * scene — `frameBox` on a single feature. The band is then taken about that
505
+ * framing as well as about the scene's, because a band centred on the part is
506
+ * the wrong band for a view of a 3 mm hole in it: ten times the *part's* fitted
507
+ * size can be less than once the hole's, so the framing the caller asked for sat
508
+ * outside the clamps meant to keep it in view. Under an orthographic camera
509
+ * `zoom` is measured against a frustum still sized to the scene, so the framing
510
+ * enters as the zoom ratio it takes to reach; under a perspective one it enters
511
+ * as its own fitted distance.
512
+ *
513
+ * Widened to the union of the two rather than moved onto `framed`, and that is
514
+ * the whole of why this takes both. A band that simply followed the last framing
515
+ * would take the part away: framing a hole would put "far enough out to see what
516
+ * the hole is in" past `maxDistance`, so the wheel could no longer get back to
517
+ * the part it had just been looking at, and Fit — a gesture with nothing on
518
+ * screen to say so — would be the only way. Reaching further in must not cost
519
+ * the reach back out, so each end takes whichever bound is looser. With no
520
+ * `framed`, or one the size of the scene, both ends collapse to the scene's own
521
+ * and this is the function it was before.
522
+ */
523
+ declare function cameraLimits(projection: Projection, size: ViewportSize, bounds: SceneBounds, margin?: number, framed?: SceneBounds): CameraLimits;
524
+ /**
525
+ * The box the orbit target is confined to — the fitted framing, several frames
526
+ * over. Written into `into` so the per-frame path allocates nothing.
527
+ */
528
+ declare function targetBoundary(bounds: SceneBounds, into: Box3, margin?: number): Box3;
529
+ /**
530
+ * Points an existing camera's frustum at `bounds` for the current viewport.
531
+ * Pose is untouched — that belongs to the controls.
532
+ */
533
+ declare function applyProjection(camera: ViewerCamera, size: ViewportSize, bounds: SceneBounds, margin?: number): void;
534
+ /**
535
+ * The world-up convention. The part data is Z-up (millimetres, no conversion) —
536
+ * not the glTF-conventional Y-up — so the camera has to say so explicitly.
537
+ */
538
+ declare const CAD_CAMERA_UP: Vector3;
539
+ /**
540
+ * `up` re-squared against a view direction: the part of it perpendicular to
541
+ * `view`, written into `into`.
542
+ *
543
+ * This is what keeps the horizon level while a free orbit carries the camera
544
+ * over a pole — the up vector follows the view instead of flipping. The
545
+ * controls run it on every update, which is what makes the next sentence the
546
+ * important one.
547
+ *
548
+ * **It is path-dependent.** It is a projection, so it discards the component it
549
+ * removes and cannot put it back: running it at one view and then another does
550
+ * not give what running it once at the second view would. So a camera carries
551
+ * the roll of every pose it has passed through, and any pose that is meant to
552
+ * be canonical — the opening frame, a reset, a named view — has to say what
553
+ * `up` is rather than inherit it. `viewer.tsx` does, at `resetContent`. The
554
+ * test beside this pins both halves.
555
+ *
556
+ * `into` may be `view` or `up` — three.js `crossVectors` reads all six
557
+ * components before it writes any, and the intermediate is this module's own
558
+ * scratch, so writing the result over an input is safe. Said here because this
559
+ * is exported and an out-parameter invites the question; the test beside this
560
+ * pins it, so a three.js change cannot quietly turn the answer around.
561
+ *
562
+ * `view` and `up` being parallel is the one degenerate case: their cross is
563
+ * zero, `normalize` leaves it zero, and the result is a zero vector rather than
564
+ * an error. A caller with a free-orbiting camera reaches that at the pole,
565
+ * which is what `freeOrbit` and a squared `up` at every canonical pose exist to
566
+ * keep it away from.
567
+ */
568
+ declare function adaptedUp(view: Vector3, up: Vector3, into: Vector3): Vector3;
569
+ /** Named viewing directions, as unit vectors from the part toward the camera. */
570
+ declare const cadViewDirections: {
571
+ readonly front: Vector3;
572
+ readonly back: Vector3;
573
+ readonly left: Vector3;
574
+ readonly right: Vector3;
575
+ readonly top: Vector3;
576
+ readonly bottom: Vector3;
577
+ readonly isometric: Vector3;
578
+ };
579
+ type ViewerView = keyof typeof cadViewDirections;
580
+ /** The current orbit direction, so Fit can retain the direction being looked from. */
581
+ declare function currentViewDirection(camera: ViewerCamera, target: Vector3, into: Vector3): Vector3;
582
+
308
583
  /**
309
584
  * Mouse and trackpad presets.
310
585
  *
@@ -341,6 +616,20 @@ type ExtendedCameraControlsOptions = {
341
616
  declare class ExtendedCameraControls extends CameraControls {
342
617
  #private;
343
618
  constructor(camera: ViewerCamera, domElement: HTMLElement, options?: ExtendedCameraControlsOptions);
619
+ /**
620
+ * Bounds how far the wheel may travel, and where the orbit target may go.
621
+ *
622
+ * Re-applied whenever the scene is re-measured rather than set once: the
623
+ * limits are derived from the part's bounds, and a viewer that loads a second
624
+ * part would otherwise keep the first part's idea of far.
625
+ *
626
+ * The boundary confines the *target*. Zoom-to-cursor moves it, and goes on
627
+ * moving it after the zoom clamp bites — which is a runaway no zoom clamp can
628
+ * catch. `boundaryEnclosesCamera` stays off: under an orthographic camera the
629
+ * camera sits well outside the framing it is looking at, so enclosing it would
630
+ * drag the target back in on every frame.
631
+ */
632
+ applyLimits(limits: CameraLimits, boundary?: Box3): void;
344
633
  get freeOrbit(): boolean;
345
634
  get scheme(): ControlScheme;
346
635
  /** Adds the listeners this subclass owns, on top of the base connection. */
@@ -368,6 +657,15 @@ interface CadCameraControlsProps {
368
657
  * about an orbit control.
369
658
  */
370
659
  freeOrbit?: boolean;
660
+ /**
661
+ * What the wheel zooms toward: the pointer, or the middle of the view.
662
+ *
663
+ * Fusion and SolidWorks zoom to the cursor and most people expect it — you
664
+ * point at the corner you want and lean in. It is not universally liked: on a
665
+ * trackpad it can walk the model off screen, which is why the other one
666
+ * stays.
667
+ */
668
+ zoomTo?: 'cursor' | 'centre';
371
669
  }
372
670
  /**
373
671
  * Mounts `camera-controls` against the R3F camera and canvas.
@@ -378,7 +676,7 @@ interface CadCameraControlsProps {
378
676
  * also registered as R3F's default `controls`, which is how anything else in
379
677
  * the scene reaches the orbit target.
380
678
  */
381
- declare const CadCameraControls: ({ controlsRef, scheme, freeOrbit, }: CadCameraControlsProps) => null;
679
+ declare const CadCameraControls: ({ controlsRef, scheme, freeOrbit, zoomTo, }: CadCameraControlsProps) => null;
382
680
 
383
681
  interface ViewerControls {
384
682
  /** Frames the part without changing the current viewing direction. */
@@ -410,16 +708,33 @@ interface ViewerControls {
410
708
  }
411
709
  interface ViewerHandle extends ViewerControls {
412
710
  }
711
+ type VectorLike = Pick<Vector3, 'x' | 'y' | 'z'>;
413
712
 
414
713
  declare const useViewerControls: () => ViewerControls;
714
+ /** Re-aims the orbit, or `null` when this viewer has the gesture turned off. */
715
+ type Retarget = ((point: VectorLike) => void) | null;
716
+ /**
717
+ * How the part reports "orbit about this from now on".
718
+ *
719
+ * Split this way because the two halves belong to different places: only the
720
+ * part knows *where* the pointer landed, and it knows it from the raycast the
721
+ * selection already ran, so nothing here casts a second ray. Only the viewer
722
+ * knows whether the gesture is wanted, because `retargetOnDoubleClick` is its
723
+ * prop. `null` is that answer, and it turns the pairing off at the source
724
+ * rather than making the part detect a gesture nobody will act on.
725
+ */
726
+ declare const useRetarget: () => Retarget;
415
727
  interface ViewerProps {
416
728
  children?: ReactNode;
417
729
  className?: string;
418
730
  style?: CSSProperties;
419
731
  /**
420
- * Perspective by default. Orthographic is what a machinist reads a part in
421
- * parallel edges stay parallel, so two features the same size measure the
422
- * same size wherever they sit.
732
+ * **Orthographic by default.** It is what a machinist reads a part in:
733
+ * parallel edges stay parallel, so a wall that looks square is square, and
734
+ * two features the same size measure the same size wherever they sit.
735
+ *
736
+ * Perspective is the better answer for reading a deep pocket *as* depth, and
737
+ * is one prop away.
423
738
  */
424
739
  projection?: Projection;
425
740
  /**
@@ -430,6 +745,57 @@ interface ViewerProps {
430
745
  controls?: ControlScheme;
431
746
  /** Orbit past the poles instead of stopping there. On by default. */
432
747
  freeOrbit?: boolean;
748
+ /**
749
+ * What the wheel zooms toward: the pointer, or the middle of the view.
750
+ *
751
+ * `cursor` by default, which is what Fusion does and what most people reach
752
+ * for. It is a preference rather than a right answer — on a trackpad it can
753
+ * walk the model off screen.
754
+ */
755
+ zoomTo?: 'cursor' | 'centre';
756
+ /**
757
+ * Whether a double **middle** click re-frames the part. On by default — it is
758
+ * the way back from having zoomed into a corner, which zooming to the cursor
759
+ * makes easy to do.
760
+ */
761
+ recentreOnDoubleClick?: boolean;
762
+ /**
763
+ * Whether a double **left** click on the part orbits about what was clicked
764
+ * from then on. On by default.
765
+ *
766
+ * The clicked point moves to the middle of the view at the same size and
767
+ * angle, and stays the pivot until something else moves it. It is how you get
768
+ * from "the whole part" to "this corner" without losing the ability to turn
769
+ * what you are looking at — and under an orthographic camera it is the only
770
+ * gesture that re-aims the pivot at all, because the wheel there scales a
771
+ * frustum rather than travelling toward anything.
772
+ *
773
+ * The move is immediate rather than eased: the damping these controls ship
774
+ * with settles a transition inside one frame. Turn on {@link showOrbitTarget}
775
+ * if the pivot moving needs to be visible.
776
+ *
777
+ * The click is still a click. Its pick arrives with `doubled: true` on it, so
778
+ * an app that wants a double click to mean something of its own can say so,
779
+ * and one that would rather a double click left the selection alone can
780
+ * ignore it. What the viewport claims is the *view*, not the pick.
781
+ */
782
+ retargetOnDoubleClick?: boolean;
783
+ /**
784
+ * Whether to show two small circles at the orbit target — the point the view
785
+ * turns and zooms about. **Off by default.**
786
+ *
787
+ * They are up while a gesture is running and flash when the pivot moves on
788
+ * its own: a cursor zoom walking it, a double click re-aiming it, a Fit
789
+ * putting it back. Then they fade. It answers "why did the part swing that
790
+ * way", which nothing else on screen does, and it is what makes a wheel that
791
+ * has carried the pivot off the part legible while it is happening rather
792
+ * than after.
793
+ *
794
+ * Off by default because it is an aid rather than furniture: a viewer that
795
+ * grew a dot in the middle of every screenshot would be a surprise to anybody
796
+ * already rendering with this.
797
+ */
798
+ showOrbitTarget?: boolean;
433
799
  /** Lighting and background. The part's own colours are tuned against this rig. */
434
800
  theme?: Partial<ViewerTheme>;
435
801
  /**
@@ -602,4 +968,117 @@ declare function bestOwner(model: PartModel, owners: readonly FeatureTag[], cont
602
968
  */
603
969
  declare function cycleOwner(owners: readonly FeatureTag[], current: FeatureTag | null): FeatureTag | null;
604
970
 
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 };
971
+ /** A camera and the point it orbits, together, because neither moves alone. */
972
+ interface RetargetPose {
973
+ readonly position: Vec3;
974
+ readonly target: Vec3;
975
+ }
976
+ /**
977
+ * Where the camera and its orbit target go when somebody asks to orbit about a
978
+ * point on the part.
979
+ *
980
+ * The target moves to the point and the camera moves by the same delta, so the
981
+ * offset between them — the viewing direction and the distance along it — comes
982
+ * through untouched. Only the pivot changes: what was clicked ends up in the
983
+ * middle of the view, at the same size, seen from the same angle, and every
984
+ * orbit and zoom afterwards is about it. This says where the pose lands, not
985
+ * how it gets there — how fast is the caller's transition and the controls'
986
+ * damping, and in this viewer that is one frame.
987
+ *
988
+ * Moving the target alone would swing the camera to look at the new point from
989
+ * the old place, which reads as the part being knocked sideways. Moving the
990
+ * camera alone would not change what anything orbits about, which is the whole
991
+ * request.
992
+ *
993
+ * This is what makes an orthographic viewport navigable. Under a perspective
994
+ * camera the wheel travels toward the target and a mis-aimed pivot is something
995
+ * you can dolly out of; under an orthographic one the wheel scales a frustum,
996
+ * so nothing about zooming ever re-aims the pivot, and F9 measured a cursor
997
+ * zoom leaving it in space off the part's corner.
998
+ *
999
+ * Pure, so the arithmetic is testable without a scene: three vectors in, two
1000
+ * out, no camera and no controls.
1001
+ */
1002
+ declare function retargetPose(cameraPosition: Vec3, currentTarget: Vec3, hitPoint: Vec3): RetargetPose;
1003
+
1004
+ /**
1005
+ * The orbit target made briefly visible.
1006
+ *
1007
+ * The pivot is the one thing the viewport moves about and never shows. It is
1008
+ * also the one thing that moves without being dragged: zooming to the cursor
1009
+ * walks it, a double click re-aims it, Fit puts it back. So "why did the part
1010
+ * swing that way" has an answer the screen has never given.
1011
+ *
1012
+ * It is an aid rather than furniture, which is why it is measured in time as
1013
+ * well as in pixels — up while a gesture is running, a flash when the pivot
1014
+ * moves on its own, and gone otherwise. A marker that stayed would be a dot in
1015
+ * the middle of every screenshot.
1016
+ */
1017
+ /** Radius of the solid centre dot, in CSS pixels. */
1018
+ declare const ORBIT_TARGET_PIXELS = 3;
1019
+ /** Outer radius of the ring around it, in CSS pixels. */
1020
+ declare const ORBIT_TARGET_RING_PIXELS = 14;
1021
+ /** The ring's thickness, as a fraction of its outer radius. */
1022
+ declare const ORBIT_TARGET_RING_WIDTH = 0.16;
1023
+ /**
1024
+ * A dot inside a ring, rather than the legacy viewer's two nested spheres.
1025
+ *
1026
+ * Legacy paired a depth-tested outer ball with a dot drawn over everything, so
1027
+ * the part burying the ball said the pivot was inside the material
1028
+ * (`use-target.ts:10-25`). Rendered against a real part that reads as a fault
1029
+ * rather than as a signal: a pivot sitting *on* a surface — which is exactly
1030
+ * what a double-click re-target produces, and the commonest case there is —
1031
+ * cuts the sphere in half, leaving a lens-shaped smudge with the dot off to one
1032
+ * side of it. The depth it was reporting is a question almost nobody asks; that
1033
+ * it looked broken is something everybody would see.
1034
+ *
1035
+ * A billboarded ring is the same marker with the ambiguity taken out. It is the
1036
+ * same shape at every pose, over any surface, and it is what a viewport
1037
+ * reticle looks like everywhere else.
1038
+ */
1039
+ declare const ORBIT_TARGET_COLOR = 3948625;
1040
+ declare const ORBIT_TARGET_RING_COLOR = 7057587;
1041
+ /** A ring is a hint about where, not a second dot. */
1042
+ declare const ORBIT_TARGET_RING_OPACITY = 0.9;
1043
+ /**
1044
+ * Both colours are the package's own rather than {@link ViewerTheme} roles.
1045
+ *
1046
+ * Adding a required field to that interface is a breaking type change for
1047
+ * anybody who builds a whole theme by hand, which is a steep price for an aid
1048
+ * that is off by default and on screen for about a second at a time. They are
1049
+ * palette colours already in use — the slate the cube labels and the section
1050
+ * handle outline take, and the teal that outlines a section cut — so the marker
1051
+ * reads as part of the control family rather than as something imported. If a
1052
+ * consumer asks to re-colour it, the roles go in at the next major and nothing
1053
+ * about this module's shape changes.
1054
+ */
1055
+ /**
1056
+ * How long the marker stays at full strength after the pivot moves on its own,
1057
+ * in milliseconds. Legacy's (`use-target.ts:96`).
1058
+ */
1059
+ declare const ORBIT_TARGET_FLASH_MS = 100;
1060
+ /**
1061
+ * How long it takes to fade once nothing is holding it up, in milliseconds.
1062
+ *
1063
+ * Legacy faded in twenty steps of 0.05 on a 50 ms timer, which is this number
1064
+ * arrived at the long way round (`use-target.ts:79-87`). Long enough that
1065
+ * letting go of an orbit does not make the marker vanish before the eye finds
1066
+ * it, short enough that it is gone before anybody wonders what it is.
1067
+ */
1068
+ declare const ORBIT_TARGET_FADE_MS = 1000;
1069
+ /**
1070
+ * How visible the marker is, `sinceHold` milliseconds after the last thing
1071
+ * holding it up let go.
1072
+ *
1073
+ * Linear, and negative input means something still is — a drag in progress, or
1074
+ * a flash inside its window — so the caller can pass one number rather than
1075
+ * branching on which of the two it is.
1076
+ *
1077
+ * Pure because the fade has to be driven from a frame rather than a timer:
1078
+ * this viewer renders on demand, so a `setInterval` writing an opacity would
1079
+ * change the material and never repaint it. The curve therefore has to be
1080
+ * something a frame can ask for the value of, at whatever moment it runs.
1081
+ */
1082
+ declare function orbitTargetOpacity(sinceHold: number, fade?: number): number;
1083
+
1084
+ export { type ArrowPlacement, Axes, type AxesProps, CAD_CAMERA_UP, CHAMFER, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type ControlScheme, type CubeZone, DEFAULT_FIT_MARGIN, DOUBLE_TAP_MS, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, MAX_FRAME_RATIO, MIN_FRAME_RATIO, type NamedDirection, ORBIT_TARGET_COLOR, ORBIT_TARGET_FADE_MS, ORBIT_TARGET_FLASH_MS, ORBIT_TARGET_PIXELS, ORBIT_TARGET_RING_COLOR, ORBIT_TARGET_RING_OPACITY, ORBIT_TARGET_RING_PIXELS, ORBIT_TARGET_RING_WIDTH, PERSPECTIVE_FOV, PartModel, PartModelFeature, PartModelRegion, PartReportFormatError, type Projection, type RankingContext, RegionIndex, type Retarget, type RetargetPose, type SceneBounds, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, type ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, type ViewerView, type ViewportSize, adaptedUp, applyProjection, arrowPlacement, aspectRatio, bestOwner, boundsFromBox, buildRegionIndex, cadViewDirections, cameraLimits, contentBounds, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, directionIndexOf, directionLabel, featureTypeRank, fitDistance, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, movedFar, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, retargetPose, sameDirection, screenLength, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useTapGuard, useViewerControls, viewKind, viewUp, viewVector, visualSurfaces };