@toolpath/viewer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/chunk-7NQBV7EQ.js +2537 -0
- package/dist/engine/index.d.ts +118 -0
- package/dist/engine/index.js +30 -0
- package/dist/index.d.ts +637 -0
- package/dist/index.js +676 -0
- package/dist/normalize-B0HBvzGu.d.ts +868 -0
- package/package.json +68 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Group, Mesh, LineSegments, Box3, Plane, BufferGeometry, Camera, Vector3, OrthographicCamera, PerspectiveCamera, Object3D } from 'three';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The normalized part — the only thing the renderer consumes.
|
|
6
|
+
*
|
|
7
|
+
* `engine/` produces this; nothing under `model/` or `render/` sees a
|
|
8
|
+
* `PartReportResponse`. The renderer therefore survives an API change, and the
|
|
9
|
+
* viewer can be driven from a local file with no API at all, which is also how
|
|
10
|
+
* it gets tested.
|
|
11
|
+
*
|
|
12
|
+
* The mesh is **millimetres** and **Z-up** — not the glTF-conventional Y-up.
|
|
13
|
+
* Machining and candidate directions are unit vectors, and are not always
|
|
14
|
+
* axis-aligned: real reports carry tilted 5-axis setups alongside the six
|
|
15
|
+
* axis-aligned ones.
|
|
16
|
+
*/
|
|
17
|
+
interface Vec3 {
|
|
18
|
+
readonly x: number;
|
|
19
|
+
readonly y: number;
|
|
20
|
+
readonly z: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Opaque per-feature identity — 16 hex characters (64 bits).
|
|
24
|
+
*
|
|
25
|
+
* Never substitute an array position for this. The `features` array is not
|
|
26
|
+
* sorted by anything, and a position-derived identity institutionalizes that
|
|
27
|
+
* mistake.
|
|
28
|
+
*/
|
|
29
|
+
type FeatureTag = string;
|
|
30
|
+
/**
|
|
31
|
+
* Feature types the Engine is known to report. The set is **open** — the
|
|
32
|
+
* kernel adds more — so this stays a widened union rather than a closed enum
|
|
33
|
+
* that breaks on the next kernel release.
|
|
34
|
+
*/
|
|
35
|
+
type KnownFeatureType = 'blind_hole' | 'boss' | 'chamfer' | 'contour_surface' | 'face' | 'filleted_blind_hole' | 'filleted_boss' | 'filleted_open_pocket' | 'filleted_pocket' | 'inner_fillet' | 'open_pocket' | 'outer_fillet' | 'pocket' | 'profile' | 'sink' | 'slanted_face' | 'through_hole' | 'through_pocket' | 'undercut_dovetail' | 'undercut_filleted_tslot' | 'undercut_tslot' | 'wall';
|
|
36
|
+
type FeatureType = KnownFeatureType | (string & Record<never, never>);
|
|
37
|
+
/**
|
|
38
|
+
* The analytic surface classification of a region. The kernel classifies more
|
|
39
|
+
* than the two seen most often, so the union stays open.
|
|
40
|
+
*/
|
|
41
|
+
type KnownShapeKind = 'Cylinder' | 'Plane';
|
|
42
|
+
type ShapeKind = KnownShapeKind | (string & Record<never, never>);
|
|
43
|
+
/** A half-open triangle range: `[start, end)`. */
|
|
44
|
+
interface TriangleRange {
|
|
45
|
+
readonly start: number;
|
|
46
|
+
readonly end: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `feature ↔ region ↔ triangle` mapping, as a pure projection of the report.
|
|
50
|
+
*
|
|
51
|
+
* The asymmetry it encodes is the single most important fact about selection:
|
|
52
|
+
*
|
|
53
|
+
* ```text
|
|
54
|
+
* feature → regions always well-defined (regionIdxs, given)
|
|
55
|
+
* region → feature genuinely one-to-many (no rule fixes this)
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* A region is owned by five to eight features *even on a cube* — measured, not
|
|
59
|
+
* estimated — because the same physical face is a `face` under one machining
|
|
60
|
+
* direction and a `wall` under others, and every direction's `profile` overlaps
|
|
61
|
+
* the surfaces it traces. Nothing here tries to reduce that to one; ranking a
|
|
62
|
+
* viewport click depends on viewer state (active direction, camera) and lives
|
|
63
|
+
* in `render/selection.ts`.
|
|
64
|
+
*
|
|
65
|
+
* The interface exists so a future change in how the Engine expresses the
|
|
66
|
+
* mapping — a per-triangle array, a vertex attribute — is absorbed in one file.
|
|
67
|
+
*/
|
|
68
|
+
interface RegionIndex {
|
|
69
|
+
readonly regionCount: number;
|
|
70
|
+
/**
|
|
71
|
+
* Triangle index → region idx, `O(log R)`.
|
|
72
|
+
*
|
|
73
|
+
* `null` means the report is malformed or the mesh and report are mismatched,
|
|
74
|
+
* **not** a state the UI needs to render gracefully: regions are guaranteed
|
|
75
|
+
* to tile the mesh completely, and `buildRegionIndex` rejects a table that
|
|
76
|
+
* does not. Treat a `null` here as a bug worth logging.
|
|
77
|
+
*/
|
|
78
|
+
regionForTriangle(triangle: number): number | null;
|
|
79
|
+
/** Every feature owning a region, in report order. Measured at 5–8 entries. */
|
|
80
|
+
featuresForRegion(region: number): readonly FeatureTag[];
|
|
81
|
+
/** A feature's regions, for highlight, framing, and isolation. */
|
|
82
|
+
regionsForFeature(tag: FeatureTag): readonly number[];
|
|
83
|
+
/** `null` for an unknown region idx. */
|
|
84
|
+
rangeForRegion(region: number): TriangleRange | null;
|
|
85
|
+
}
|
|
86
|
+
interface PartModelRegion {
|
|
87
|
+
readonly idx: number;
|
|
88
|
+
readonly shapeKind: ShapeKind;
|
|
89
|
+
/**
|
|
90
|
+
* **Analytic** area, not faceted. Use it to sort, filter, and display — never
|
|
91
|
+
* to validate geometry: a computed-vs-reported area check fails by around a
|
|
92
|
+
* percent on perfectly correct data and reads as a triangle-ordering bug.
|
|
93
|
+
*/
|
|
94
|
+
readonly area: number;
|
|
95
|
+
readonly triangles: TriangleRange;
|
|
96
|
+
}
|
|
97
|
+
interface PartModelFeature {
|
|
98
|
+
/** Opaque identity. Never an array position. */
|
|
99
|
+
readonly tag: FeatureTag;
|
|
100
|
+
readonly featureType: FeatureType;
|
|
101
|
+
/** A unit vector, not necessarily axis-aligned. */
|
|
102
|
+
readonly machiningDirection: Vec3;
|
|
103
|
+
/**
|
|
104
|
+
* The feature's own axis, where it has one — a bore's centreline, a wall's
|
|
105
|
+
* normal — and `null` on anything without a natural axis.
|
|
106
|
+
*
|
|
107
|
+
* Kept because it is how somebody names an orientation without knowing its
|
|
108
|
+
* numbers: "hold it square to that hole" is a sentence a machinist says, and
|
|
109
|
+
* this is the vector behind it.
|
|
110
|
+
*/
|
|
111
|
+
readonly axis: Vec3 | null;
|
|
112
|
+
readonly regionIdxs: readonly number[];
|
|
113
|
+
}
|
|
114
|
+
interface PartMeshRefs {
|
|
115
|
+
readonly pointCount: number;
|
|
116
|
+
readonly triangleCount: number;
|
|
117
|
+
/**
|
|
118
|
+
* Presigned and short-lived. Fetch promptly; refetch the report rather than
|
|
119
|
+
* persisting the URL.
|
|
120
|
+
*/
|
|
121
|
+
readonly glbUrl: string | null;
|
|
122
|
+
readonly stlUrl: string | null;
|
|
123
|
+
readonly thumbnailUrl: string | null;
|
|
124
|
+
}
|
|
125
|
+
interface PartModel {
|
|
126
|
+
readonly partId: string;
|
|
127
|
+
readonly kernelVersion: string;
|
|
128
|
+
readonly features: readonly PartModelFeature[];
|
|
129
|
+
readonly regions: readonly PartModelRegion[];
|
|
130
|
+
readonly candidateDirections: readonly Vec3[];
|
|
131
|
+
readonly mesh: PartMeshRefs;
|
|
132
|
+
readonly regionIndex: RegionIndex;
|
|
133
|
+
/** Non-fatal problems found while normalizing. */
|
|
134
|
+
readonly warnings: readonly string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Viewer colors.
|
|
139
|
+
*
|
|
140
|
+
* Plain hex numbers rather than CSS custom properties because three needs
|
|
141
|
+
* numbers; wiring these to a design-token source is a separate decision. The
|
|
142
|
+
* values are tuned against the viewer's light rig, and changing either in
|
|
143
|
+
* isolation changes how every part reads.
|
|
144
|
+
*/
|
|
145
|
+
interface ViewerTheme {
|
|
146
|
+
/** `null` keeps the canvas transparent so the page background shows through. */
|
|
147
|
+
readonly background: number | null;
|
|
148
|
+
readonly hemisphereSky: number;
|
|
149
|
+
readonly hemisphereGround: number;
|
|
150
|
+
readonly hemisphereIntensity: number;
|
|
151
|
+
readonly ambient: number;
|
|
152
|
+
readonly ambientIntensity: number;
|
|
153
|
+
/** Unhighlighted part surface. */
|
|
154
|
+
readonly part: number;
|
|
155
|
+
readonly partEmissive: number;
|
|
156
|
+
/** The region under the cursor. */
|
|
157
|
+
readonly hover: number;
|
|
158
|
+
/** Selected features; see also {@link HIGHLIGHT_COLORS}. */
|
|
159
|
+
readonly highlight: number;
|
|
160
|
+
/**
|
|
161
|
+
* The faces a click just picked — the one thing on the part that is about
|
|
162
|
+
* this moment rather than about the plan.
|
|
163
|
+
*/
|
|
164
|
+
readonly picked: number;
|
|
165
|
+
/** `EdgesGeometry` line color and opacity. */
|
|
166
|
+
readonly edge: number;
|
|
167
|
+
readonly edgeOpacity: number;
|
|
168
|
+
/** The capped face of a section cut, and the cutting plane's outline. */
|
|
169
|
+
readonly sectionCap: number;
|
|
170
|
+
readonly sectionOutline: number;
|
|
171
|
+
/**
|
|
172
|
+
* The arrow that drags the cut, and the shell that outlines it. Hovered it
|
|
173
|
+
* takes {@link ViewerTheme.hover}, like every other control here.
|
|
174
|
+
*/
|
|
175
|
+
readonly sectionHandle: number;
|
|
176
|
+
readonly sectionHandleOutline: number;
|
|
177
|
+
/**
|
|
178
|
+
* The view cube's panels, the lines between them, and its face names. A
|
|
179
|
+
* hovered panel takes {@link ViewerTheme.hover}, the same color the part uses
|
|
180
|
+
* — the cube is a control, and one hover color across the viewport is one
|
|
181
|
+
* thing to learn.
|
|
182
|
+
*/
|
|
183
|
+
readonly cube: number;
|
|
184
|
+
readonly cubeEdge: number;
|
|
185
|
+
readonly cubeLabel: number;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Selection highlight by issue type. `default` is the plain selection color;
|
|
189
|
+
* the other two mark a feature the analysis flagged.
|
|
190
|
+
*/
|
|
191
|
+
declare const HIGHLIGHT_COLORS: {
|
|
192
|
+
readonly default: 16753434;
|
|
193
|
+
readonly toolIssue: 9647082;
|
|
194
|
+
readonly geometryIssue: 16711680;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Per-direction color cycle for the machining-direction overlay.
|
|
198
|
+
*
|
|
199
|
+
* Nine entries, so a part with ten candidate directions wraps — real reports
|
|
200
|
+
* carry exactly ten, which is what makes the wrap reachable rather than
|
|
201
|
+
* theoretical.
|
|
202
|
+
*
|
|
203
|
+
* The same color identifies a direction in three places at once: on the part,
|
|
204
|
+
* on its arrow, and on its row in the directions list. That triple is the point
|
|
205
|
+
* of the palette — it is an identity, not a ranking, which is why the colors
|
|
206
|
+
* are unordered and deliberately not a scale.
|
|
207
|
+
*/
|
|
208
|
+
declare const DIRECTION_COLORS: readonly [3900150, 1357990, 14239471, 440020, 6660877, 15485081, 6583435, 1096065, 6514417];
|
|
209
|
+
declare const DEFAULT_THEME: ViewerTheme;
|
|
210
|
+
declare function resolveTheme(overrides?: Partial<ViewerTheme>): ViewerTheme;
|
|
211
|
+
/**
|
|
212
|
+
* Whether two themes would paint identically.
|
|
213
|
+
*
|
|
214
|
+
* Written out rather than derived from `Object.keys`, which cannot be typed
|
|
215
|
+
* without an assertion — and this way adding a field to {@link ViewerTheme}
|
|
216
|
+
* makes the compiler point here.
|
|
217
|
+
*/
|
|
218
|
+
declare function themesEqual(a: ViewerTheme, b: ViewerTheme): boolean;
|
|
219
|
+
/** The color for a candidate direction, wrapping at {@link DIRECTION_COLORS}. */
|
|
220
|
+
declare function directionColor(index: number): number;
|
|
221
|
+
|
|
222
|
+
/** The vertex attribute carrying each vertex's column in the state texture. */
|
|
223
|
+
declare const REGION_ATTRIBUTE = "aRegion";
|
|
224
|
+
/**
|
|
225
|
+
* A part on screen: one mesh, one draw call, one material.
|
|
226
|
+
*
|
|
227
|
+
* Highlighting is a texture write, not a material change. Every region owns one
|
|
228
|
+
* texel of a `regionCount + 1` wide RGBA texture holding a color and a blend
|
|
229
|
+
* weight, and every vertex carries its region's column in {@link
|
|
230
|
+
* REGION_ATTRIBUTE}. Hover, select, candidate, and dim are then all the same
|
|
231
|
+
* operation — write a texel, flag the texture — with no material churn, no
|
|
232
|
+
* geometry rebuild, and no re-upload of positions.
|
|
233
|
+
*
|
|
234
|
+
* What it replaces allocated a material per visual state and rebuilt the
|
|
235
|
+
* geometry's draw groups on every hover, which is a draw call per highlighted
|
|
236
|
+
* feature and a buffer walk per pointer move.
|
|
237
|
+
*/
|
|
238
|
+
interface PartObject {
|
|
239
|
+
/** Add this to the scene. Holds the mesh and its edges. */
|
|
240
|
+
readonly object: Group;
|
|
241
|
+
readonly mesh: Mesh;
|
|
242
|
+
/** The overlaid edge lines, exposed so a consumer can hide them. */
|
|
243
|
+
readonly edges: LineSegments;
|
|
244
|
+
readonly model: PartModel;
|
|
245
|
+
/** Paints one region. `weight` 0 clears it, 1 replaces the surface color. */
|
|
246
|
+
paintRegion(region: number, color: number, weight: number): void;
|
|
247
|
+
/** What a region is painted with now. `null` for a region it does not have. */
|
|
248
|
+
regionPaint(region: number): RegionPaint | null;
|
|
249
|
+
/** Paints every region a feature owns. */
|
|
250
|
+
paintFeature(tag: FeatureTag, color: number, weight: number): void;
|
|
251
|
+
clearPaint(): void;
|
|
252
|
+
/** A feature's bounds in part space, for framing. `null` if it has none. */
|
|
253
|
+
boxForFeature(tag: FeatureTag): Box3 | null;
|
|
254
|
+
/**
|
|
255
|
+
* Applies a section's clipping plane to the part and its edges. `null`
|
|
256
|
+
* removes it. A setter rather than a constructor argument because a section
|
|
257
|
+
* is toggled far more often than a part is loaded.
|
|
258
|
+
*/
|
|
259
|
+
setClippingPlanes(planes: readonly Plane[] | null): void;
|
|
260
|
+
setTheme(theme: ViewerTheme): void;
|
|
261
|
+
dispose(): void;
|
|
262
|
+
}
|
|
263
|
+
interface RegionPaint {
|
|
264
|
+
readonly color: number;
|
|
265
|
+
/** 0 for untouched, 1 for fully painted. */
|
|
266
|
+
readonly weight: number;
|
|
267
|
+
}
|
|
268
|
+
/** The part of a `PartModel` the buffer builders need. */
|
|
269
|
+
type RegionTable = Pick<PartModel, 'regions'>;
|
|
270
|
+
/**
|
|
271
|
+
* Maps a region's `idx` to its column in the state texture.
|
|
272
|
+
*
|
|
273
|
+
* Real reports number regions densely from zero, but `idx` is documented as an
|
|
274
|
+
* identifier rather than a position, so it is looked up instead of assumed —
|
|
275
|
+
* the same discipline `regionIdxs` needs. The extra column past the end is a
|
|
276
|
+
* permanently transparent slot for a vertex belonging to no region; region
|
|
277
|
+
* tiling is enforced at normalization so nothing should land there, and if
|
|
278
|
+
* something does, it paints nothing rather than painting region zero.
|
|
279
|
+
*/
|
|
280
|
+
declare function buildRegionTexels(model: RegionTable): Map<number, number>;
|
|
281
|
+
/**
|
|
282
|
+
* The per-vertex region attribute, built by walking the region table once.
|
|
283
|
+
*
|
|
284
|
+
* The mesh must be non-indexed — `engine/geometry.ts` guarantees it — because a
|
|
285
|
+
* shared vertex belongs to several regions and there is no single value to
|
|
286
|
+
* write into it. With three vertices per triangle, region `[start, end)` owns
|
|
287
|
+
* the vertex range `[start * 3, end * 3)`, which makes this a handful of `fill`
|
|
288
|
+
* calls.
|
|
289
|
+
*/
|
|
290
|
+
declare function buildRegionAttribute(model: RegionTable, texels: Map<number, number>, vertexCount: number): Float32Array;
|
|
291
|
+
/**
|
|
292
|
+
* Builds the renderable part.
|
|
293
|
+
*
|
|
294
|
+
* Takes exclusive use of `geometry`: it adds {@link REGION_ATTRIBUTE} to it and
|
|
295
|
+
* removes that again on dispose, but never disposes the geometry itself — the
|
|
296
|
+
* loader's caller owns it, and a viewer destroying an object it was handed is a
|
|
297
|
+
* good way to break a cache that is legitimately sharing it. One geometry backs
|
|
298
|
+
* one part; sharing it between two parts at once is unsupported.
|
|
299
|
+
*/
|
|
300
|
+
declare function createPart(model: PartModel, geometry: BufferGeometry, theme: ViewerTheme): PartObject;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A feature the consumer wants coloured, for a reason the viewer does not need
|
|
304
|
+
* to know — a difficulty band, a setup, a material.
|
|
305
|
+
*
|
|
306
|
+
* A colour rather than a concept: the viewer paints it and stays out of what it
|
|
307
|
+
* means. `weight` is how strongly it covers the surface beneath, 0 to 1;
|
|
308
|
+
* omitted, it takes a wash that stays under the selection and candidate layers.
|
|
309
|
+
*/
|
|
310
|
+
interface FeatureHighlight {
|
|
311
|
+
readonly tag: FeatureTag;
|
|
312
|
+
readonly color: number;
|
|
313
|
+
readonly weight?: number;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A colour on one face, named directly rather than through a feature.
|
|
317
|
+
*
|
|
318
|
+
* Features are the usual way to say what a colour means, but a face is a thing
|
|
319
|
+
* in its own right: a consumer proposing work face by face, or showing which
|
|
320
|
+
* part of a feature it is talking about, has no feature tag for "these four
|
|
321
|
+
* faces and not the fifth". So regions can be painted too, over the feature
|
|
322
|
+
* layer and under selection.
|
|
323
|
+
*/
|
|
324
|
+
interface RegionHighlight {
|
|
325
|
+
readonly region: number;
|
|
326
|
+
readonly color: number;
|
|
327
|
+
readonly weight?: number;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* How strongly a consumer's own layer paints.
|
|
331
|
+
*
|
|
332
|
+
* Below a candidate and well below a selection: a wash the whole part can wear
|
|
333
|
+
* at once — every feature banded by how hard it is to cut — has to stay legible
|
|
334
|
+
* underneath the two layers that answer "what did I just click".
|
|
335
|
+
*/
|
|
336
|
+
declare const HIGHLIGHT_WEIGHT = 0.7;
|
|
337
|
+
/** How strongly a candidate paints, relative to a pick. */
|
|
338
|
+
declare const CANDIDATE_WEIGHT = 0.4;
|
|
339
|
+
/** Hover, just under a selection: a question, not a decision. */
|
|
340
|
+
declare const HOVER_WEIGHT = 0.85;
|
|
341
|
+
interface HighlightLayers {
|
|
342
|
+
/** The consumer's own colouring, painted under everything else. */
|
|
343
|
+
readonly highlights?: readonly FeatureHighlight[];
|
|
344
|
+
/** Colours on named faces, over the feature highlights. */
|
|
345
|
+
readonly regionHighlights?: readonly RegionHighlight[];
|
|
346
|
+
/**
|
|
347
|
+
* Every feature a click could have meant, each faintly in its own direction's
|
|
348
|
+
* colour. Distinct from `selection` on purpose: a ranked guess that quietly
|
|
349
|
+
* discarded its alternatives is the main way this interaction goes wrong.
|
|
350
|
+
*/
|
|
351
|
+
readonly candidates?: readonly FeatureTag[];
|
|
352
|
+
/** The features being read. */
|
|
353
|
+
readonly selection?: readonly FeatureTag[];
|
|
354
|
+
/**
|
|
355
|
+
* The faces a click just picked, painted over the reading they resolved to.
|
|
356
|
+
*
|
|
357
|
+
* Above the selection rather than below it, which is where the feature picker
|
|
358
|
+
* puts them — because the picker paints nothing for a guessed reading, so its
|
|
359
|
+
* picked faces are never covered. Here the guess *is* painted, so held faces
|
|
360
|
+
* under it would vanish and a modifier-click would look like it did nothing.
|
|
361
|
+
*/
|
|
362
|
+
readonly pickedRegions?: readonly number[];
|
|
363
|
+
/** Features shown as hovered from outside the viewport — a list row. */
|
|
364
|
+
readonly hoveredFeatures?: readonly FeatureTag[];
|
|
365
|
+
/** The face under the pointer. */
|
|
366
|
+
readonly hoverRegion?: number | null;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Paints the layer stack onto a part.
|
|
370
|
+
*
|
|
371
|
+
* **A face can only be one colour.** The part is one mesh and each region
|
|
372
|
+
* carries a single texel, so every question the part answers has to be answered
|
|
373
|
+
* by one mark. Layers are therefore painted weakest first and each one
|
|
374
|
+
* overwrites what is under it outright — nothing blends.
|
|
375
|
+
*
|
|
376
|
+
* The order is the argument. Layers 1–2 are the consumer's standing opinion;
|
|
377
|
+
* 3–4 are this moment; 5–6 are the pointer. A question asked with the mouse
|
|
378
|
+
* always beats a decision already made, because the decision is still there
|
|
379
|
+
* when the pointer moves away.
|
|
380
|
+
*/
|
|
381
|
+
declare function applyHighlightLayers(part: PartObject, layers: HighlightLayers, theme: ViewerTheme): void;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Which modifier keys were down when the pick happened.
|
|
385
|
+
*
|
|
386
|
+
* Reported rather than interpreted: "hold this to add to the selection" is a
|
|
387
|
+
* platform convention — command on a Mac, control everywhere else — and which
|
|
388
|
+
* key means what belongs to the app, not to a viewport.
|
|
389
|
+
*/
|
|
390
|
+
interface PickModifiers {
|
|
391
|
+
readonly alt: boolean;
|
|
392
|
+
readonly ctrl: boolean;
|
|
393
|
+
readonly meta: boolean;
|
|
394
|
+
readonly shift: boolean;
|
|
395
|
+
/** The right button, on a click that did not become a pan. */
|
|
396
|
+
readonly secondary: boolean;
|
|
397
|
+
}
|
|
398
|
+
declare const NO_MODIFIERS: PickModifiers;
|
|
399
|
+
/**
|
|
400
|
+
* A pointer event on the part, resolved to the face it landed on and the
|
|
401
|
+
* features that own it.
|
|
402
|
+
*
|
|
403
|
+
* `owners` travels alongside `best` on purpose. A click resolves to five to
|
|
404
|
+
* eight readings and the ranking puts one of them up; that is a *default*, not
|
|
405
|
+
* a claim of correctness, and a consumer that was handed only the winner could
|
|
406
|
+
* not offer the alternatives it silently discarded.
|
|
407
|
+
*/
|
|
408
|
+
interface PartPick {
|
|
409
|
+
readonly region: number;
|
|
410
|
+
/** Every feature owning the face, in report order. */
|
|
411
|
+
readonly owners: readonly FeatureTag[];
|
|
412
|
+
/** The same set, ranked for this click. Filtered by an active direction. */
|
|
413
|
+
readonly ranked: readonly FeatureTag[];
|
|
414
|
+
/** The ranked pick, or `null` when nothing here is reachable that way. */
|
|
415
|
+
readonly best: FeatureTag | null;
|
|
416
|
+
readonly triangleIndex: number;
|
|
417
|
+
readonly point: readonly [number, number, number];
|
|
418
|
+
/** The surface's outward normal in world space — the plane under the cursor. */
|
|
419
|
+
readonly normal: readonly [number, number, number];
|
|
420
|
+
readonly modifiers: PickModifiers;
|
|
421
|
+
}
|
|
422
|
+
/** A unit vector from the part toward the camera, for the owner ranking. */
|
|
423
|
+
declare function viewDirection(camera: Camera, target: Vector3): Vec3;
|
|
424
|
+
interface BuildPickInput {
|
|
425
|
+
readonly model: PartModel;
|
|
426
|
+
readonly region: number;
|
|
427
|
+
readonly triangleIndex: number;
|
|
428
|
+
readonly point: readonly [number, number, number];
|
|
429
|
+
readonly normal: readonly [number, number, number];
|
|
430
|
+
readonly modifiers?: PickModifiers;
|
|
431
|
+
/**
|
|
432
|
+
* The machining direction the pick is scoped to, as an index into
|
|
433
|
+
* `candidateDirections`. Narrows the owners to two, one, or **none** — the
|
|
434
|
+
* empty case is real, and reads as "nothing here in this direction" rather
|
|
435
|
+
* than as a pick that missed.
|
|
436
|
+
*/
|
|
437
|
+
readonly activeDirection?: number | null;
|
|
438
|
+
readonly viewDirection?: Vec3 | null;
|
|
439
|
+
}
|
|
440
|
+
declare function buildPick(input: BuildPickInput): PartPick;
|
|
441
|
+
/**
|
|
442
|
+
* The owner to focus for a click, given what the last click focused.
|
|
443
|
+
*
|
|
444
|
+
* Clicking the same face again walks its readings — the standard CAD escape
|
|
445
|
+
* hatch for an ambiguous click — while a click on a different face starts from
|
|
446
|
+
* that face's own best answer.
|
|
447
|
+
*/
|
|
448
|
+
declare function focusForPick(pick: PartPick, previousRegion: number | null, previousFocus: FeatureTag | null): FeatureTag | null;
|
|
449
|
+
|
|
450
|
+
type Projection = 'orthographic' | 'perspective';
|
|
451
|
+
type ViewerCamera = OrthographicCamera | PerspectiveCamera;
|
|
452
|
+
interface ViewportSize {
|
|
453
|
+
readonly width: number;
|
|
454
|
+
readonly height: number;
|
|
455
|
+
}
|
|
456
|
+
/** Vertical field of view, in degrees, for the perspective camera. */
|
|
457
|
+
declare const PERSPECTIVE_FOV = 30;
|
|
458
|
+
/** Padding around the framed bounds, as a multiple of its radius. */
|
|
459
|
+
declare const DEFAULT_FIT_MARGIN = 1.2;
|
|
460
|
+
/** Marks scene furniture — grid, axes — that the camera should not frame. */
|
|
461
|
+
declare const EXCLUDE_FROM_FRAME = "viewerExcludeFromFrame";
|
|
462
|
+
/**
|
|
463
|
+
* What the camera frames: a bounding *sphere*, not a box.
|
|
464
|
+
*
|
|
465
|
+
* A sphere makes framing rotation-invariant — the part stays fully visible from
|
|
466
|
+
* every angle, and a resize never needs to know the current pose. Framing
|
|
467
|
+
* per-axis box dimensions instead means recomputing the camera distance on
|
|
468
|
+
* every resize, and still clipping on some orientations.
|
|
469
|
+
*/
|
|
470
|
+
interface SceneBounds {
|
|
471
|
+
readonly center: Vector3;
|
|
472
|
+
readonly radius: number;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Bounds for a scene with nothing in it. A viewer is mounted before it has a
|
|
476
|
+
* part, and a camera with a zero-size frustum renders nothing at all.
|
|
477
|
+
*/
|
|
478
|
+
declare function defaultBounds(): SceneBounds;
|
|
479
|
+
/**
|
|
480
|
+
* A container can be laid out at zero size, and a degenerate aspect ratio
|
|
481
|
+
* produces a `NaN` projection matrix that never recovers. Fall back to square.
|
|
482
|
+
*/
|
|
483
|
+
declare function aspectRatio(size: ViewportSize): number;
|
|
484
|
+
declare function boundsFromBox(box: Box3): SceneBounds;
|
|
485
|
+
/**
|
|
486
|
+
* The bounds of everything worth framing under `root`.
|
|
487
|
+
*
|
|
488
|
+
* Skips objects flagged with {@link EXCLUDE_FROM_FRAME} and their descendants:
|
|
489
|
+
* a 100 mm grid around a 6 mm part would otherwise frame the grid, and the part
|
|
490
|
+
* would be a speck.
|
|
491
|
+
*/
|
|
492
|
+
declare function contentBounds(root: Object3D, into: Box3): SceneBounds;
|
|
493
|
+
/**
|
|
494
|
+
* The distance at which a sphere of `radius` fits inside a perspective frustum.
|
|
495
|
+
* Uses the narrower of the two field-of-view angles, so a portrait viewport
|
|
496
|
+
* frames on width and a landscape one on height.
|
|
497
|
+
*/
|
|
498
|
+
declare function perspectiveFitDistance(fovDegrees: number, aspect: number, radius: number): number;
|
|
499
|
+
/**
|
|
500
|
+
* Half-height of an orthographic frustum that fits a sphere of `radius`. A
|
|
501
|
+
* portrait viewport grows the height so the width still clears the sphere.
|
|
502
|
+
*/
|
|
503
|
+
declare function orthographicHalfHeight(aspect: number, radius: number): number;
|
|
504
|
+
declare function fitDistance(projection: Projection, size: ViewportSize, bounds: SceneBounds, margin?: number): number;
|
|
505
|
+
declare function startPosition(projection: Projection, size: ViewportSize, bounds: SceneBounds, margin?: number): Vector3;
|
|
506
|
+
/**
|
|
507
|
+
* Points an existing camera's frustum at `bounds` for the current viewport.
|
|
508
|
+
* Pose is untouched — that belongs to the controls.
|
|
509
|
+
*/
|
|
510
|
+
declare function applyProjection(camera: ViewerCamera, size: ViewportSize, bounds: SceneBounds, margin?: number): void;
|
|
511
|
+
/**
|
|
512
|
+
* The world-up convention. The part data is Z-up (millimetres, no conversion) —
|
|
513
|
+
* not the glTF-conventional Y-up — so the camera has to say so explicitly.
|
|
514
|
+
*/
|
|
515
|
+
declare const CAD_CAMERA_UP: Vector3;
|
|
516
|
+
/** Named viewing directions, as unit vectors from the part toward the camera. */
|
|
517
|
+
declare const cadViewDirections: {
|
|
518
|
+
readonly front: Vector3;
|
|
519
|
+
readonly back: Vector3;
|
|
520
|
+
readonly left: Vector3;
|
|
521
|
+
readonly right: Vector3;
|
|
522
|
+
readonly top: Vector3;
|
|
523
|
+
readonly bottom: Vector3;
|
|
524
|
+
readonly isometric: Vector3;
|
|
525
|
+
};
|
|
526
|
+
type ViewerView = keyof typeof cadViewDirections;
|
|
527
|
+
/** The current orbit direction, so Fit can retain the direction being looked from. */
|
|
528
|
+
declare function currentViewDirection(camera: ViewerCamera, target: Vector3, into: Vector3): Vector3;
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Render order. The stencil pass must precede the cap, and the part must draw
|
|
532
|
+
* after both or it overwrites the cap it is supposed to be capped by. The part
|
|
533
|
+
* draws at 3 and its edges at 4, so the handle sits above the cap it stands on.
|
|
534
|
+
*/
|
|
535
|
+
declare const SECTION_RENDER_ORDER: {
|
|
536
|
+
readonly stencil: 1;
|
|
537
|
+
readonly cap: 2;
|
|
538
|
+
readonly handle: 6;
|
|
539
|
+
};
|
|
540
|
+
/** The handle's length on screen, in CSS pixels, whatever the zoom. */
|
|
541
|
+
declare const HANDLE_PIXELS = 78;
|
|
542
|
+
interface SectionBounds {
|
|
543
|
+
/** Plane constant at which the whole part is clipped away. */
|
|
544
|
+
readonly min: number;
|
|
545
|
+
/** Plane constant at which nothing is clipped. */
|
|
546
|
+
readonly max: number;
|
|
547
|
+
}
|
|
548
|
+
/** A point the cut's depth is measured from, and what to call it in a panel. */
|
|
549
|
+
interface SectionAnchor {
|
|
550
|
+
readonly point: Vec3;
|
|
551
|
+
readonly label: string;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* A plane placed at a point rather than swept through the part.
|
|
555
|
+
*
|
|
556
|
+
* `normal` is the plane's own — three keeps the half-space it points into — so
|
|
557
|
+
* it faces *away* from the material the cut removes. Use {@link sectionFromPick}
|
|
558
|
+
* to build one from a picked surface rather than negating by hand.
|
|
559
|
+
*/
|
|
560
|
+
interface SectionPlacement {
|
|
561
|
+
readonly normal: Vec3;
|
|
562
|
+
readonly point: Vec3;
|
|
563
|
+
/** Shown as the cut's reference. Defaults to {@link PICKED_SURFACE_LABEL}. */
|
|
564
|
+
readonly label?: string;
|
|
565
|
+
}
|
|
566
|
+
declare const PICKED_SURFACE_LABEL = "Part surface";
|
|
567
|
+
/**
|
|
568
|
+
* The range of plane constants that sweeps a box along `normal`.
|
|
569
|
+
*
|
|
570
|
+
* Derived from the eight corners rather than from a single axis extent, which is
|
|
571
|
+
* what makes an arbitrary normal work: a tilted plane leaves the box through a
|
|
572
|
+
* corner, and an axis-aligned approximation either stops short of cutting the
|
|
573
|
+
* part or sweeps a long way through empty space before reaching it.
|
|
574
|
+
*
|
|
575
|
+
* `Plane` keeps the half-space where `normal · p + constant > 0`, so a *larger*
|
|
576
|
+
* constant clips less. `min` and `max` are named for the constant, not for how
|
|
577
|
+
* much they remove.
|
|
578
|
+
*/
|
|
579
|
+
declare function sectionBounds(box: Box3, normal: Vec3): SectionBounds;
|
|
580
|
+
/** The plane constant at `t`, from 0 (uncut) to 1 (fully cut away). */
|
|
581
|
+
declare function sectionConstant(bounds: SectionBounds, t: number): number;
|
|
582
|
+
/**
|
|
583
|
+
* Where a plane constant sits in the sweep, inverting {@link sectionConstant}.
|
|
584
|
+
*
|
|
585
|
+
* A part with no extent along the normal has a degenerate range and no
|
|
586
|
+
* meaningful position within it; 0 keeps a slider at rest rather than at NaN.
|
|
587
|
+
*/
|
|
588
|
+
declare function sectionOffset(bounds: SectionBounds, constant: number): number;
|
|
589
|
+
/**
|
|
590
|
+
* How far past `anchor` a plane at `constant` cuts, along its own normal.
|
|
591
|
+
*
|
|
592
|
+
* Positive is into the material the anchor's surface faces away from: pick the
|
|
593
|
+
* top of a part and a depth of 3 removes the top 3 mm. Its own inverse, since
|
|
594
|
+
* `depth = −(n · a) − constant` either way.
|
|
595
|
+
*/
|
|
596
|
+
declare function sectionDepth(normal: Vec3, anchor: Vec3, constant: number): number;
|
|
597
|
+
/** The plane constant that cuts `depth` past `anchor`. */
|
|
598
|
+
declare function sectionDepthConstant(normal: Vec3, anchor: Vec3, depth: number): number;
|
|
599
|
+
/** The depths at which the cut starts and finishes, for a bounded control. */
|
|
600
|
+
declare function sectionDepthRange(bounds: SectionBounds, normal: Vec3, anchor: Vec3): {
|
|
601
|
+
readonly min: number;
|
|
602
|
+
readonly max: number;
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Turns a picked surface into a cut that starts at it.
|
|
606
|
+
*
|
|
607
|
+
* The pick reports the surface normal, which faces the viewer; the plane keeps
|
|
608
|
+
* what its own normal points into, so the two are opposite. Getting this
|
|
609
|
+
* backwards leaves the part whole with a plane drawn behind it, which is the
|
|
610
|
+
* failure this helper exists to make unrepeatable.
|
|
611
|
+
*/
|
|
612
|
+
declare function sectionFromPick(surface: {
|
|
613
|
+
readonly point: Vec3;
|
|
614
|
+
readonly normal: Vec3;
|
|
615
|
+
}, label?: string): SectionPlacement;
|
|
616
|
+
/**
|
|
617
|
+
* The starting depth for a cut keyed off a picked surface.
|
|
618
|
+
*
|
|
619
|
+
* A plane placed exactly on the surface it was picked from cuts nothing and
|
|
620
|
+
* z-fights with that surface, so the click reads as having done nothing. It
|
|
621
|
+
* starts a hair inside instead — engaged, and still "at" the face.
|
|
622
|
+
*/
|
|
623
|
+
declare function pickedStartDepth(box: Box3): number;
|
|
624
|
+
/** The plane a cut sits on, for a placement or a swept offset. */
|
|
625
|
+
declare function sectionPlane(box: Box3, normal: Vec3, offset: number, into?: Plane): Plane;
|
|
626
|
+
/**
|
|
627
|
+
* A world length that covers `pixels` on screen at `point`.
|
|
628
|
+
*
|
|
629
|
+
* A handle sized in world units is a thumbnail on a plate and a wall on an
|
|
630
|
+
* insert; the whole reason it is measured this way is that it is a control
|
|
631
|
+
* rather than part of the model.
|
|
632
|
+
*/
|
|
633
|
+
declare function screenLength(camera: ViewerCamera, point: Vector3, viewport: ViewportSize, pixels: number): number;
|
|
634
|
+
/**
|
|
635
|
+
* The plane a drag is projected onto: the one containing the handle's axis and
|
|
636
|
+
* facing the camera as squarely as it can.
|
|
637
|
+
*
|
|
638
|
+
* Dragging along a line in a 3D view has to resolve a 2D pointer to a distance,
|
|
639
|
+
* and this is the surface that makes the pointer track the arrow rather than
|
|
640
|
+
* running away from it when the axis is nearly edge-on.
|
|
641
|
+
*/
|
|
642
|
+
declare function dragPlane(axis: Vector3, view: Vector3, point: Vector3, into?: Plane): Plane;
|
|
643
|
+
|
|
644
|
+
interface SectionOptions {
|
|
645
|
+
enabled: boolean;
|
|
646
|
+
/**
|
|
647
|
+
* The half-space that stays. Defaults to +Z, which keeps the top of the part
|
|
648
|
+
* and eats upward from the bottom as `offset` grows.
|
|
649
|
+
*/
|
|
650
|
+
normal?: Vec3;
|
|
651
|
+
/** Where the sweep sits, 0 (whole part) to 1 (gone). */
|
|
652
|
+
offset?: number;
|
|
653
|
+
/** Key the cut off one surface instead, usually from `sectionFromPick`. */
|
|
654
|
+
plane?: SectionPlacement | null;
|
|
655
|
+
/** How far past that surface to cut, in model units. */
|
|
656
|
+
depth?: number;
|
|
657
|
+
}
|
|
658
|
+
interface SectionState {
|
|
659
|
+
readonly enabled: boolean;
|
|
660
|
+
readonly normal: Vec3;
|
|
661
|
+
readonly offset: number;
|
|
662
|
+
readonly constant: number;
|
|
663
|
+
readonly plane: SectionPlacement | null;
|
|
664
|
+
readonly depth: number | null;
|
|
665
|
+
/**
|
|
666
|
+
* How far the cut can travel from its anchor, in model units, or `null` for a
|
|
667
|
+
* sweep — which is measured as a fraction of the part rather than a distance.
|
|
668
|
+
*
|
|
669
|
+
* Reported because a control that moves the cut has to be bounded by the same
|
|
670
|
+
* numbers the cut is, and only the viewer knows the part's extent along a
|
|
671
|
+
* given normal.
|
|
672
|
+
*/
|
|
673
|
+
readonly depthRange: {
|
|
674
|
+
readonly min: number;
|
|
675
|
+
readonly max: number;
|
|
676
|
+
} | null;
|
|
677
|
+
}
|
|
678
|
+
/** The cut's plane for a set of options, or `null` when there is no cut. */
|
|
679
|
+
declare function resolveSectionPlane(options: SectionOptions | undefined, box: Box3): {
|
|
680
|
+
plane: Plane;
|
|
681
|
+
state: SectionState;
|
|
682
|
+
} | null;
|
|
683
|
+
interface SectionViewProps {
|
|
684
|
+
geometry: BufferGeometry;
|
|
685
|
+
box: Box3;
|
|
686
|
+
plane: Plane;
|
|
687
|
+
theme: ViewerTheme;
|
|
688
|
+
showHandle: boolean;
|
|
689
|
+
onDrag?: (constant: number) => void;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* A clipping plane with a solid cap over the cut, and an arrow that drags it.
|
|
693
|
+
*
|
|
694
|
+
* The cap is the standard two-pass stencil trick: draw the clipped geometry's
|
|
695
|
+
* back faces incrementing the stencil and its front faces decrementing it, so a
|
|
696
|
+
* non-zero stencil marks exactly where the plane passes through solid material,
|
|
697
|
+
* then fill that region with a quad. Without it a section shows the inside of
|
|
698
|
+
* the far wall and the part reads as hollow.
|
|
699
|
+
*
|
|
700
|
+
* **The renderer must be created with `stencil: true`.** three defaults it to
|
|
701
|
+
* false, and without it every one of those stencil operations is a silent
|
|
702
|
+
* no-op — the cut still happens and the part still looks hollow, which is a
|
|
703
|
+
* confusing way to find out.
|
|
704
|
+
*/
|
|
705
|
+
declare const SectionView: ({ geometry, box, plane, theme, showHandle, onDrag, }: SectionViewProps) => react.JSX.Element;
|
|
706
|
+
|
|
707
|
+
interface PartMeshProps {
|
|
708
|
+
/** The normalized report. `engine/normalize.ts` produces it. */
|
|
709
|
+
model: PartModel;
|
|
710
|
+
/**
|
|
711
|
+
* The part's mesh, from `loadPartMesh`, which checks the two describe the
|
|
712
|
+
* same artifact. Owned by the caller: this adds a region attribute to it and
|
|
713
|
+
* removes that again, but never disposes it.
|
|
714
|
+
*/
|
|
715
|
+
geometry: BufferGeometry;
|
|
716
|
+
/**
|
|
717
|
+
* The features being read. The consumer owns this — a feature panel is
|
|
718
|
+
* authoritative for what is selected, because `region → feature` is
|
|
719
|
+
* one-to-many and no scoping rule fixes that.
|
|
720
|
+
*/
|
|
721
|
+
selection?: readonly FeatureTag[];
|
|
722
|
+
/**
|
|
723
|
+
* Every feature a click could have meant, painted faintly in each one's own
|
|
724
|
+
* direction colour, under the selection.
|
|
725
|
+
*/
|
|
726
|
+
candidates?: readonly FeatureTag[];
|
|
727
|
+
/** The consumer's own colouring, painted under everything else. */
|
|
728
|
+
highlights?: readonly FeatureHighlight[];
|
|
729
|
+
/** Colours on named faces, over the feature highlights. */
|
|
730
|
+
regionHighlights?: readonly RegionHighlight[];
|
|
731
|
+
/**
|
|
732
|
+
* The faces a click just picked, painted over the reading they resolved to so
|
|
733
|
+
* that holding a second face shows what it did even when the reading is
|
|
734
|
+
* unchanged.
|
|
735
|
+
*/
|
|
736
|
+
pickedRegions?: readonly number[];
|
|
737
|
+
/**
|
|
738
|
+
* Features to show as hovered from outside the viewport — a list row under
|
|
739
|
+
* the pointer. The face under the pointer *in* the viewport is tracked here
|
|
740
|
+
* and needs no prop.
|
|
741
|
+
*/
|
|
742
|
+
hoveredFeatureIds?: readonly FeatureTag[];
|
|
743
|
+
/**
|
|
744
|
+
* Scopes a pick to one machining direction, as an index into the model's
|
|
745
|
+
* `candidateDirections`. A face that direction cannot reach then picks to
|
|
746
|
+
* nothing, which is a real answer rather than a missed click.
|
|
747
|
+
*/
|
|
748
|
+
activeDirection?: number | null;
|
|
749
|
+
/**
|
|
750
|
+
* The section cut. Omit, or pass `enabled: false`, for none.
|
|
751
|
+
*
|
|
752
|
+
* Either sweep an axis — `normal` points into the half that stays and
|
|
753
|
+
* `offset` runs 0 (whole) to 1 (gone) — or key the cut off one surface with
|
|
754
|
+
* `plane`, which `depth` then moves in model units. `sectionFromPick` turns a
|
|
755
|
+
* pick into that placement with the normal the right way round.
|
|
756
|
+
*/
|
|
757
|
+
section?: SectionOptions;
|
|
758
|
+
/**
|
|
759
|
+
* The cut changed, including when the handle was dragged. Emitted only on a
|
|
760
|
+
* real change, so echoing it into state is safe.
|
|
761
|
+
*/
|
|
762
|
+
onSectionChange?: (state: SectionState) => void;
|
|
763
|
+
/**
|
|
764
|
+
* A feature to frame. Framed when it changes, so setting it to the feature
|
|
765
|
+
* already framed does nothing — a zoom is a request, not a state to hold.
|
|
766
|
+
*/
|
|
767
|
+
focusFeature?: FeatureTag | null;
|
|
768
|
+
onHover?: (pick: PartPick | null) => void;
|
|
769
|
+
/**
|
|
770
|
+
* A click on the part.
|
|
771
|
+
*
|
|
772
|
+
* Never `null`: a mesh's own "missed" event fires whenever *it* was not hit,
|
|
773
|
+
* including when the click landed on an arrow or a section handle, so
|
|
774
|
+
* emitting an empty pick from here made pressing an arrow clear the
|
|
775
|
+
* selection. Clicking nothing at all is a fact about the scene, and
|
|
776
|
+
* `<Viewer onPointerMissed>` is where it is reported.
|
|
777
|
+
*/
|
|
778
|
+
onPick?: (pick: PartPick) => void;
|
|
779
|
+
theme?: Partial<ViewerTheme>;
|
|
780
|
+
showEdges?: boolean;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* The part, as one mesh with one material.
|
|
784
|
+
*
|
|
785
|
+
* Highlighting writes texels rather than swapping materials or rebuilding draw
|
|
786
|
+
* groups — see `render/part.ts` — so a repaint costs one texture upload. That is
|
|
787
|
+
* why the paint runs in a layout effect against a ref rather than being
|
|
788
|
+
* expressed as JSX, and why moving the pointer over the part repaints with no
|
|
789
|
+
* React render at all: the hovered face lives in a ref, and routing it through
|
|
790
|
+
* state is what this rewrite exists to stop doing.
|
|
791
|
+
*/
|
|
792
|
+
declare const PartMesh: ({ model, geometry, selection, candidates, highlights, regionHighlights, pickedRegions, hoveredFeatureIds, activeDirection, section, onSectionChange, focusFeature, onHover, onPick, theme, showEdges, }: PartMeshProps) => react.JSX.Element;
|
|
793
|
+
|
|
794
|
+
interface EnginePartProps extends Omit<PartMeshProps, 'model' | 'geometry'> {
|
|
795
|
+
/**
|
|
796
|
+
* A Toolpath Engine part report, exactly as the API returned it.
|
|
797
|
+
*
|
|
798
|
+
* Typed `unknown` because it is validated rather than trusted: a report read
|
|
799
|
+
* from a file is treated the same as one off the wire. A malformed one throws
|
|
800
|
+
* `PartReportFormatError`, and one from a pre-0.3.0 kernel throws
|
|
801
|
+
* `UnsupportedKernelVersionError` — both worth catching in an error boundary,
|
|
802
|
+
* since neither is a state the viewport can render.
|
|
803
|
+
*/
|
|
804
|
+
report: unknown;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Renders a Toolpath Engine report, fetching the mesh it describes.
|
|
808
|
+
*
|
|
809
|
+
* Suspends while the mesh loads and throws its failure, so a caller wraps it in
|
|
810
|
+
* `<Suspense>` and an error boundary rather than threading loading state
|
|
811
|
+
* through props.
|
|
812
|
+
*/
|
|
813
|
+
declare const EnginePart: ({ report, ...props }: EnginePartProps) => react.JSX.Element;
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Shades each region smoothly and every boundary between them hard.
|
|
817
|
+
*
|
|
818
|
+
* The Engine's mesh carries positions and nothing else, so the normals have to
|
|
819
|
+
* be invented. Averaging them across the whole mesh smooth-shades a cube —
|
|
820
|
+
* every corner reads as a ball bearing. Not averaging at all gives every
|
|
821
|
+
* triangle its own normal, which is honest but leaves a bore looking like a
|
|
822
|
+
* fifty-sided nut, because that is exactly what its triangles are.
|
|
823
|
+
*
|
|
824
|
+
* Neither is necessary here, because the report says which triangles belong to
|
|
825
|
+
* one analytic surface. Averaging *within* a region and never *across* one
|
|
826
|
+
* gives a bore that shades like a bore and an edge that stays an edge: the
|
|
827
|
+
* distinction a mesh cannot express is one the region table can.
|
|
828
|
+
*
|
|
829
|
+
* Two vertices are the same point if their coordinates match exactly. That is
|
|
830
|
+
* safe rather than optimistic — `toNonIndexed` copies each shared vertex from
|
|
831
|
+
* one source value, so the duplicates it makes are bit-identical, which is the
|
|
832
|
+
* only case this needs to find.
|
|
833
|
+
*
|
|
834
|
+
* The geometry must be non-indexed, which `parsePartGeometry` guarantees.
|
|
835
|
+
*/
|
|
836
|
+
declare function smoothRegionNormals(geometry: BufferGeometry, regions: readonly PartModelRegion[]): void;
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* The first kernel to publish `regions[]` and `featureTag`, and therefore the
|
|
840
|
+
* first that can drive feature selection at all.
|
|
841
|
+
*/
|
|
842
|
+
declare const MIN_KERNEL_VERSION = "0.3.0";
|
|
843
|
+
/**
|
|
844
|
+
* Validates a part report and projects it into the `PartModel` the renderer
|
|
845
|
+
* consumes.
|
|
846
|
+
*
|
|
847
|
+
* Takes `unknown` on purpose: a report read from a file gets exactly the same
|
|
848
|
+
* treatment as one off the wire, which is what makes the viewer drivable — and
|
|
849
|
+
* testable — with no API at all. `openapi/openapi.json` is the contract this
|
|
850
|
+
* validates against; the checks are structural so that a response which does
|
|
851
|
+
* not match cannot reach the renderer regardless of how it was typed upstream.
|
|
852
|
+
*
|
|
853
|
+
* Throws `UnsupportedKernelVersionError` for a pre-`0.3.0` report, and
|
|
854
|
+
* `PartReportFormatError` (carrying every problem found, not just the first)
|
|
855
|
+
* for anything else.
|
|
856
|
+
*/
|
|
857
|
+
declare function normalizePartReport(report: unknown): PartModel;
|
|
858
|
+
/**
|
|
859
|
+
* Rejects reports from a kernel older than `0.3.0`.
|
|
860
|
+
*
|
|
861
|
+
* `0.2.0` reports parse *almost* correctly — same envelope, same mesh — but
|
|
862
|
+
* carry `featureIndex` instead of `featureTag` and no `regions[]` at all, so
|
|
863
|
+
* every selection path would silently do nothing. Failing here, with the
|
|
864
|
+
* version named, is the whole point.
|
|
865
|
+
*/
|
|
866
|
+
declare function assertSupportedKernelVersion(kernelVersion: string): void;
|
|
867
|
+
|
|
868
|
+
export { applyHighlightLayers as $, type PartPick as A, type BuildPickInput as B, CAD_CAMERA_UP as C, DEFAULT_FIT_MARGIN as D, EnginePart as E, type FeatureTag as F, type PickModifiers as G, HANDLE_PIXELS as H, REGION_ATTRIBUTE as I, type RegionHighlight as J, type KnownFeatureType as K, type RegionPaint as L, MIN_KERNEL_VERSION as M, NO_MODIFIERS as N, type SceneBounds as O, type PartModel as P, type SectionAnchor as Q, type RegionIndex as R, SECTION_RENDER_ORDER as S, type TriangleRange as T, type SectionBounds as U, type Vec3 as V, type SectionOptions as W, type SectionPlacement as X, type SectionState as Y, SectionView as Z, type ViewportSize as _, type PartMeshRefs as a, applyProjection as a0, aspectRatio as a1, boundsFromBox as a2, buildPick as a3, buildRegionAttribute as a4, buildRegionTexels as a5, cadViewDirections as a6, contentBounds as a7, createPart as a8, currentViewDirection as a9, defaultBounds as aa, directionColor as ab, dragPlane as ac, fitDistance as ad, focusForPick as ae, orthographicHalfHeight as af, perspectiveFitDistance as ag, pickedStartDepth as ah, resolveSectionPlane as ai, resolveTheme as aj, screenLength as ak, sectionBounds as al, sectionConstant as am, sectionDepth as an, sectionDepthConstant as ao, sectionDepthRange as ap, sectionFromPick as aq, sectionOffset as ar, sectionPlane as as, startPosition as at, themesEqual as au, viewDirection as av, type EnginePartProps as b, assertSupportedKernelVersion as c, type ViewerTheme as d, type PartModelRegion as e, type ViewerCamera as f, type ViewerView as g, type Projection as h, type PartModelFeature as i, type FeatureType as j, CANDIDATE_WEIGHT as k, DEFAULT_THEME as l, DIRECTION_COLORS as m, normalizePartReport as n, EXCLUDE_FROM_FRAME as o, type FeatureHighlight as p, HIGHLIGHT_COLORS as q, HIGHLIGHT_WEIGHT as r, smoothRegionNormals as s, HOVER_WEIGHT as t, type HighlightLayers as u, PERSPECTIVE_FOV as v, PICKED_SURFACE_LABEL as w, PartMesh as x, type PartMeshProps as y, type PartObject as z };
|