@toolpath/viewer 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,7 +76,7 @@ What each wrapper is for:
76
76
  ├─ <EnginePart> validates the report, loads the mesh, then renders <PartMesh>
77
77
  │ └─ <PartMesh> draws the part and handles hover, click, colours, section cuts
78
78
  ├─ <DirectionArrows> arrows for the directions the part can be machined from
79
- ├─ <Stock> <BoxStock> translucent stock, included when fitting the camera
79
+ ├─ <Stock> translucent geometry, GLB, box or cylinder stock, included in Fit
80
80
  ├─ <SectionTool> optional: click a face or a plane to cut the part open
81
81
  ├─ <MeasureTool> optional: click two points for a distance, three for an angle
82
82
  ├─ <Grid> <Axes> reference geometry, sized to the part
@@ -146,9 +146,8 @@ context rather than a fact of the part surface.
146
146
 
147
147
  ### Stock and display controls
148
148
 
149
- Use `<BoxStock>` for an axis-aligned blank around a part, or `<Stock geometry={stockGeometry} />`
150
- for an actual stock mesh, including cylindrical or irregular blanks. Stock and part coordinates
151
- must use the same millimetre, Z-up frame. Both components include stock in Fit and Reset while
149
+ Use `<Stock>` for a box, cylinder, prepared geometry or GLB bytes. Stock and part coordinates
150
+ must use the same millimetre, Z-up frame. Stock is included in Fit and Reset while
152
151
  keeping section tools, measurements, the grid, and direction arrows sized to the finished part.
153
152
  Stock does not intercept clicks or get clipped by the part's section plane.
154
153
 
@@ -156,7 +155,7 @@ Stock does not intercept clicks or get clipped by the part's section plane.
156
155
  import { useMemo, useRef, useState } from 'react'
157
156
  import {
158
157
  Axes,
159
- BoxStock,
158
+ Stock,
160
159
  DirectionArrows,
161
160
  PartMesh,
162
161
  Viewer,
@@ -187,7 +186,7 @@ export function StockPreview({ model, geometry }: { model: PartModel; geometry:
187
186
  regionHighlights={colors}
188
187
  activeDirection={directions ? direction : null}
189
188
  />
190
- {stock && <BoxStock partGeometry={geometry} allowance={{ wall: 3, floor: 3 }} />}
189
+ {stock && <Stock box={{ partGeometry: geometry, allowance: { wall: 3, floor: 3 } }} />}
191
190
  {axes && <Axes />}
192
191
  <DirectionArrows
193
192
  directions={model.candidateDirections}
@@ -214,25 +213,103 @@ export function StockPreview({ model, geometry }: { model: PartModel; geometry:
214
213
  }
215
214
  ```
216
215
 
217
- `BoxStock.allowance` is padding **per side**, in millimetres. The preferred form is
216
+ For a box preview, pass `box={{ partGeometry, allowance, offset }}`. Allowance is padding
217
+ **per side**, in millimetres. The preferred form is
218
218
  `{ wall, floor }`: wall stock expands X/Y and floor stock expands Z, matching the wall/floor
219
219
  roughing-stock distinction used by machining settings. A number or `{ x, y, z }` remains accepted
220
220
  for uniform or axis-specific padding. It defaults to zero. `offset` translates the stock from the
221
221
  part's bounding-box centre. `boxStockBounds(geometry, allowance, offset)` returns the same `Box3`
222
222
  for displaying dimensions.
223
223
 
224
- For fixed-box stock, pass `dimensions={{ x, y, z }}` instead. `position` accepts
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
225
227
  `model_centered`, `offset_from_top`, or `offset_from_bottom`, and `positionOffset` is the distance
226
228
  from the selected top or bottom bound. All fixed-box values use millimetres and the part's Z-up
227
229
  coordinate frame.
228
230
  Negative/non-finite allowances, non-finite coordinates, and empty or non-positive stock dimensions
229
231
  throw `RangeError`. The 3 mm allowance above is an example, not an automatic stock recommendation.
230
232
 
231
- `Stock` accepts `color`, `opacity` (default `0.2`), `edgeColor`, `edgeOpacity`, and `showEdges`.
232
- `BoxStock` accepts the same appearance props. Caller-provided geometry is never disposed; the
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
233
303
  components dispose their own materials and outlines. Conditionally mount stock to toggle it.
234
304
  Toggling stock preserves the camera; Fit/Reset then frames the visible stock together with the part.
235
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
+
236
313
  `directionHighlights(model, activeDirection?)` returns region colors in the same palette as
237
314
  `DirectionArrows`. For a face with several owners, the most specific feature wins, followed by
238
315
  candidate-direction order and feature tag. Unmatched directions are left unpainted. Hover and
package/dist/index.d.ts CHANGED
@@ -106,8 +106,82 @@ type StockAllowance = number | Vec3 | {
106
106
  wall: number;
107
107
  floor: number;
108
108
  };
109
- /** How an explicit box is positioned relative to the part's Z bounds. */
109
+ /** How explicit fixed stock is positioned relative to the part's Z bounds. */
110
110
  type StockPosition = 'model_centered' | 'offset_from_top' | 'offset_from_bottom';
111
+ /**
112
+ * Round stock in part coordinates, in millimetres: the centre of its base, the
113
+ * direction from the base toward its far end, and its size. The Engine's
114
+ * resolved `StockCylinder` carries the same four figures.
115
+ */
116
+ interface CylinderStockFigure {
117
+ origin: Vec3;
118
+ axis: Vec3;
119
+ diameter: number;
120
+ length: number;
121
+ }
122
+ /**
123
+ * A block squared to a frame of its own rather than to the part, in
124
+ * millimetres. `lower` and `upper` are corners in `frame`, whose point
125
+ * `(x, y, z)` sits at `location + x·refDirection + y·(axis × refDirection) + z·axis`
126
+ * in part coordinates, with `axis` normalised and `refDirection` projected
127
+ * square to it and normalised. This is the shape of the Engine's plan
128
+ * `StockBox`, which is squared to the setup.
129
+ */
130
+ interface BoxStockFigure {
131
+ /** Omit for a box whose corners are already in part coordinates. */
132
+ frame?: {
133
+ location: Vec3;
134
+ axis: Vec3;
135
+ refDirection: Vec3;
136
+ };
137
+ lower: Vec3;
138
+ upper: Vec3;
139
+ }
140
+ /** Box preview around the part: explicit dimensions or per-side allowance. */
141
+ type BoxStockPreview = {
142
+ partGeometry: BufferGeometry;
143
+ offset?: Vec3;
144
+ } & ({
145
+ dimensions: Vec3;
146
+ position?: StockPosition;
147
+ positionOffset?: number;
148
+ allowance?: never;
149
+ } | {
150
+ allowance?: StockAllowance;
151
+ dimensions?: never;
152
+ position?: never;
153
+ positionOffset?: never;
154
+ });
155
+ /** Resolved corners or part-relative preview options, never both. */
156
+ type BoxStockInput = (BoxStockFigure & {
157
+ partGeometry?: never;
158
+ offset?: never;
159
+ allowance?: never;
160
+ dimensions?: never;
161
+ position?: never;
162
+ positionOffset?: never;
163
+ }) | (BoxStockPreview & {
164
+ lower?: never;
165
+ upper?: never;
166
+ frame?: never;
167
+ });
168
+ /** Upright cylinder preview centered on the part's bounding box. */
169
+ interface CylinderStockPreview {
170
+ partGeometry: BufferGeometry;
171
+ diameter: number;
172
+ length: number;
173
+ position?: StockPosition;
174
+ positionOffset?: number;
175
+ }
176
+ /** Resolved cylinder placement or part-relative preview options, never both. */
177
+ type CylinderStockInput = (CylinderStockFigure & {
178
+ partGeometry?: never;
179
+ position?: never;
180
+ positionOffset?: never;
181
+ }) | (CylinderStockPreview & {
182
+ origin?: never;
183
+ axis?: never;
184
+ });
111
185
  /**
112
186
  * Bounds for an explicit fixed box. X/Y are centered on the part's bounding
113
187
  * box; Z is centered, offset from the top, or offset from the bottom. `offset`
@@ -117,36 +191,38 @@ declare function fixedBoxStockBounds(geometry: BufferGeometry, dimensions: Vec3,
117
191
  /** Per-side allowance and centre offset in millimetres, in the part's coordinates. */
118
192
  declare function boxStockBounds(geometry: BufferGeometry, allowance?: StockAllowance, offset?: Vec3): Box3;
119
193
 
120
- interface StockProps {
121
- /** Caller-owned stock mesh in the same millimetre, Z-up coordinates as the part. */
122
- geometry: BufferGeometry;
194
+ interface StockAppearanceProps {
195
+ /** Keep the preceding decoded GLB visible until its replacement is ready. Key by part/run to reset. */
196
+ retainPrevious?: boolean;
197
+ /** Reproduce the legacy Workpiece's materials, duplicate surface pass and edge ordering. */
198
+ renderStyle?: 'overlay' | 'legacy-workpiece';
123
199
  color?: number;
200
+ /** Unlit surface contribution, useful for readable workpiece shading beside the CAD model. */
201
+ emissive?: number;
202
+ /** Shade each triangle independently without changing the source's vertices or normals. */
203
+ flatShading?: boolean;
124
204
  opacity?: number;
125
205
  edgeColor?: number;
126
206
  edgeOpacity?: number;
127
207
  showEdges?: boolean;
128
208
  }
129
- /** Translucent stock, included in Fit but ignored by picking, sections and measurements. */
130
- declare const Stock: ({ geometry, color, opacity, edgeColor, edgeOpacity, showEdges, }: StockProps) => react.JSX.Element;
131
- interface BoxStockProps extends Omit<StockProps, 'geometry'> {
132
- partGeometry: BufferGeometry;
133
- /** Explicit X/Y/Z dimensions, in millimetres, for fixed-box stock. */
134
- dimensions?: Vec3;
135
- /** Position mode for explicit fixed-box stock. Defaults to model-centered. */
136
- position?: StockPosition;
137
- /** Distance from the selected top/bottom part bound, in millimetres. */
138
- positionOffset?: number;
139
- /**
140
- * Stock left around the part, in millimetres. In the `{ wall, floor }` form,
141
- * wall applies to X/Y and floor applies to Z. Number and `{ x, y, z }` forms
142
- * are retained for compatibility. Defaults to zero.
143
- */
144
- allowance?: StockAllowance;
145
- /** Translation from the part's bounding-box centre, in millimetres. */
146
- offset?: Vec3;
209
+ interface StockSources {
210
+ /** Caller-owned geometry; Stock never disposes it. */
211
+ geometry: BufferGeometry;
212
+ /** Caller-provided GLB bytes; Stock owns and cleans up the decoded geometry. */
213
+ glb: ArrayBuffer;
214
+ /** Resolved box corners/frame, or part-relative box preview options. */
215
+ box: BoxStockInput;
216
+ /** Resolved cylinder placement, or part-relative cylinder preview options. */
217
+ cylinder: CylinderStockInput;
147
218
  }
148
- /** An axis-aligned blank around the part. Use Stock for an arbitrary stock mesh. */
149
- declare const BoxStock: ({ partGeometry, dimensions, position, positionOffset, allowance, offset, ...props }: BoxStockProps) => react.JSX.Element;
219
+ /** Exactly one source, in the same millimetre, Z-up coordinates as the part. */
220
+ type StockSource = {
221
+ [K in keyof StockSources]: Pick<StockSources, K> & Partial<Record<Exclude<keyof StockSources, K>, never>>;
222
+ }[keyof StockSources];
223
+ type StockProps = StockAppearanceProps & StockSource;
224
+ /** Translucent stock, included in Fit but ignored by picking, sections and measurements. */
225
+ declare const Stock: ({ geometry, glb, box, cylinder, ...appearance }: StockProps) => react.JSX.Element;
150
226
 
151
227
  /** The six-inch (154 mm) reference model bundled with `@toolpath/viewer`. */
152
228
  declare const BANANA_MODEL_URL: string;
@@ -1778,4 +1854,4 @@ declare const ORBIT_TARGET_FADE_MS = 1000;
1778
1854
  */
1779
1855
  declare function orbitTargetOpacity(sinceHold: number, fade?: number): number;
1780
1856
 
1781
- export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, BANANA_GAP, BANANA_MODEL_URL, Banana, type BananaProps, BoxStock, type BoxStockProps, CAD_CAMERA_UP, CHAMFER, CONTROL_SCHEME_OPTIONS, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type ControlSchemeOption, type CubeZone, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, HoverCard, type HoverCardProps, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, 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, POINTS_PER_MEASUREMENT, PartModel, PartModelFeature, PartModelRegion, PartPick, PartReportFormatError, type Projection, type RankingContext, RegionHighlight, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, type SectionSample, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, Stock, type StockAllowance, type StockPosition, type StockProps, 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, ViewerToolbar, type ViewerToolbarControl, type ViewerToolbarControls, type ViewerToolbarControlsProps, type ViewerToolbarProps, ViewerToolbarProvider, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bananaFrameBounds, bananaPosition, bestOwner, boundsFromBox, boxStockBounds, buildRegionIndex, cadViewDirections, cameraLimits, capHit, clipSegments, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionHighlights, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTypeRank, fitDistance, fixedBoxStockBounds, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, pointInContour, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, sampleSection, screenLength, screenPoint, sectionContour, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useSectionStore, useTapGuard, useViewerControls, useViewerToolbar, viewKind, viewUp, viewVector, visualSurfaces };
1857
+ export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, BANANA_GAP, BANANA_MODEL_URL, Banana, type BananaProps, type BoxStockFigure, type BoxStockInput, type BoxStockPreview, CAD_CAMERA_UP, CHAMFER, CONTROL_SCHEME_OPTIONS, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type ControlSchemeOption, type CubeZone, type CylinderStockFigure, type CylinderStockInput, type CylinderStockPreview, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, HoverCard, type HoverCardProps, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, 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, POINTS_PER_MEASUREMENT, PartModel, PartModelFeature, PartModelRegion, PartPick, PartReportFormatError, type Projection, type RankingContext, RegionHighlight, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, type SectionSample, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, Stock, type StockAllowance, type StockAppearanceProps, type StockPosition, type StockProps, type StockSource, 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, ViewerToolbar, type ViewerToolbarControl, type ViewerToolbarControls, type ViewerToolbarControlsProps, type ViewerToolbarProps, ViewerToolbarProvider, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bananaFrameBounds, bananaPosition, bestOwner, boundsFromBox, boxStockBounds, buildRegionIndex, cadViewDirections, cameraLimits, capHit, clipSegments, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionHighlights, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTypeRank, fitDistance, fixedBoxStockBounds, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, pointInContour, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, sampleSection, screenLength, screenPoint, sectionContour, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useSectionStore, useTapGuard, useViewerControls, useViewerToolbar, viewKind, viewUp, viewVector, visualSurfaces };
package/dist/index.js CHANGED
@@ -430,20 +430,105 @@ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
430
430
 
431
431
  // src/stock.tsx
432
432
  import { useThree } from "@react-three/fiber";
433
- import { useEffect as useEffect2, useLayoutEffect, useMemo } from "react";
434
- import { BoxGeometry, Vector3 as Vector32 } from "three";
433
+ import { useEffect as useEffect2, useLayoutEffect, useMemo, useState as useState2 } from "react";
434
+
435
+ // src/render/stock-mesh.ts
436
+ import { Mesh } from "three";
437
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
438
+ async function parseStockGlb(data) {
439
+ const { scene } = await new GLTFLoader().parseAsync(data, "");
440
+ try {
441
+ return stockMeshGeometries(scene);
442
+ } finally {
443
+ scene.traverse((object) => {
444
+ if (!(object instanceof Mesh)) return;
445
+ object.geometry.dispose();
446
+ for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
447
+ material.dispose();
448
+ }
449
+ });
450
+ }
451
+ }
452
+ function stockMeshGeometries(scene) {
453
+ const geometries = [];
454
+ scene.updateWorldMatrix(true, true);
455
+ scene.traverse((object) => {
456
+ if (!(object instanceof Mesh)) return;
457
+ const geometry = object.geometry.clone().applyMatrix4(object.matrixWorld);
458
+ if (!geometry.hasAttribute("normal")) geometry.computeVertexNormals();
459
+ geometries.push(geometry);
460
+ });
461
+ if (geometries.length === 0) throw new Error("The stock GLB contains no mesh.");
462
+ return geometries;
463
+ }
464
+
465
+ // src/render/stock-mesh-buffer.ts
466
+ function createStockMeshBuffer() {
467
+ const owned = /* @__PURE__ */ new Set();
468
+ let disposed = false;
469
+ return {
470
+ request(decode, ready, failed) {
471
+ disposed = false;
472
+ let active = true;
473
+ void decode().then(
474
+ (geometries) => {
475
+ if (!active || disposed) {
476
+ geometries.forEach((geometry) => geometry.dispose());
477
+ return;
478
+ }
479
+ geometries.forEach((geometry) => owned.add(geometry));
480
+ ready(geometries);
481
+ },
482
+ (cause) => {
483
+ if (active && !disposed) {
484
+ failed(cause);
485
+ }
486
+ }
487
+ );
488
+ return () => {
489
+ active = false;
490
+ };
491
+ },
492
+ commit(geometries) {
493
+ const visible = new Set(geometries);
494
+ for (const geometry of owned) {
495
+ if (!visible.has(geometry)) {
496
+ owned.delete(geometry);
497
+ geometry.dispose();
498
+ }
499
+ }
500
+ },
501
+ dispose() {
502
+ disposed = true;
503
+ owned.forEach((geometry) => geometry.dispose());
504
+ owned.clear();
505
+ }
506
+ };
507
+ }
435
508
 
436
509
  // src/render/stock.ts
437
510
  import {
438
511
  Box3,
512
+ BoxGeometry,
513
+ CylinderGeometry,
439
514
  EdgesGeometry,
440
515
  Group,
441
516
  LineBasicMaterial,
442
517
  LineSegments,
443
- Mesh,
518
+ Matrix4,
519
+ Mesh as Mesh2,
444
520
  MeshLambertMaterial,
521
+ Quaternion,
445
522
  Vector3
446
523
  } from "three";
524
+ function boxFigure(bounds) {
525
+ return {
526
+ lower: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
527
+ upper: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }
528
+ };
529
+ }
530
+ var CYLINDER_SEGMENTS = 64;
531
+ var CYLINDER_AXIS = new Vector3(0, 1, 0);
447
532
  function fixedBoxStockBounds(geometry, dimensions, position = "model_centered", positionOffset = 0, offset = { x: 0, y: 0, z: 0 }) {
448
533
  if (![dimensions.x, dimensions.y, dimensions.z].every(Number.isFinite) || [dimensions.x, dimensions.y, dimensions.z].some((value) => value <= 0)) {
449
534
  throw new RangeError("Fixed stock dimensions must be finite and positive.");
@@ -456,13 +541,97 @@ function fixedBoxStockBounds(geometry, dimensions, position = "model_centered",
456
541
  }
457
542
  const part = partBounds(geometry);
458
543
  const center = part.getCenter(new Vector3());
459
- const z = position === "offset_from_top" ? part.max.z + positionOffset - dimensions.z / 2 : position === "offset_from_bottom" ? part.min.z - positionOffset + dimensions.z / 2 : center.z;
460
- const half = new Vector3(dimensions.x / 2, dimensions.y / 2, dimensions.z / 2);
544
+ const z = fixedStockLowerZ(part, dimensions.z, position, positionOffset);
461
545
  return new Box3(
462
- new Vector3(center.x, center.y, z).sub(half),
463
- new Vector3(center.x, center.y, z).add(half)
546
+ new Vector3(center.x - dimensions.x / 2, center.y - dimensions.y / 2, z),
547
+ new Vector3(center.x + dimensions.x / 2, center.y + dimensions.y / 2, z + dimensions.z)
464
548
  ).translate(new Vector3(offset.x, offset.y, offset.z));
465
549
  }
550
+ function fixedCylinderStock(geometry, size, position = "model_centered", positionOffset = 0) {
551
+ if (![size.diameter, size.length].every((value) => Number.isFinite(value) && value > 0)) {
552
+ throw new RangeError("Fixed stock diameter and length must be finite and positive.");
553
+ }
554
+ if (!Number.isFinite(positionOffset)) {
555
+ throw new RangeError("Fixed stock position offset must be finite.");
556
+ }
557
+ const part = partBounds(geometry);
558
+ const center = part.getCenter(new Vector3());
559
+ return {
560
+ origin: {
561
+ x: center.x,
562
+ y: center.y,
563
+ z: fixedStockLowerZ(part, size.length, position, positionOffset)
564
+ },
565
+ axis: { x: 0, y: 0, z: 1 },
566
+ diameter: size.diameter,
567
+ length: size.length
568
+ };
569
+ }
570
+ function fixedStockLowerZ(part, length, position, positionOffset) {
571
+ if (position === "offset_from_top") return part.max.z + positionOffset - length;
572
+ if (position === "offset_from_bottom") return part.min.z - positionOffset;
573
+ return (part.min.z + part.max.z - length) / 2;
574
+ }
575
+ function cylinderStockGeometry(input) {
576
+ const figure = input.partGeometry ? fixedCylinderStock(input.partGeometry, input, input.position, input.positionOffset) : input;
577
+ const { origin, diameter, length } = figure;
578
+ if (![origin.x, origin.y, origin.z].every(Number.isFinite)) {
579
+ throw new RangeError("Cylinder stock origin must be finite.");
580
+ }
581
+ if (![diameter, length].every((value) => Number.isFinite(value) && value > 0)) {
582
+ throw new RangeError("Cylinder stock diameter and length must be finite and positive.");
583
+ }
584
+ const axis = new Vector3(figure.axis.x, figure.axis.y, figure.axis.z);
585
+ const norm = axis.length();
586
+ if (!Number.isFinite(norm) || norm === 0) {
587
+ throw new RangeError("Cylinder stock axis must be finite and non-zero.");
588
+ }
589
+ axis.divideScalar(norm);
590
+ const center = axis.clone().multiplyScalar(length / 2).add(new Vector3(origin.x, origin.y, origin.z));
591
+ return new CylinderGeometry(diameter / 2, diameter / 2, length, CYLINDER_SEGMENTS).applyQuaternion(new Quaternion().setFromUnitVectors(CYLINDER_AXIS, axis)).translate(center.x, center.y, center.z);
592
+ }
593
+ function boxStockGeometry(input) {
594
+ const figure = input.partGeometry ? boxFigure(
595
+ input.dimensions ? fixedBoxStockBounds(
596
+ input.partGeometry,
597
+ input.dimensions,
598
+ input.position,
599
+ input.positionOffset,
600
+ input.offset
601
+ ) : boxStockBounds(input.partGeometry, input.allowance, input.offset)
602
+ ) : input;
603
+ return orientedBoxStockGeometry(figure);
604
+ }
605
+ function orientedBoxStockGeometry(figure) {
606
+ const { lower, upper } = figure;
607
+ const frame = figure.frame ?? {
608
+ location: { x: 0, y: 0, z: 0 },
609
+ axis: { x: 0, y: 0, z: 1 },
610
+ refDirection: { x: 1, y: 0, z: 0 }
611
+ };
612
+ const { location, axis, refDirection } = frame;
613
+ if (![location, axis, refDirection, lower, upper].every(
614
+ (vector) => [vector.x, vector.y, vector.z].every(Number.isFinite)
615
+ )) {
616
+ throw new RangeError("Box stock frame and corners must be finite.");
617
+ }
618
+ const size = new Vector3(upper.x - lower.x, upper.y - lower.y, upper.z - lower.z);
619
+ if (size.toArray().some((value) => !(value > 0))) {
620
+ throw new RangeError("Box stock upper corner must be above its lower corner on every axis.");
621
+ }
622
+ const z = new Vector3(axis.x, axis.y, axis.z).normalize();
623
+ const x = new Vector3(refDirection.x, refDirection.y, refDirection.z);
624
+ const reference = x.length();
625
+ x.addScaledVector(z, -x.dot(z));
626
+ if (!(z.lengthSq() > 0 && x.length() > 1e-9 * reference)) {
627
+ throw new RangeError(
628
+ "Box stock frame axis and reference direction must be non-zero and not parallel."
629
+ );
630
+ }
631
+ x.normalize();
632
+ const y = new Vector3().crossVectors(z, x);
633
+ return new BoxGeometry(size.x, size.y, size.z).translate((lower.x + upper.x) / 2, (lower.y + upper.y) / 2, (lower.z + upper.z) / 2).applyMatrix4(new Matrix4().makeBasis(x, y, z).setPosition(location.x, location.y, location.z));
634
+ }
466
635
  function boxStockBounds(geometry, allowance = 0, offset = { x: 0, y: 0, z: 0 }) {
467
636
  const padding = typeof allowance === "number" ? new Vector3(allowance, allowance, allowance) : "wall" in allowance ? new Vector3(allowance.wall, allowance.wall, allowance.floor) : new Vector3(allowance.x, allowance.y, allowance.z);
468
637
  if (padding.toArray().some((value) => !Number.isFinite(value) || value < 0)) {
@@ -494,81 +663,167 @@ function partBounds(geometry) {
494
663
  }
495
664
  return box;
496
665
  }
497
- function createStock(geometry) {
666
+ function createStock(geometry, legacyWorkpiece = false) {
498
667
  const material = new MeshLambertMaterial({ transparent: true, depthWrite: false });
499
668
  const edgeMaterial = new LineBasicMaterial({ transparent: true, depthWrite: false });
500
- const mesh = new Mesh(geometry, material);
501
- const edgeGeometry = new EdgesGeometry(geometry, 15);
502
- const edges = new LineSegments(edgeGeometry, edgeMaterial);
669
+ const mesh = new Mesh2(geometry, material);
503
670
  const object = new Group();
504
671
  object.userData[STOCK_OBJECT] = true;
505
- mesh.renderOrder = 5;
506
- edges.renderOrder = 6;
672
+ mesh.renderOrder = legacyWorkpiece ? 2 : 5;
507
673
  mesh.raycast = () => {
508
674
  };
509
- edges.raycast = () => {
510
- };
511
- object.add(mesh, edges);
675
+ object.add(mesh);
676
+ if (legacyWorkpiece) {
677
+ const shadow = mesh.clone();
678
+ shadow.raycast = () => {
679
+ };
680
+ object.add(shadow);
681
+ edgeMaterial.depthWrite = true;
682
+ }
683
+ let edges = null;
512
684
  return {
513
685
  object,
514
686
  material,
515
687
  edgeMaterial,
516
- edges,
688
+ /**
689
+ * Shows or hides the outline, building it the first time it is shown: the
690
+ * edge pass reads every triangle, and stock cut by a toolpath has a great
691
+ * many of them.
692
+ */
693
+ showEdges(show) {
694
+ if (show && !edges) {
695
+ edges = new LineSegments(new EdgesGeometry(geometry, 15), edgeMaterial);
696
+ edges.renderOrder = legacyWorkpiece ? 1 : 6;
697
+ edges.raycast = () => {
698
+ };
699
+ object.add(edges);
700
+ }
701
+ if (edges) edges.visible = show;
702
+ },
517
703
  dispose() {
518
704
  material.dispose();
519
705
  edgeMaterial.dispose();
520
- edgeGeometry.dispose();
706
+ edges?.geometry.dispose();
521
707
  }
522
708
  };
523
709
  }
524
710
 
525
711
  // src/stock.tsx
526
712
  import { jsx as jsx4 } from "react/jsx-runtime";
527
- var Stock = ({
713
+ var Stock = ({ geometry, glb, box, cylinder, ...appearance }) => geometry ? /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance }) : glb ? /* @__PURE__ */ jsx4(GlbStock, { glb, ...appearance }) : /* @__PURE__ */ jsx4(ShapeStock, { box, cylinder, ...appearance });
714
+ var GeometryStock = ({
528
715
  geometry,
529
- color = 12176354,
716
+ renderStyle = "overlay",
717
+ color = renderStyle === "legacy-workpiece" ? 16777215 : 12176354,
718
+ emissive = renderStyle === "legacy-workpiece" ? 3948625 : 0,
719
+ flatShading = renderStyle === "legacy-workpiece",
530
720
  opacity = 0.2,
531
- edgeColor = 11058648,
532
- edgeOpacity = 0.75,
721
+ edgeColor = renderStyle === "legacy-workpiece" ? 0 : 11058648,
722
+ edgeOpacity = renderStyle === "legacy-workpiece" ? 0.5 : 0.75,
533
723
  showEdges = true
534
724
  }) => {
535
725
  const invalidate = useThree((state) => state.invalidate);
536
- const stock = useMemo(() => createStock(geometry), [geometry]);
726
+ const stock = useMemo(
727
+ () => createStock(geometry, renderStyle === "legacy-workpiece"),
728
+ [geometry, renderStyle]
729
+ );
537
730
  useEffect2(() => () => stock.dispose(), [stock]);
538
731
  useLayoutEffect(() => {
539
732
  stock.material.color.setHex(color);
733
+ stock.material.emissive.setHex(emissive);
734
+ if (stock.material.flatShading !== flatShading) {
735
+ stock.material.flatShading = flatShading;
736
+ stock.material.needsUpdate = true;
737
+ }
540
738
  stock.material.opacity = opacity;
739
+ stock.material.depthWrite = renderStyle === "legacy-workpiece" && opacity === 1;
740
+ stock.material.polygonOffset = renderStyle === "legacy-workpiece" && opacity === 1;
741
+ stock.material.polygonOffsetFactor = 1;
742
+ stock.material.polygonOffsetUnits = 1;
541
743
  stock.edgeMaterial.color.setHex(edgeColor);
542
744
  stock.edgeMaterial.opacity = edgeOpacity;
543
- stock.edges.visible = showEdges;
745
+ stock.showEdges(showEdges);
544
746
  invalidate();
545
- }, [color, edgeColor, edgeOpacity, invalidate, opacity, showEdges, stock]);
747
+ }, [
748
+ color,
749
+ emissive,
750
+ flatShading,
751
+ edgeColor,
752
+ edgeOpacity,
753
+ invalidate,
754
+ opacity,
755
+ renderStyle,
756
+ showEdges,
757
+ stock
758
+ ]);
546
759
  return /* @__PURE__ */ jsx4("primitive", { object: stock.object, dispose: null });
547
760
  };
548
- var BoxStock = ({
549
- partGeometry,
550
- dimensions,
551
- position = "model_centered",
552
- positionOffset = 0,
553
- allowance = 0,
554
- offset,
555
- ...props
761
+ var GlbStock = ({
762
+ glb,
763
+ retainPrevious = false,
764
+ ...appearance
765
+ }) => {
766
+ const buffer = useMemo(() => createStockMeshBuffer(), []);
767
+ const [decoded, setDecoded] = useState2(null);
768
+ useEffect2(() => () => buffer.dispose(), [buffer]);
769
+ useLayoutEffect(() => {
770
+ buffer.commit(decoded?.geometries ?? []);
771
+ }, [buffer, decoded]);
772
+ useEffect2(
773
+ () => buffer.request(
774
+ () => parseStockGlb(glb),
775
+ (geometries) => {
776
+ setDecoded({ source: glb, geometries });
777
+ },
778
+ (cause) => {
779
+ setDecoded({
780
+ source: glb,
781
+ error: cause instanceof Error ? cause : new Error(String(cause))
782
+ });
783
+ }
784
+ ),
785
+ [buffer, glb]
786
+ );
787
+ if (!decoded || !retainPrevious && decoded.source !== glb) return null;
788
+ if (decoded.error) throw decoded.error;
789
+ return decoded.geometries?.map((geometry) => /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance }, geometry.uuid));
790
+ };
791
+ var keepNonFinite = (_key, value) => typeof value === "number" && !Number.isFinite(value) ? String(value) : value;
792
+ var restoreNonFinite = (_key, value) => value === "NaN" || value === "Infinity" || value === "-Infinity" ? Number(value) : value;
793
+ var ShapeStock = ({
794
+ box,
795
+ cylinder,
796
+ ...appearance
556
797
  }) => {
557
- const ox = offset?.x ?? 0;
558
- const oy = offset?.y ?? 0;
559
- const oz = offset?.z ?? 0;
798
+ const isBox = box !== void 0;
799
+ const partGeometry = box?.partGeometry ?? cylinder?.partGeometry;
800
+ const figure = JSON.stringify(
801
+ box ? box.partGeometry ? {
802
+ dimensions: box.dimensions,
803
+ allowance: box.allowance,
804
+ offset: box.offset,
805
+ position: box.position,
806
+ positionOffset: box.positionOffset
807
+ } : { frame: box.frame, lower: box.lower, upper: box.upper } : cylinder.partGeometry ? {
808
+ diameter: cylinder.diameter,
809
+ length: cylinder.length,
810
+ position: cylinder.position,
811
+ positionOffset: cylinder.positionOffset
812
+ } : {
813
+ origin: cylinder.origin,
814
+ axis: cylinder.axis,
815
+ diameter: cylinder.diameter,
816
+ length: cylinder.length
817
+ },
818
+ keepNonFinite
819
+ );
560
820
  const geometry = useMemo(() => {
561
- const box = dimensions ? fixedBoxStockBounds(partGeometry, dimensions, position, positionOffset, {
562
- x: ox,
563
- y: oy,
564
- z: oz
565
- }) : boxStockBounds(partGeometry, allowance, { x: ox, y: oy, z: oz });
566
- const size = box.getSize(new Vector32());
567
- const center = box.getCenter(new Vector32());
568
- return new BoxGeometry(size.x, size.y, size.z).translate(center.x, center.y, center.z);
569
- }, [allowance, dimensions, ox, oy, oz, partGeometry, position, positionOffset]);
821
+ const options = JSON.parse(figure, restoreNonFinite);
822
+ const input = partGeometry ? { ...options, partGeometry } : options;
823
+ return isBox ? boxStockGeometry(input) : cylinderStockGeometry(input);
824
+ }, [figure, isBox, partGeometry]);
570
825
  useEffect2(() => () => geometry.dispose(), [geometry]);
571
- return /* @__PURE__ */ jsx4(Stock, { geometry, ...props });
826
+ return /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance });
572
827
  };
573
828
 
574
829
  // src/banana.tsx
@@ -577,13 +832,13 @@ import { useThree as useThree2 } from "@react-three/fiber";
577
832
  import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef } from "react";
578
833
 
579
834
  // src/render/banana.ts
580
- import { Vector3 as Vector33 } from "three";
835
+ import { Vector3 as Vector32 } from "three";
581
836
  var BANANA_GAP = 0.1;
582
837
  function bananaPosition(part, banana) {
583
- const size = part.getSize(new Vector33());
838
+ const size = part.getSize(new Vector32());
584
839
  const reach = Math.max(size.x, size.y, size.z);
585
- const centre = part.getCenter(new Vector33());
586
- return new Vector33(
840
+ const centre = part.getCenter(new Vector32());
841
+ return new Vector32(
587
842
  part.max.x + reach * BANANA_GAP - banana.min.x,
588
843
  centre.y - (banana.min.y + banana.max.y) / 2,
589
844
  part.min.z - banana.min.z
@@ -650,16 +905,16 @@ function directionHighlights(model, activeDirection = null) {
650
905
  // src/primitives.tsx
651
906
  import { GizmoHelper } from "@react-three/drei";
652
907
  import { useThree as useThree3 } from "@react-three/fiber";
653
- import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
908
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
654
909
 
655
910
  // src/render/grid.ts
656
- import { BufferGeometry, Float32BufferAttribute, Vector3 as Vector34 } from "three";
911
+ import { BufferGeometry, Float32BufferAttribute, Vector3 as Vector33 } from "three";
657
912
  var STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1e3];
658
913
  var TARGET_CELLS = 10;
659
914
  var OVERHANG = 1.6;
660
915
  function gridSpec(box) {
661
- const size = box.getSize(new Vector34());
662
- const center = box.getCenter(new Vector34());
916
+ const size = box.getSize(new Vector33());
917
+ const center = box.getCenter(new Vector33());
663
918
  const largest = Math.max(size.x, size.y, 1e-6);
664
919
  const ideal = largest / TARGET_CELLS;
665
920
  const step = [...STEPS].reverse().find((candidate) => candidate <= ideal) ?? STEPS[0];
@@ -732,7 +987,7 @@ var CubePanels = ({
732
987
  const controls = useViewerControls();
733
988
  const invalidate = useThree3((state) => state.invalidate);
734
989
  const groupRef = useRef2(null);
735
- const [hovered, setHovered] = useState2(null);
990
+ const [hovered, setHovered] = useState3(null);
736
991
  const panels = useMemo3(
737
992
  () => zones.map((zone) => ({ zone, geometry: panelGeometry(zone) })),
738
993
  [zones]
@@ -801,7 +1056,7 @@ var CubePanels = ({
801
1056
  // src/direction-arrows.tsx
802
1057
  import { useThree as useThree4 } from "@react-three/fiber";
803
1058
  import { useMemo as useMemo4 } from "react";
804
- import { Quaternion, Vector3 as Vector35 } from "three";
1059
+ import { Quaternion as Quaternion2, Vector3 as Vector34 } from "three";
805
1060
  import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
806
1061
  var FURNITURE3 = { [EXCLUDE_FROM_FRAME]: true };
807
1062
  var DirectionArrows = ({
@@ -861,9 +1116,9 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
861
1116
  const invalidate = useThree4((state) => state.invalidate);
862
1117
  const { tip, length, quaternion } = useMemo4(() => {
863
1118
  const placement = arrowPlacement(direction, box);
864
- const aim = new Quaternion().setFromUnitVectors(
1119
+ const aim = new Quaternion2().setFromUnitVectors(
865
1120
  CONE_AXIS,
866
- new Vector35(direction.x, direction.y, direction.z).normalize()
1121
+ new Vector34(direction.x, direction.y, direction.z).normalize()
867
1122
  );
868
1123
  return { tip: placement.tip, length: placement.length, quaternion: aim };
869
1124
  }, [box, direction]);
@@ -890,18 +1145,18 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
890
1145
 
891
1146
  // src/section-tool.tsx
892
1147
  import { useFrame, useThree as useThree5 } from "@react-three/fiber";
893
- import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
1148
+ import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState4, useSyncExternalStore } from "react";
894
1149
  import {
895
1150
  BufferGeometry as BufferGeometry2,
896
1151
  DoubleSide,
897
- Quaternion as Quaternion2,
1152
+ Quaternion as Quaternion3,
898
1153
  Raycaster,
899
1154
  Vector2,
900
- Vector3 as Vector36
1155
+ Vector3 as Vector35
901
1156
  } from "three";
902
1157
  import { Fragment, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
903
1158
  var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
904
- var Z = new Vector36(0, 0, 1);
1159
+ var Z = new Vector35(0, 0, 1);
905
1160
  var AXIS_ROTATIONS = {
906
1161
  x: [0, Math.PI / 2, 0],
907
1162
  y: [Math.PI / 2, 0, 0],
@@ -932,10 +1187,10 @@ var SectionTool = ({ theme }) => {
932
1187
  var useSquare = () => {
933
1188
  const square = useMemo5(
934
1189
  () => new BufferGeometry2().setFromPoints([
935
- new Vector36(-1, 1, 0),
936
- new Vector36(1, 1, 0),
937
- new Vector36(1, -1, 0),
938
- new Vector36(-1, -1, 0)
1190
+ new Vector35(-1, 1, 0),
1191
+ new Vector35(1, 1, 0),
1192
+ new Vector35(1, -1, 0),
1193
+ new Vector35(-1, -1, 0)
939
1194
  ]),
940
1195
  []
941
1196
  );
@@ -952,9 +1207,9 @@ var Picker = ({ box, theme }) => {
952
1207
  const square = useSquare();
953
1208
  const previewRef = useRef3(null);
954
1209
  const hover = useRef3(null);
955
- const [hoveredPlane, setHoveredPlane] = useState3(null);
956
- const span = useMemo5(() => box.getSize(new Vector36()).length(), [box]);
957
- const centre = useMemo5(() => box.getCenter(new Vector36()), [box]);
1210
+ const [hoveredPlane, setHoveredPlane] = useState4(null);
1211
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1212
+ const centre = useMemo5(() => box.getCenter(new Vector35()), [box]);
958
1213
  const previewSize = span * PREVIEW_SCALE;
959
1214
  useEffect4(() => {
960
1215
  store.setPicking(true);
@@ -963,7 +1218,7 @@ var Picker = ({ box, theme }) => {
963
1218
  useEffect4(() => {
964
1219
  const raycaster = new Raycaster();
965
1220
  const pointer = new Vector2();
966
- const aim = new Vector36();
1221
+ const aim = new Vector35();
967
1222
  const hide = () => {
968
1223
  if (!hover.current) return;
969
1224
  hover.current = null;
@@ -1008,7 +1263,7 @@ var Picker = ({ box, theme }) => {
1008
1263
  domElement.removeEventListener("pointerup", up);
1009
1264
  };
1010
1265
  }, [box, camera, domElement, invalidate, isTap, scene, span, store]);
1011
- const [planes, setPlanes] = useState3(() => axesPlanes(box, camera.position));
1266
+ const [planes, setPlanes] = useState4(() => axesPlanes(box, camera.position));
1012
1267
  const key = planes.map((plane) => plane.sign).join();
1013
1268
  useFrame(() => {
1014
1269
  const next = [camera.position.x, camera.position.y, camera.position.z].map((eye, axis) => eye - [centre.x, centre.y, centre.z][axis] < 0 ? -1 : 1).join();
@@ -1107,14 +1362,14 @@ var Picker = ({ box, theme }) => {
1107
1362
  var CutSheet = ({ box, section, theme }) => {
1108
1363
  const square = useSquare();
1109
1364
  const cut = useMemo5(() => resolveSectionPlane(section, box), [box, section]);
1110
- const span = useMemo5(() => box.getSize(new Vector36()).length(), [box]);
1365
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1111
1366
  const placement = useMemo5(() => {
1112
1367
  if (!cut) return null;
1113
1368
  const position = sectionAnchor(box, cut.plane).addScaledVector(
1114
1369
  cut.plane.normal,
1115
1370
  -span * SHEET_SETBACK
1116
1371
  );
1117
- return { position, quaternion: new Quaternion2().setFromUnitVectors(Z, cut.plane.normal) };
1372
+ return { position, quaternion: new Quaternion3().setFromUnitVectors(Z, cut.plane.normal) };
1118
1373
  }, [box, cut, span]);
1119
1374
  if (!placement) return null;
1120
1375
  const size = span * OUTLINE_SCALE;
@@ -1162,26 +1417,26 @@ import {
1162
1417
  useLayoutEffect as useLayoutEffect3,
1163
1418
  useMemo as useMemo6,
1164
1419
  useRef as useRef4,
1165
- useState as useState4,
1420
+ useState as useState5,
1166
1421
  useSyncExternalStore as useSyncExternalStore2
1167
1422
  } from "react";
1168
1423
  import {
1169
- Matrix4,
1424
+ Matrix4 as Matrix42,
1170
1425
  Raycaster as Raycaster2,
1171
1426
  SphereGeometry,
1172
1427
  Vector2 as Vector22,
1173
- Vector3 as Vector38
1428
+ Vector3 as Vector37
1174
1429
  } from "three";
1175
1430
 
1176
1431
  // src/render/resample.ts
1177
- import { Vector3 as Vector37 } from "three";
1432
+ import { Vector3 as Vector36 } from "three";
1178
1433
  var ON_PLANE = 1e-7;
1179
1434
  var STITCH = 1e-5;
1180
1435
  var COLLINEAR = 1e-6;
1181
1436
  function clipSegments(positions, plane) {
1182
1437
  const out = [];
1183
- const a = new Vector37();
1184
- const b = new Vector37();
1438
+ const a = new Vector36();
1439
+ const b = new Vector36();
1185
1440
  const count = Math.floor(positions.length / 6);
1186
1441
  for (let i = 0; i < count; i += 1) {
1187
1442
  a.set(positions[i * 6], positions[i * 6 + 1], positions[i * 6 + 2]);
@@ -1196,11 +1451,11 @@ function clipSegments(positions, plane) {
1196
1451
  return Float32Array.from(out);
1197
1452
  }
1198
1453
  function sectionContour(positions, regionOf, plane) {
1199
- const a = new Vector37();
1200
- const b = new Vector37();
1201
- const c = new Vector37();
1202
- const p = new Vector37();
1203
- const q = new Vector37();
1454
+ const a = new Vector36();
1455
+ const b = new Vector36();
1456
+ const c = new Vector36();
1457
+ const p = new Vector36();
1458
+ const q = new Vector36();
1204
1459
  const byRegion = /* @__PURE__ */ new Map();
1205
1460
  const triangleCount = Math.floor(positions.length / 9);
1206
1461
  for (let t = 0; t < triangleCount; t += 1) {
@@ -1258,17 +1513,17 @@ function stitch(segments, out) {
1258
1513
  return into.set(segments[o], segments[o + 1], segments[o + 2]);
1259
1514
  };
1260
1515
  const direction = (i, into) => {
1261
- const s = new Vector37();
1262
- const e = new Vector37();
1516
+ const s = new Vector36();
1517
+ const e = new Vector36();
1263
1518
  point(i, 0, s);
1264
1519
  point(i, 1, e);
1265
1520
  return into.subVectors(e, s).normalize();
1266
1521
  };
1267
1522
  const consumed = new Uint8Array(count);
1268
- const d = new Vector37();
1269
- const n = new Vector37();
1523
+ const d = new Vector36();
1524
+ const n = new Vector36();
1270
1525
  const next = (i, end) => {
1271
- const at = point(i, end, new Vector37());
1526
+ const at = point(i, end, new Vector36());
1272
1527
  const list = ends.get(key(at.x, at.y, at.z)) ?? [];
1273
1528
  if (list.length !== 2) return null;
1274
1529
  const other = list.find((entry) => entry >> 1 !== i);
@@ -1297,8 +1552,8 @@ function stitch(segments, out) {
1297
1552
  };
1298
1553
  const back = walk(0);
1299
1554
  const forward = walk(1);
1300
- const s = point(back.index, back.end, new Vector37());
1301
- const e = point(forward.index, forward.end, new Vector37());
1555
+ const s = point(back.index, back.end, new Vector36());
1556
+ const e = point(forward.index, forward.end, new Vector36());
1302
1557
  out.push(s.x, s.y, s.z, e.x, e.y, e.z);
1303
1558
  }
1304
1559
  }
@@ -1306,8 +1561,8 @@ function pointInContour(point, plane, contour) {
1306
1561
  const [u, v] = planeAxes(plane);
1307
1562
  const px = point.dot(u);
1308
1563
  const py = point.dot(v);
1309
- const a = new Vector37();
1310
- const b = new Vector37();
1564
+ const a = new Vector36();
1565
+ const b = new Vector36();
1311
1566
  let inside = false;
1312
1567
  const count = Math.floor(contour.length / 6);
1313
1568
  for (let i = 0; i < count; i += 1) {
@@ -1324,15 +1579,15 @@ function pointInContour(point, plane, contour) {
1324
1579
  return inside;
1325
1580
  }
1326
1581
  function capHit(ray, plane, contour) {
1327
- const point = ray.intersectPlane(plane, new Vector37());
1582
+ const point = ray.intersectPlane(plane, new Vector36());
1328
1583
  if (!point) return null;
1329
1584
  return pointInContour(point, plane, contour) ? point : null;
1330
1585
  }
1331
1586
  function planeAxes(plane) {
1332
1587
  const n = plane.normal;
1333
- const seed = Math.abs(n.x) < 0.9 ? new Vector37(1, 0, 0) : new Vector37(0, 1, 0);
1334
- const u = new Vector37().crossVectors(n, seed).normalize();
1335
- const v = new Vector37().crossVectors(n, u).normalize();
1588
+ const seed = Math.abs(n.x) < 0.9 ? new Vector36(1, 0, 0) : new Vector36(0, 1, 0);
1589
+ const u = new Vector36().crossVectors(n, seed).normalize();
1590
+ const v = new Vector36().crossVectors(n, u).normalize();
1336
1591
  return [u, v];
1337
1592
  }
1338
1593
  function sampleSection(meshPositions, regionOf, edgePositions, plane) {
@@ -1361,10 +1616,10 @@ var MeasureTool = ({
1361
1616
  const box = useContentBox();
1362
1617
  const resolved = useMemo6(() => resolveTheme(theme), [theme]);
1363
1618
  const picking = useSyncExternalStore2(store.subscribe, store.isPicking, store.isPicking);
1364
- const [held, setHeld] = useState4([]);
1619
+ const [held, setHeld] = useState5([]);
1365
1620
  const controlled = measurements !== void 0;
1366
1621
  const list = controlled ? measurements : held;
1367
- const [draft, setDraft] = useState4([]);
1622
+ const [draft, setDraft] = useState5([]);
1368
1623
  useEffect5(() => {
1369
1624
  store.setEngaged(true);
1370
1625
  return () => store.setEngaged(false);
@@ -1442,7 +1697,7 @@ var MeasureTool = ({
1442
1697
  ] });
1443
1698
  };
1444
1699
  var NO_POINTER = { pointerEvents: "none" };
1445
- var NOWHERE = [new Vector38(), new Vector38()];
1700
+ var NOWHERE = [new Vector37(), new Vector37()];
1446
1701
  var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1447
1702
  const camera = useThree6((state) => state.camera);
1448
1703
  const scene = useThree6((state) => state.scene);
@@ -1495,7 +1750,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1495
1750
  const snapped = snapAt(target, target.edges, camera, viewport);
1496
1751
  const from = placed[placed.length - 1];
1497
1752
  const lock = shift && from ? lockToAxis(from, snapped.point) : null;
1498
- const found = lock ? { ...snapped, point: new Vector38(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1753
+ const found = lock ? { ...snapped, point: new Vector37(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1499
1754
  snap.current = found;
1500
1755
  const node = marker.current;
1501
1756
  if (node) {
@@ -1657,7 +1912,7 @@ function targetUnderRay(raycaster, scene, samples) {
1657
1912
  edges: samples.edgesFor(hit.object)
1658
1913
  };
1659
1914
  }
1660
- const local = new Matrix4();
1915
+ const local = new Matrix42();
1661
1916
  for (const mesh of cutMeshes(scene)) {
1662
1917
  const planes = clippingPlanesOf(mesh);
1663
1918
  const sample = samples.for(mesh, planes);
@@ -1720,7 +1975,7 @@ var SampleCache = class {
1720
1975
  const key = planes.map((plane) => `${plane.normal.x},${plane.normal.y},${plane.normal.z},${plane.constant}`).join("|");
1721
1976
  const cached = this.held.get(mesh);
1722
1977
  if (cached && cached.key === key) return cached;
1723
- const local = new Matrix4().copy(mesh.matrixWorld).invert();
1978
+ const local = new Matrix42().copy(mesh.matrixWorld).invert();
1724
1979
  const localPlanes = planes.map((plane) => plane.clone().applyMatrix4(local));
1725
1980
  const positions = mesh.geometry.getAttribute("position")?.array ?? [];
1726
1981
  const regionOf = this.regionsOf(mesh.geometry);
@@ -1803,7 +2058,7 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1803
2058
  const lines = useRef4([]);
1804
2059
  const centre = useMemo6(() => {
1805
2060
  const at = midpoint(a, b);
1806
- return new Vector38(at.x, at.y, at.z);
2061
+ return new Vector37(at.x, at.y, at.z);
1807
2062
  }, [a, b]);
1808
2063
  useFrame2(() => {
1809
2064
  const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
@@ -1825,8 +2080,8 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1825
2080
  lines.current[index] = line;
1826
2081
  },
1827
2082
  points: [
1828
- new Vector38(leg.from.x, leg.from.y, leg.from.z),
1829
- new Vector38(leg.to.x, leg.to.y, leg.to.z)
2083
+ new Vector37(leg.from.x, leg.from.y, leg.from.z),
2084
+ new Vector37(leg.to.x, leg.to.y, leg.to.z)
1830
2085
  ],
1831
2086
  color: AXIS_COLORS[leg.axis],
1832
2087
  lineWidth: MEASURE_LINE_PIXELS.leg,
@@ -1862,7 +2117,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1862
2117
  const invalidate = useThree6((state) => state.invalidate);
1863
2118
  const node = useRef4(null);
1864
2119
  const dot = useDot();
1865
- const position = useMemo6(() => new Vector38(point.x, point.y, point.z), [point]);
2120
+ const position = useMemo6(() => new Vector37(point.x, point.y, point.z), [point]);
1866
2121
  useFrame2(() => {
1867
2122
  const group = node.current;
1868
2123
  if (!group) return;
@@ -1877,7 +2132,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1877
2132
  var Segment = ({ points, color }) => {
1878
2133
  const [a, b] = points;
1879
2134
  const ends = useMemo6(
1880
- () => [new Vector38(a.x, a.y, a.z), new Vector38(b.x, b.y, b.z)],
2135
+ () => [new Vector37(a.x, a.y, a.z), new Vector37(b.x, b.y, b.z)],
1881
2136
  // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1882
2137
  // and a rebuilt array would rebuild the line's geometry with it.
1883
2138
  [a.x, a.y, a.z, b.x, b.y, b.z]
@@ -1956,7 +2211,6 @@ export {
1956
2211
  BANANA_GAP,
1957
2212
  BANANA_MODEL_URL,
1958
2213
  Banana,
1959
- BoxStock,
1960
2214
  CAD_CAMERA_UP,
1961
2215
  CANDIDATE_WEIGHT,
1962
2216
  CAP_HATCH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/viewer",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "React Three Fiber viewer for Toolpath Engine part reports",
5
5
  "license": "MIT",
6
6
  "engines": {