three-cad-viewer 5.0.6 → 5.1.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/dist/core/picking-controller.d.ts +17 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/viewer.d.ts +32 -0
- package/dist/rendering/cap-outline.d.ts +60 -0
- package/dist/rendering/clip-style.d.ts +49 -0
- package/dist/rendering/highlight.d.ts +5 -0
- package/dist/rendering/id-picking.d.ts +4 -1
- package/dist/scene/clipping.d.ts +7 -0
- package/dist/three-cad-viewer.esm.js +719 -50
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +3 -3
- package/dist/three-cad-viewer.js +719 -50
- package/dist/three-cad-viewer.min.js +3 -3
- package/dist/ui/display.d.ts +5 -0
- package/dist/ui/treeview.d.ts +4 -1
- package/dist/utils/utils.d.ts +14 -1
- package/package.json +1 -1
|
@@ -81829,6 +81829,45 @@ class EventListenerManager {
|
|
|
81829
81829
|
this.listeners = [];
|
|
81830
81830
|
}
|
|
81831
81831
|
}
|
|
81832
|
+
/**
|
|
81833
|
+
* Copy `text` to the clipboard from inside a user-initiated event handler.
|
|
81834
|
+
*
|
|
81835
|
+
* Tries the synchronous hidden-textarea `execCommand("copy")` first: it needs no
|
|
81836
|
+
* clipboard permission, so it also works in embedding iframes and WebKit views that
|
|
81837
|
+
* refuse `navigator.clipboard`. Falls back to the async Clipboard API.
|
|
81838
|
+
* Focus is restored to the previously focused element.
|
|
81839
|
+
*
|
|
81840
|
+
* @param text - The text to copy.
|
|
81841
|
+
* @param parent - Element the temporary textarea is attached to.
|
|
81842
|
+
* @returns true when the synchronous copy reported success.
|
|
81843
|
+
*/
|
|
81844
|
+
function copyText(text, parent) {
|
|
81845
|
+
const previous = document.activeElement;
|
|
81846
|
+
const area = document.createElement("textarea");
|
|
81847
|
+
area.value = text;
|
|
81848
|
+
area.setAttribute("readonly", "");
|
|
81849
|
+
area.style.position = "fixed";
|
|
81850
|
+
area.style.left = "-9999px";
|
|
81851
|
+
area.style.opacity = "0";
|
|
81852
|
+
parent.appendChild(area);
|
|
81853
|
+
area.select();
|
|
81854
|
+
let copied;
|
|
81855
|
+
try {
|
|
81856
|
+
copied = document.execCommand("copy");
|
|
81857
|
+
}
|
|
81858
|
+
catch {
|
|
81859
|
+
copied = false;
|
|
81860
|
+
}
|
|
81861
|
+
parent.removeChild(area);
|
|
81862
|
+
if (previous instanceof HTMLElement)
|
|
81863
|
+
previous.focus({ preventScroll: true });
|
|
81864
|
+
if (!copied && navigator.clipboard !== undefined) {
|
|
81865
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
81866
|
+
logger.debug("Clipboard write refused:", err);
|
|
81867
|
+
});
|
|
81868
|
+
}
|
|
81869
|
+
return copied;
|
|
81870
|
+
}
|
|
81832
81871
|
|
|
81833
81872
|
/**
|
|
81834
81873
|
* Zebra Analysis Tool for Three.js CAD Viewer
|
|
@@ -83999,7 +84038,10 @@ const BACKGROUND_ID = 0;
|
|
|
83999
84038
|
* `Uint32BufferAttribute` under this name; the pick pass reads it as an **integer**
|
|
84000
84039
|
* attribute (`gpuType = THREE.IntType`, GLSL3 `in uint`) so ids stay exact past
|
|
84001
84040
|
* 2^24. The geometry side must set `gpuType = THREE.IntType` on the attribute for
|
|
84002
|
-
* the pick shader to read it.
|
|
84041
|
+
* the pick shader to read it. Between the stages the id travels as two float
|
|
84042
|
+
* varyings (16-bit halves), deliberately NOT as a `flat uint` — see the note in
|
|
84043
|
+
* highlight.ts: a `flat` varying costs ~25x the scene's buffer bytes in graphics
|
|
84044
|
+
* memory under WebKit's ANGLE-on-Metal backend.
|
|
84003
84045
|
*/
|
|
84004
84046
|
const COMPONENT_ID_ATTRIBUTE = "componentId";
|
|
84005
84047
|
/**
|
|
@@ -84297,8 +84339,9 @@ const VERTEX_DEPTH_BIAS_FACTOR = 2e-4;
|
|
|
84297
84339
|
*/
|
|
84298
84340
|
const PICK_POINT_SIZE = 3;
|
|
84299
84341
|
/**
|
|
84300
|
-
* Shared GLSL3 MRT fragment shader for all pick materials (face/vertex/edge):
|
|
84301
|
-
* the
|
|
84342
|
+
* Shared GLSL3 MRT fragment shader for all pick materials (face/vertex/edge):
|
|
84343
|
+
* reassembles the id from its two interpolated halves and packs it into attachment 0
|
|
84344
|
+
* (byte order MUST match `packId()`: low byte in R)
|
|
84302
84345
|
* and writes the interpolated world position into attachment 1 (RGBA32F; `w=1` marks
|
|
84303
84346
|
* "has position"). The fragment clip chunk only `discard`s (non-ALPHA_TO_COVERAGE),
|
|
84304
84347
|
* so no `diffuseColor` symbol is required.
|
|
@@ -84306,13 +84349,15 @@ const PICK_POINT_SIZE = 3;
|
|
|
84306
84349
|
const PICK_FRAGMENT_SHADER = /* glsl */ `
|
|
84307
84350
|
#include <clipping_planes_pars_fragment>
|
|
84308
84351
|
|
|
84309
|
-
|
|
84352
|
+
in float vIdLo;
|
|
84353
|
+
in float vIdHi;
|
|
84310
84354
|
in vec3 vWorldPos;
|
|
84311
84355
|
layout(location = 0) out vec4 fragId;
|
|
84312
84356
|
layout(location = 1) out vec4 fragPos;
|
|
84313
84357
|
|
|
84314
84358
|
void main() {
|
|
84315
84359
|
#include <clipping_planes_fragment>
|
|
84360
|
+
uint vId = ( uint( vIdHi + 0.5 ) << 16 ) | uint( vIdLo + 0.5 );
|
|
84316
84361
|
fragId = vec4(
|
|
84317
84362
|
float( vId & 0xFFu ) / 255.0,
|
|
84318
84363
|
float( ( vId >> 8 ) & 0xFFu ) / 255.0,
|
|
@@ -84330,19 +84375,24 @@ const PICK_FRAGMENT_SHADER = /* glsl */ `
|
|
|
84330
84375
|
function createFacePickMaterial(options = {}) {
|
|
84331
84376
|
// GLSL3 (WebGL2). For a (non-Raw) ShaderMaterial three.js auto-injects
|
|
84332
84377
|
// `in vec3 position;`, `uniform mat4 modelMatrix/modelViewMatrix/projectionMatrix`,
|
|
84333
|
-
// so we declare ONLY the custom integer attribute +
|
|
84334
|
-
//
|
|
84378
|
+
// so we declare ONLY the custom integer attribute + the two id-half varyings
|
|
84379
|
+
// (float, interpolated — an integer varying would have to be `flat`, which is
|
|
84380
|
+
// the ANGLE-on-Metal memory trap described in highlight.ts; all vertices of a
|
|
84381
|
+
// primitive carry the same id, so interpolation + rounding is exact). Clip
|
|
84382
|
+
// chunks use legacy `varying`/`attribute` keywords
|
|
84335
84383
|
// which three remaps via `#define` under GLSL3; the vertex chunk reads a local
|
|
84336
84384
|
// `vec4 mvPosition`, which we compute before the include.
|
|
84337
84385
|
const vertexShader = /* glsl */ `
|
|
84338
84386
|
#include <clipping_planes_pars_vertex>
|
|
84339
84387
|
|
|
84340
84388
|
in uint componentId;
|
|
84341
|
-
|
|
84389
|
+
out float vIdLo;
|
|
84390
|
+
out float vIdHi;
|
|
84342
84391
|
out vec3 vWorldPos;
|
|
84343
84392
|
|
|
84344
84393
|
void main() {
|
|
84345
|
-
|
|
84394
|
+
vIdLo = float( componentId & 0xFFFFu );
|
|
84395
|
+
vIdHi = float( componentId >> 16 );
|
|
84346
84396
|
vec4 worldPos = modelMatrix * vec4( position, 1.0 );
|
|
84347
84397
|
vWorldPos = worldPos.xyz;
|
|
84348
84398
|
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
|
|
@@ -84381,13 +84431,15 @@ function createVertexPickMaterial(options = {}) {
|
|
|
84381
84431
|
#include <clipping_planes_pars_vertex>
|
|
84382
84432
|
|
|
84383
84433
|
in uint componentId;
|
|
84384
|
-
|
|
84434
|
+
out float vIdLo;
|
|
84435
|
+
out float vIdHi;
|
|
84385
84436
|
out vec3 vWorldPos;
|
|
84386
84437
|
uniform float uPickSize;
|
|
84387
84438
|
uniform float uDepthBias;
|
|
84388
84439
|
|
|
84389
84440
|
void main() {
|
|
84390
|
-
|
|
84441
|
+
vIdLo = float( componentId & 0xFFFFu );
|
|
84442
|
+
vIdHi = float( componentId >> 16 );
|
|
84391
84443
|
vec4 worldPos = modelMatrix * vec4( position, 1.0 );
|
|
84392
84444
|
vWorldPos = worldPos.xyz;
|
|
84393
84445
|
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
|
|
@@ -84447,7 +84499,8 @@ function createEdgePickMaterial(options = {}) {
|
|
|
84447
84499
|
in vec3 instanceEnd;
|
|
84448
84500
|
in uint componentId;
|
|
84449
84501
|
|
|
84450
|
-
|
|
84502
|
+
out float vIdLo;
|
|
84503
|
+
out float vIdHi;
|
|
84451
84504
|
out vec3 vWorldPos;
|
|
84452
84505
|
|
|
84453
84506
|
void trimSegment( const in vec4 start, inout vec4 end ) {
|
|
@@ -84459,7 +84512,8 @@ function createEdgePickMaterial(options = {}) {
|
|
|
84459
84512
|
}
|
|
84460
84513
|
|
|
84461
84514
|
void main() {
|
|
84462
|
-
|
|
84515
|
+
vIdLo = float( componentId & 0xFFFFu );
|
|
84516
|
+
vIdHi = float( componentId >> 16 );
|
|
84463
84517
|
|
|
84464
84518
|
// World-space segment endpoints → approximate hit point along the edge.
|
|
84465
84519
|
vWorldPos = ( position.y < 0.5 )
|
|
@@ -84997,20 +85051,41 @@ const U_HIGHLIGHT_HOVER_COLOR = "uHighlightHoverColor";
|
|
|
84997
85051
|
//
|
|
84998
85052
|
// three upgrades stock materials to GLSL ES 3.00 on WebGL2 via `#define attribute
|
|
84999
85053
|
// in` / `#define varying out|in` macros (WebGLProgram.js), so writing `attribute` /
|
|
85000
|
-
// `
|
|
85054
|
+
// `varying` here is converted automatically; `usampler2D` / `texelFetch` /
|
|
85001
85055
|
// integer attributes are then available.
|
|
85056
|
+
//
|
|
85057
|
+
// The component id crosses to the fragment stage as TWO ordinary (interpolated)
|
|
85058
|
+
// float varyings holding its 16-bit halves — NOT as a `flat uint`. A `flat`
|
|
85059
|
+
// varying is what an integer varying would require, and it is exactly what must
|
|
85060
|
+
// be avoided here: under WebKit's ANGLE-on-Metal backend, every draw whose
|
|
85061
|
+
// program has a `flat` varying makes ANGLE allocate a converted copy of the
|
|
85062
|
+
// draw's index data (>= 64 KB each, pooled, never returned to the GL on
|
|
85063
|
+
// deleteBuffer). With this varying on every visible material that was ~25x the
|
|
85064
|
+
// scene's vertex/index bytes in graphics memory (26 MB of buffers -> 812 MB;
|
|
85065
|
+
// 100 MB -> 2.3 GB) and it is charged to the page's WebContent process in the
|
|
85066
|
+
// system WebKit, which killed build123d Studio at 16 GB after a few large
|
|
85067
|
+
// shows. Without `flat` the same scene takes 63 MB / 160 MB. Measured 2026-09-17
|
|
85068
|
+
// (working-docs/leak-harness.html + probe2.html, Playwright WebKit and Safari).
|
|
85069
|
+
//
|
|
85070
|
+
// Interpolating floats is exact for the id's purpose: every vertex of a face,
|
|
85071
|
+
// every vertex of an instanced segment and a point carry the SAME id, so the
|
|
85072
|
+
// interpolated value equals it up to rounding, and `uint(x + 0.5)` recovers it.
|
|
85073
|
+
// Two 16-bit halves (both < 65536, exactly representable in float32) keep the
|
|
85074
|
+
// full 32-bit id range that `applyComponentIds` promises.
|
|
85002
85075
|
// ---------------------------------------------------------------------------
|
|
85003
85076
|
/**
|
|
85004
85077
|
* Shared state-fetch GLSL, injected into BOTH stages: the vertex stage needs it for
|
|
85005
85078
|
* widening / point size, the fragment for color. Declares the sampler + texWidth +
|
|
85006
|
-
* the
|
|
85007
|
-
* (0 for background / nothing).
|
|
85079
|
+
* the two id-half varyings and a helper returning the component's
|
|
85080
|
+
* {@link HighlightFlag} bits (0 for background / nothing).
|
|
85008
85081
|
*/
|
|
85009
85082
|
const HL_STATE_GLSL = `
|
|
85010
|
-
|
|
85083
|
+
varying float vHighlightIdLo;
|
|
85084
|
+
varying float vHighlightIdHi;
|
|
85011
85085
|
uniform highp usampler2D ${U_HIGHLIGHT_STATE};
|
|
85012
85086
|
uniform int ${U_HIGHLIGHT_TEX_WIDTH};
|
|
85013
85087
|
uint highlightState() {
|
|
85088
|
+
uint vHighlightId = (uint(vHighlightIdHi + 0.5) << 16) | uint(vHighlightIdLo + 0.5);
|
|
85014
85089
|
if (vHighlightId == 0u) return 0u;
|
|
85015
85090
|
ivec2 hlUv = ivec2(
|
|
85016
85091
|
int(vHighlightId) % ${U_HIGHLIGHT_TEX_WIDTH},
|
|
@@ -85025,8 +85100,8 @@ uint highlightState() {
|
|
|
85025
85100
|
const HL_VERTEX_HEADER = `
|
|
85026
85101
|
attribute uint ${COMPONENT_ID_ATTRIBUTE};
|
|
85027
85102
|
${HL_STATE_GLSL}`;
|
|
85028
|
-
/** Vertex main: forward the id. Injected right after `void main() {`. */
|
|
85029
|
-
const HL_VERTEX_ASSIGN = `
|
|
85103
|
+
/** Vertex main: forward the id as two 16-bit halves. Injected right after `void main() {`. */
|
|
85104
|
+
const HL_VERTEX_ASSIGN = `vHighlightIdLo = float(${COMPONENT_ID_ATTRIBUTE} & 0xFFFFu); vHighlightIdHi = float(${COMPONENT_ID_ATTRIBUTE} >> 16);`;
|
|
85030
85105
|
/** Fragment header: the shared state fetch + the two highlight colors. */
|
|
85031
85106
|
const HL_FRAGMENT_HEADER = `
|
|
85032
85107
|
${HL_STATE_GLSL}
|
|
@@ -85060,7 +85135,7 @@ function focusSizeExpr(stateVar, hover, selected, none) {
|
|
|
85060
85135
|
* renames an anchor must fail LOUDLY here, not silently drop the highlight (build
|
|
85061
85136
|
* would otherwise stay green). One guard test asserts each anchor still exists.
|
|
85062
85137
|
*/
|
|
85063
|
-
function replaceOrThrow(src, anchor, replacement, where) {
|
|
85138
|
+
function replaceOrThrow$1(src, anchor, replacement, where) {
|
|
85064
85139
|
if (!src.includes(anchor)) {
|
|
85065
85140
|
throw new Error(`HighlightController.${where}: shader anchor not found: ${JSON.stringify(anchor)}`);
|
|
85066
85141
|
}
|
|
@@ -85178,6 +85253,23 @@ class HighlightController {
|
|
|
85178
85253
|
}
|
|
85179
85254
|
this._applyHover(`s${solidPath}`, ids);
|
|
85180
85255
|
}
|
|
85256
|
+
/**
|
|
85257
|
+
* HOVER every FACE at or below the tree path `prefix` (a leaf or a whole group),
|
|
85258
|
+
* or clear hover when `null`. Drives the tree-row hover highlight.
|
|
85259
|
+
*/
|
|
85260
|
+
setHoverPath(prefix) {
|
|
85261
|
+
if (prefix == null) {
|
|
85262
|
+
this._applyHover("", []);
|
|
85263
|
+
return;
|
|
85264
|
+
}
|
|
85265
|
+
const sub = prefix + "/";
|
|
85266
|
+
const ids = [];
|
|
85267
|
+
for (const info of this.registry.entries()) {
|
|
85268
|
+
if (info.topo === "face" && info.path.startsWith(sub))
|
|
85269
|
+
ids.push(info.id);
|
|
85270
|
+
}
|
|
85271
|
+
this._applyHover(`p${prefix}`, ids);
|
|
85272
|
+
}
|
|
85181
85273
|
/** Set or clear the SELECTED flag for a single component id. */
|
|
85182
85274
|
setSelected(id, flag) {
|
|
85183
85275
|
this._setBit(id, HighlightFlag.SELECTED, flag);
|
|
@@ -85276,11 +85368,11 @@ class HighlightController {
|
|
|
85276
85368
|
this.uniforms.uHighlightSelectedColor;
|
|
85277
85369
|
shader.uniforms[U_HIGHLIGHT_HOVER_COLOR] =
|
|
85278
85370
|
this.uniforms.uHighlightHoverColor;
|
|
85279
|
-
// Common: forward the component id as
|
|
85371
|
+
// Common: forward the component id (as two interpolated float halves).
|
|
85280
85372
|
shader.vertexShader =
|
|
85281
85373
|
HL_VERTEX_HEADER +
|
|
85282
85374
|
"\n" +
|
|
85283
|
-
replaceOrThrow(shader.vertexShader, "void main() {", `void main() {\n ${HL_VERTEX_ASSIGN}`, where);
|
|
85375
|
+
replaceOrThrow$1(shader.vertexShader, "void main() {", `void main() {\n ${HL_VERTEX_ASSIGN}`, where);
|
|
85284
85376
|
shader.fragmentShader = HL_FRAGMENT_HEADER + "\n" + shader.fragmentShader;
|
|
85285
85377
|
// Topo-specific color / size injection.
|
|
85286
85378
|
customize(shader);
|
|
@@ -85295,7 +85387,7 @@ class HighlightController {
|
|
|
85295
85387
|
*/
|
|
85296
85388
|
patchFaceMaterial(material) {
|
|
85297
85389
|
this._install(material, "patchFaceMaterial", (shader) => {
|
|
85298
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchFaceMaterial");
|
|
85390
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchFaceMaterial");
|
|
85299
85391
|
});
|
|
85300
85392
|
}
|
|
85301
85393
|
/**
|
|
@@ -85307,9 +85399,9 @@ class HighlightController {
|
|
|
85307
85399
|
*/
|
|
85308
85400
|
patchEdgeMaterial(material) {
|
|
85309
85401
|
this._install(material, "patchEdgeMaterial", (shader) => {
|
|
85310
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85402
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85311
85403
|
offset *= ${focusSizeExpr("hlEdgeState", EDGE_FOCUS_WIDTH, EDGE_FOCUS_WIDTH - 2, "linewidth")};`, "patchEdgeMaterial");
|
|
85312
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchEdgeMaterial");
|
|
85404
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchEdgeMaterial");
|
|
85313
85405
|
});
|
|
85314
85406
|
}
|
|
85315
85407
|
/**
|
|
@@ -85324,12 +85416,12 @@ class HighlightController {
|
|
|
85324
85416
|
const cull = options.cullUnhighlighted === true;
|
|
85325
85417
|
const none = cull ? "0.0" : "size";
|
|
85326
85418
|
this._install(material, "patchVertexMaterial", (shader) => {
|
|
85327
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85419
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85328
85420
|
gl_PointSize = ${focusSizeExpr("hlPtState", VERTEX_FOCUS_SIZE, VERTEX_FOCUS_SIZE - 2, none)};`, "patchVertexMaterial");
|
|
85329
85421
|
const discard = cull
|
|
85330
85422
|
? "\n if (highlightState() == 0u) discard;"
|
|
85331
85423
|
: "";
|
|
85332
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${discard}${HL_COLOR_OVERRIDE}`, "patchVertexMaterial");
|
|
85424
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${discard}${HL_COLOR_OVERRIDE}`, "patchVertexMaterial");
|
|
85333
85425
|
},
|
|
85334
85426
|
// The cull discard is the variant that must NOT share a program with the
|
|
85335
85427
|
// standalone (always-visible) vertex cloud — the ghost-vertex bug.
|
|
@@ -88808,7 +88900,14 @@ class Grid extends Group {
|
|
|
88808
88900
|
group.name = `GridHelper-${i}`;
|
|
88809
88901
|
group.add(new GridHelper(this.size, 2 * this.ticks, this.colors[this.theme][i === 0 ? 1 : i === 1 ? 0 : 2], this.colors[this.theme][i === 0 ? 0 : i === 1 ? 2 : 1], this.theme == "dark" ? 0x7777777 : 0xbbbbbb));
|
|
88810
88902
|
let label;
|
|
88811
|
-
|
|
88903
|
+
// A grid of no size has no labels, and asking for them hangs the tab:
|
|
88904
|
+
// `niceBounds` answers [0, 0, 0] for a bounding box whose largest extent
|
|
88905
|
+
// is zero - which is what an empty model gives it - so `delta` is zero
|
|
88906
|
+
// and the loop below steps by zero, never advancing and never throwing.
|
|
88907
|
+
// Guarded here rather than in `niceBounds`, whose answer is right: there
|
|
88908
|
+
// is nothing to place ticks on.
|
|
88909
|
+
const step = this.delta / 2;
|
|
88910
|
+
for (let x = -this.size / 2; step > 0 && x <= this.size / 2; x += step) {
|
|
88812
88911
|
if (Math.abs(x) < 1e-6) {
|
|
88813
88912
|
continue;
|
|
88814
88913
|
} // skip center label
|
|
@@ -90115,8 +90214,9 @@ class TreeView {
|
|
|
90115
90214
|
* @param theme - The UI theme ('light' or 'dark').
|
|
90116
90215
|
* @param linkIcons - Whether icon 0 and 1 are linked.
|
|
90117
90216
|
* @param debug - Enable debug logging.
|
|
90217
|
+
* @param hoverHandler - Callback for label hover (node path, or null on leave).
|
|
90118
90218
|
*/
|
|
90119
|
-
constructor(tree, scrollContainer, objectHandler, pickHandler, updateHandler, notificationHandler, colorGetter, theme, linkIcons, debug = false) {
|
|
90219
|
+
constructor(tree, scrollContainer, objectHandler, pickHandler, updateHandler, notificationHandler, colorGetter, theme, linkIcons, debug = false, hoverHandler = null) {
|
|
90120
90220
|
/************************************************************************************
|
|
90121
90221
|
* Handlers
|
|
90122
90222
|
************************************************************************************/
|
|
@@ -90231,6 +90331,7 @@ class TreeView {
|
|
|
90231
90331
|
this.theme = theme;
|
|
90232
90332
|
this.linkIcons = linkIcons;
|
|
90233
90333
|
this.debug = debug;
|
|
90334
|
+
this.hoverHandler = hoverHandler;
|
|
90234
90335
|
this.model = null;
|
|
90235
90336
|
this.container = null;
|
|
90236
90337
|
this.lastLabel = null;
|
|
@@ -90428,6 +90529,12 @@ class TreeView {
|
|
|
90428
90529
|
e.stopPropagation();
|
|
90429
90530
|
this.handleLabelClick(node, e);
|
|
90430
90531
|
};
|
|
90532
|
+
label.onmouseenter = () => {
|
|
90533
|
+
this.hoverHandler?.(this.getNodePath(node));
|
|
90534
|
+
};
|
|
90535
|
+
label.onmouseleave = () => {
|
|
90536
|
+
this.hoverHandler?.(null);
|
|
90537
|
+
};
|
|
90431
90538
|
nodeContent.appendChild(label);
|
|
90432
90539
|
let childrenContainer = null;
|
|
90433
90540
|
if (node.children) {
|
|
@@ -90919,6 +91026,357 @@ let Timer$1 = class Timer {
|
|
|
90919
91026
|
}
|
|
90920
91027
|
};
|
|
90921
91028
|
|
|
91029
|
+
/**
|
|
91030
|
+
* Section hatching for clip caps (CAD mode, shader-only). The per-solid cap quads
|
|
91031
|
+
* draw hatch lines anchored to the plane (stable while rotating/panning) whose
|
|
91032
|
+
* spacing is kept at a constant pixel distance by a per-frame world-per-pixel
|
|
91033
|
+
* uniform. The outline of the caps is drawn separately (see cap-outline.ts); for its
|
|
91034
|
+
* id pass the same cap shader can output the cap's id instead of its color.
|
|
91035
|
+
*
|
|
91036
|
+
* All shared uniforms are plain `{ value }` objects held by reference, so a single
|
|
91037
|
+
* write (e.g. the per-frame hatch spacing) updates every patched material without a
|
|
91038
|
+
* recompile.
|
|
91039
|
+
*/
|
|
91040
|
+
/** Distance between hatch lines in pixels. */
|
|
91041
|
+
const HATCH_SPACING_PX = 8;
|
|
91042
|
+
/** Width of a hatch line in pixels. */
|
|
91043
|
+
const HATCH_LINE_WIDTH_PX = 1;
|
|
91044
|
+
/**
|
|
91045
|
+
* Hatch angle (radians) and spacing factor per solid, cycled by the solid's index
|
|
91046
|
+
* so that neighbouring solids are told apart (drawing convention: alternate the
|
|
91047
|
+
* hatch direction between adjacent parts).
|
|
91048
|
+
*/
|
|
91049
|
+
const HATCH_VARIANTS = [
|
|
91050
|
+
{ angle: Math.PI / 4, scale: 1.0 },
|
|
91051
|
+
{ angle: (3 * Math.PI) / 4, scale: 1.0 },
|
|
91052
|
+
{ angle: Math.PI / 4, scale: 1.6 },
|
|
91053
|
+
{ angle: (3 * Math.PI) / 4, scale: 1.6 },
|
|
91054
|
+
];
|
|
91055
|
+
function createHatchUniforms(capHalfSize) {
|
|
91056
|
+
return {
|
|
91057
|
+
uHatchSpacing: { value: 1 },
|
|
91058
|
+
uCapHalfSize: { value: capHalfSize },
|
|
91059
|
+
uHatchLineWidth: { value: HATCH_LINE_WIDTH_PX },
|
|
91060
|
+
uHatchOn: { value: 1 },
|
|
91061
|
+
uCapIdPass: { value: 0 },
|
|
91062
|
+
};
|
|
91063
|
+
}
|
|
91064
|
+
/**
|
|
91065
|
+
* World units covered by one screen pixel at the given point (exact for an
|
|
91066
|
+
* orthographic camera; at the point's distance for a perspective camera).
|
|
91067
|
+
*/
|
|
91068
|
+
function worldPerPixel(camera, point, height) {
|
|
91069
|
+
if (height <= 0)
|
|
91070
|
+
return 1;
|
|
91071
|
+
if (camera instanceof OrthographicCamera) {
|
|
91072
|
+
return (camera.top - camera.bottom) / camera.zoom / height;
|
|
91073
|
+
}
|
|
91074
|
+
if (camera instanceof PerspectiveCamera) {
|
|
91075
|
+
const dist = camera.position.distanceTo(point);
|
|
91076
|
+
const halfFov = MathUtils.degToRad(camera.fov) / 2;
|
|
91077
|
+
return (2 * dist * Math.tan(halfFov)) / camera.zoom / height;
|
|
91078
|
+
}
|
|
91079
|
+
return 1;
|
|
91080
|
+
}
|
|
91081
|
+
function replaceOrThrow(source, anchor, replacement, where) {
|
|
91082
|
+
if (!source.includes(anchor)) {
|
|
91083
|
+
throw new Error(`${where}: shader anchor "${anchor}" not found`);
|
|
91084
|
+
}
|
|
91085
|
+
return source.replace(anchor, replacement);
|
|
91086
|
+
}
|
|
91087
|
+
/**
|
|
91088
|
+
* Patch a cap quad material to draw section hatching. `index` is the solid's index
|
|
91089
|
+
* among the caps of its plane; it selects the hatch direction and spacing. `capId`
|
|
91090
|
+
* is the cap's unique id color for the outline id pass (see `capIdColor`).
|
|
91091
|
+
*/
|
|
91092
|
+
function patchHatchMaterial(material, uniforms, index, capId) {
|
|
91093
|
+
if (material.userData.hatchPatched === true)
|
|
91094
|
+
return;
|
|
91095
|
+
material.userData.hatchPatched = true;
|
|
91096
|
+
const variant = HATCH_VARIANTS[index % HATCH_VARIANTS.length];
|
|
91097
|
+
const uHatchAngle = { value: variant.angle };
|
|
91098
|
+
const uHatchScale = { value: variant.scale };
|
|
91099
|
+
const uCapId = { value: capId };
|
|
91100
|
+
const prev = material.onBeforeCompile;
|
|
91101
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
91102
|
+
prev.call(material, shader, renderer);
|
|
91103
|
+
shader.uniforms.uHatchSpacing = uniforms.uHatchSpacing;
|
|
91104
|
+
shader.uniforms.uCapHalfSize = uniforms.uCapHalfSize;
|
|
91105
|
+
shader.uniforms.uHatchLineWidth = uniforms.uHatchLineWidth;
|
|
91106
|
+
shader.uniforms.uHatchOn = uniforms.uHatchOn;
|
|
91107
|
+
shader.uniforms.uHatchAngle = uHatchAngle;
|
|
91108
|
+
shader.uniforms.uHatchScale = uHatchScale;
|
|
91109
|
+
shader.uniforms.uCapIdPass = uniforms.uCapIdPass;
|
|
91110
|
+
shader.uniforms.uCapId = uCapId;
|
|
91111
|
+
shader.vertexShader =
|
|
91112
|
+
"uniform float uCapHalfSize;\nvarying vec2 vTcvCapUV;\n" +
|
|
91113
|
+
replaceOrThrow(shader.vertexShader, "#include <begin_vertex>", "#include <begin_vertex>\n vTcvCapUV = position.xy * uCapHalfSize;", "patchHatchMaterial");
|
|
91114
|
+
shader.fragmentShader =
|
|
91115
|
+
"uniform float uHatchSpacing;\nuniform float uHatchLineWidth;\nuniform float uHatchOn;\n" +
|
|
91116
|
+
"uniform float uHatchAngle;\nuniform float uHatchScale;\nvarying vec2 vTcvCapUV;\n" +
|
|
91117
|
+
"uniform float uCapIdPass;\nuniform vec3 uCapId;\n" +
|
|
91118
|
+
shader.fragmentShader;
|
|
91119
|
+
// Hatch the unlit base color, before lighting: the ink (dark lines on light
|
|
91120
|
+
// parts, light lines on dark parts) is then fixed per part, and lines and cap
|
|
91121
|
+
// darken together when the cap turns away from the light.
|
|
91122
|
+
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>",
|
|
91123
|
+
/* glsl */ `#include <color_fragment>
|
|
91124
|
+
if ( uHatchOn > 0.5 ) {
|
|
91125
|
+
vec2 tcvAcross = vec2( - sin( uHatchAngle ), cos( uHatchAngle ) );
|
|
91126
|
+
float t = dot( vTcvCapUV, tcvAcross ) / max( uHatchSpacing * uHatchScale, 1e-9 );
|
|
91127
|
+
float tw = max( fwidth( t ), 1e-6 );
|
|
91128
|
+
float dpx = ( 0.5 - abs( fract( t ) - 0.5 ) ) / tw;
|
|
91129
|
+
float line = 1.0 - smoothstep( 0.5 * uHatchLineWidth - 0.5, 0.5 * uHatchLineWidth + 0.5, dpx );
|
|
91130
|
+
// Seen edge-on the lines crowd below a few pixels apart and would merge into
|
|
91131
|
+
// solid ink: fade them out between 4 and 2 px spacing (1 / tw = px per line).
|
|
91132
|
+
line *= smoothstep( 2.0, 4.0, 1.0 / tw );
|
|
91133
|
+
float lum = dot( diffuseColor.rgb, vec3( 0.2126, 0.7152, 0.0722 ) );
|
|
91134
|
+
vec3 ink = lum > 0.2 ? diffuseColor.rgb * 0.3 : mix( diffuseColor.rgb, vec3( 1.0 ), 0.6 );
|
|
91135
|
+
diffuseColor.rgb = mix( diffuseColor.rgb, ink, line );
|
|
91136
|
+
}`, "patchHatchMaterial");
|
|
91137
|
+
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <opaque_fragment>", `#include <opaque_fragment>
|
|
91138
|
+
if ( uCapIdPass > 0.5 ) gl_FragColor = vec4( uCapId, 1.0 );`, "patchHatchMaterial");
|
|
91139
|
+
};
|
|
91140
|
+
material.needsUpdate = true;
|
|
91141
|
+
}
|
|
91142
|
+
|
|
91143
|
+
/**
|
|
91144
|
+
* Screen-space outline of the clip caps (CAD mode).
|
|
91145
|
+
*
|
|
91146
|
+
* The caps carry no geometry of their own outline, and the faces next to a cut may
|
|
91147
|
+
* face away from the camera or belong to a touching solid, so the outline is found
|
|
91148
|
+
* in screen space instead:
|
|
91149
|
+
*
|
|
91150
|
+
* 1. Id pass into an offscreen target (color + depth + stencil):
|
|
91151
|
+
* - visible faces as black occluders (face pick layer + override material), so a
|
|
91152
|
+
* cap hidden behind another part gets no outline; face pixels within the line
|
|
91153
|
+
* width of a clip plane write {@link CUT_EDGE_ID} instead, which draws the cut
|
|
91154
|
+
* contour on the faces themselves (needed when the caps are behind the faces),
|
|
91155
|
+
* - the stencil meshes and caps (on {@link CAP_LAYER}) with each cap writing its
|
|
91156
|
+
* own id instead of its shaded color — the same stencil sequence as the main
|
|
91157
|
+
* render, since three orders them identically (material creation order).
|
|
91158
|
+
* 2. Composite over the canvas: a pixel is drawn in the edge color when its id is
|
|
91159
|
+
* larger than a neighbour's id, which yields a line on one side of every boundary
|
|
91160
|
+
* cap↔cap, cap↔surface and cap↔background. Cut-edge pixels are the largest id, so
|
|
91161
|
+
* where they meet a cap only they are drawn — one line, not two.
|
|
91162
|
+
*/
|
|
91163
|
+
/** Render layer of the clip stencil meshes and cap quads (pick layers use 1-3). */
|
|
91164
|
+
const CAP_LAYER = 4;
|
|
91165
|
+
/** Id of face pixels on the cut contour (larger than any cap id). */
|
|
91166
|
+
const CUT_EDGE_ID = 0xffffff;
|
|
91167
|
+
/** Encode a cap id (1..2^24-1) as an exact 8-bit RGB color (0 = no cap). */
|
|
91168
|
+
function capIdColor(id) {
|
|
91169
|
+
return new Vector3((id & 0xff) / 255, ((id >> 8) & 0xff) / 255, ((id >> 16) & 0xff) / 255);
|
|
91170
|
+
}
|
|
91171
|
+
// Occluder: a face fragment lies on the cut contour when it is within uCutWidth
|
|
91172
|
+
// pixels of a clip plane that bounds the visible part, mirroring three's
|
|
91173
|
+
// clipping_planes_fragment (kept fragments have d >= 0). Skipped where the face is
|
|
91174
|
+
// (nearly) parallel to the plane: there d barely changes across the pixel, and a
|
|
91175
|
+
// face lying in the plane would otherwise become one big edge.
|
|
91176
|
+
const CUT_EDGE_GLSL = /* glsl */ `
|
|
91177
|
+
bool tcvCut = false;
|
|
91178
|
+
#if NUM_CLIPPING_PLANES > 0
|
|
91179
|
+
{
|
|
91180
|
+
float tcvPosGrad = length( fwidth( vClipPosition ) );
|
|
91181
|
+
float tcvD[ NUM_CLIPPING_PLANES ];
|
|
91182
|
+
bool tcvNear[ NUM_CLIPPING_PLANES ];
|
|
91183
|
+
for ( int i = 0; i < NUM_CLIPPING_PLANES; i ++ ) {
|
|
91184
|
+
vec4 p = clippingPlanes[ i ];
|
|
91185
|
+
float d = p.w - dot( vClipPosition, p.xyz );
|
|
91186
|
+
// Step of d per pixel along the dominant screen axis — the same 1-pixel
|
|
91187
|
+
// convention as the composite's horizontal/vertical neighbour test.
|
|
91188
|
+
float g = max( abs( dFdx( d ) ), abs( dFdy( d ) ) );
|
|
91189
|
+
tcvD[ i ] = d;
|
|
91190
|
+
tcvNear[ i ] = g > 0.05 * tcvPosGrad && d >= 0.0 && d < uCutWidth * g;
|
|
91191
|
+
}
|
|
91192
|
+
for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {
|
|
91193
|
+
tcvCut = tcvCut || tcvNear[ i ];
|
|
91194
|
+
}
|
|
91195
|
+
#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES
|
|
91196
|
+
// Intersection planes: plane i bounds the visible part only where every
|
|
91197
|
+
// other intersection plane clips.
|
|
91198
|
+
for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {
|
|
91199
|
+
bool othersClip = true;
|
|
91200
|
+
for ( int j = UNION_CLIPPING_PLANES; j < NUM_CLIPPING_PLANES; j ++ ) {
|
|
91201
|
+
if ( j != i && tcvD[ j ] >= 0.0 ) othersClip = false;
|
|
91202
|
+
}
|
|
91203
|
+
if ( othersClip && tcvNear[ i ] ) tcvCut = true;
|
|
91204
|
+
}
|
|
91205
|
+
#endif
|
|
91206
|
+
}
|
|
91207
|
+
#endif
|
|
91208
|
+
`;
|
|
91209
|
+
const COMPOSITE_VERTEX = /* glsl */ `
|
|
91210
|
+
varying vec2 vUv;
|
|
91211
|
+
void main() {
|
|
91212
|
+
vUv = uv;
|
|
91213
|
+
gl_Position = vec4( position.xy, 0.0, 1.0 );
|
|
91214
|
+
}
|
|
91215
|
+
`;
|
|
91216
|
+
const COMPOSITE_FRAGMENT = /* glsl */ `
|
|
91217
|
+
uniform sampler2D tId;
|
|
91218
|
+
uniform vec2 uTexel;
|
|
91219
|
+
uniform float uWidth;
|
|
91220
|
+
uniform vec3 uColor;
|
|
91221
|
+
varying vec2 vUv;
|
|
91222
|
+
|
|
91223
|
+
float idAt( vec2 uv ) {
|
|
91224
|
+
vec3 c = floor( texture2D( tId, uv ).rgb * 255.0 + 0.5 );
|
|
91225
|
+
return c.r + 256.0 * c.g + 65536.0 * c.b;
|
|
91226
|
+
}
|
|
91227
|
+
|
|
91228
|
+
void main() {
|
|
91229
|
+
float c = idAt( vUv );
|
|
91230
|
+
bool edge = c == ${CUT_EDGE_ID.toFixed(1)};
|
|
91231
|
+
for ( int k = 1; k <= 4; k ++ ) {
|
|
91232
|
+
if ( float( k ) > uWidth ) break;
|
|
91233
|
+
vec2 o = uTexel * float( k );
|
|
91234
|
+
edge = edge
|
|
91235
|
+
|| c > idAt( vUv + vec2( o.x, 0.0 ) ) || c > idAt( vUv - vec2( o.x, 0.0 ) )
|
|
91236
|
+
|| c > idAt( vUv + vec2( 0.0, o.y ) ) || c > idAt( vUv - vec2( 0.0, o.y ) );
|
|
91237
|
+
}
|
|
91238
|
+
if ( ! edge ) discard;
|
|
91239
|
+
gl_FragColor = vec4( uColor, 1.0 );
|
|
91240
|
+
#include <colorspace_fragment>
|
|
91241
|
+
}
|
|
91242
|
+
`;
|
|
91243
|
+
class CapOutlinePass {
|
|
91244
|
+
constructor() {
|
|
91245
|
+
this.target = null;
|
|
91246
|
+
/** Width of the cut contour on the faces, in device pixels (= outline width). */
|
|
91247
|
+
this.cutWidth = { value: 1 };
|
|
91248
|
+
this.size = new Vector2();
|
|
91249
|
+
this.savedClearColor = new Color();
|
|
91250
|
+
this.savedViewport = new Vector4();
|
|
91251
|
+
// One depth unit behind the caps (polygonOffset 1/1), so a face coplanar with a
|
|
91252
|
+
// cap does not hide it; same slope factor, so steep faces in front still occlude.
|
|
91253
|
+
this.occluder = new MeshBasicMaterial({
|
|
91254
|
+
color: 0x000000,
|
|
91255
|
+
side: DoubleSide,
|
|
91256
|
+
polygonOffset: true,
|
|
91257
|
+
polygonOffsetFactor: 1,
|
|
91258
|
+
polygonOffsetUnits: 2,
|
|
91259
|
+
});
|
|
91260
|
+
this.occluder.onBeforeCompile = (shader) => {
|
|
91261
|
+
const clipAnchor = "#include <clipping_planes_fragment>";
|
|
91262
|
+
const opaqueAnchor = "#include <opaque_fragment>";
|
|
91263
|
+
for (const anchor of [clipAnchor, opaqueAnchor]) {
|
|
91264
|
+
if (!shader.fragmentShader.includes(anchor)) {
|
|
91265
|
+
throw new Error(`CapOutlinePass: shader anchor "${anchor}" not found`);
|
|
91266
|
+
}
|
|
91267
|
+
}
|
|
91268
|
+
shader.uniforms.uCutWidth = this.cutWidth;
|
|
91269
|
+
shader.fragmentShader =
|
|
91270
|
+
"uniform float uCutWidth;\n" +
|
|
91271
|
+
shader.fragmentShader
|
|
91272
|
+
.replace(clipAnchor, `${clipAnchor}\n${CUT_EDGE_GLSL}`)
|
|
91273
|
+
.replace(opaqueAnchor, `${opaqueAnchor}\n if ( tcvCut ) gl_FragColor = vec4( 1.0 );`);
|
|
91274
|
+
};
|
|
91275
|
+
this.composite = new ShaderMaterial({
|
|
91276
|
+
uniforms: {
|
|
91277
|
+
tId: { value: null },
|
|
91278
|
+
uTexel: { value: new Vector2(1, 1) },
|
|
91279
|
+
uWidth: { value: 1 },
|
|
91280
|
+
uColor: { value: new Color() },
|
|
91281
|
+
},
|
|
91282
|
+
vertexShader: COMPOSITE_VERTEX,
|
|
91283
|
+
fragmentShader: COMPOSITE_FRAGMENT,
|
|
91284
|
+
depthTest: false,
|
|
91285
|
+
depthWrite: false,
|
|
91286
|
+
});
|
|
91287
|
+
this.quad = new Mesh(new PlaneGeometry(2, 2), this.composite);
|
|
91288
|
+
this.quad.frustumCulled = false;
|
|
91289
|
+
this.quadScene = new Scene();
|
|
91290
|
+
this.quadScene.add(this.quad);
|
|
91291
|
+
this.quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
91292
|
+
}
|
|
91293
|
+
ensureTarget(width, height) {
|
|
91294
|
+
if (this.target === null) {
|
|
91295
|
+
this.target = new WebGLRenderTarget(width, height, {
|
|
91296
|
+
minFilter: NearestFilter,
|
|
91297
|
+
magFilter: NearestFilter,
|
|
91298
|
+
generateMipmaps: false,
|
|
91299
|
+
depthBuffer: true,
|
|
91300
|
+
stencilBuffer: true,
|
|
91301
|
+
});
|
|
91302
|
+
}
|
|
91303
|
+
else if (this.target.width !== width || this.target.height !== height) {
|
|
91304
|
+
this.target.setSize(width, height);
|
|
91305
|
+
}
|
|
91306
|
+
return this.target;
|
|
91307
|
+
}
|
|
91308
|
+
/**
|
|
91309
|
+
* Render the id pass and draw the outline over the current (canvas) framebuffer.
|
|
91310
|
+
* Call right after the main scene render.
|
|
91311
|
+
*/
|
|
91312
|
+
render(renderer, scene, camera, options) {
|
|
91313
|
+
renderer.getDrawingBufferSize(this.size);
|
|
91314
|
+
const width = Math.floor(this.size.x);
|
|
91315
|
+
const height = Math.floor(this.size.y);
|
|
91316
|
+
if (width < 1 || height < 1)
|
|
91317
|
+
return;
|
|
91318
|
+
const target = this.ensureTarget(width, height);
|
|
91319
|
+
// Line width in device pixels, shared by the cut contour and the cap outline.
|
|
91320
|
+
const lineWidth = Math.max(1, Math.round(renderer.getPixelRatio()));
|
|
91321
|
+
this.cutWidth.value = lineWidth;
|
|
91322
|
+
const savedMask = camera.layers.mask;
|
|
91323
|
+
const savedOverride = scene.overrideMaterial;
|
|
91324
|
+
const savedBackground = scene.background;
|
|
91325
|
+
const savedTarget = renderer.getRenderTarget();
|
|
91326
|
+
const savedAutoClear = renderer.autoClear;
|
|
91327
|
+
const savedAlpha = renderer.getClearAlpha();
|
|
91328
|
+
renderer.getClearColor(this.savedClearColor);
|
|
91329
|
+
renderer.getViewport(this.savedViewport);
|
|
91330
|
+
try {
|
|
91331
|
+
renderer.autoClear = false;
|
|
91332
|
+
scene.background = null;
|
|
91333
|
+
renderer.setRenderTarget(target);
|
|
91334
|
+
renderer.setClearColor(0x000000, 0); // id 0 = no cap
|
|
91335
|
+
renderer.clear(true, true, true);
|
|
91336
|
+
if (options.occlude) {
|
|
91337
|
+
this.occluder.clippingPlanes = options.planes;
|
|
91338
|
+
this.occluder.clipIntersection = options.intersection;
|
|
91339
|
+
camera.layers.set(PICK_LAYER.FACE);
|
|
91340
|
+
scene.overrideMaterial = this.occluder;
|
|
91341
|
+
renderer.render(scene, camera);
|
|
91342
|
+
scene.overrideMaterial = savedOverride;
|
|
91343
|
+
}
|
|
91344
|
+
options.setCapIdPass(true);
|
|
91345
|
+
try {
|
|
91346
|
+
camera.layers.set(CAP_LAYER);
|
|
91347
|
+
renderer.render(scene, camera);
|
|
91348
|
+
}
|
|
91349
|
+
finally {
|
|
91350
|
+
options.setCapIdPass(false);
|
|
91351
|
+
}
|
|
91352
|
+
renderer.setRenderTarget(savedTarget);
|
|
91353
|
+
renderer.setViewport(this.savedViewport);
|
|
91354
|
+
const u = this.composite.uniforms;
|
|
91355
|
+
u.tId.value = target.texture;
|
|
91356
|
+
u.uTexel.value.set(1 / width, 1 / height);
|
|
91357
|
+
u.uWidth.value = lineWidth;
|
|
91358
|
+
u.uColor.value.set(options.color);
|
|
91359
|
+
renderer.render(this.quadScene, this.quadCamera);
|
|
91360
|
+
}
|
|
91361
|
+
finally {
|
|
91362
|
+
camera.layers.mask = savedMask;
|
|
91363
|
+
scene.overrideMaterial = savedOverride;
|
|
91364
|
+
scene.background = savedBackground;
|
|
91365
|
+
renderer.setRenderTarget(savedTarget);
|
|
91366
|
+
renderer.setClearColor(this.savedClearColor, savedAlpha);
|
|
91367
|
+
renderer.setViewport(this.savedViewport);
|
|
91368
|
+
renderer.autoClear = savedAutoClear;
|
|
91369
|
+
}
|
|
91370
|
+
}
|
|
91371
|
+
dispose() {
|
|
91372
|
+
this.target?.dispose();
|
|
91373
|
+
this.target = null;
|
|
91374
|
+
this.occluder.dispose();
|
|
91375
|
+
this.composite.dispose();
|
|
91376
|
+
this.quad.geometry.dispose();
|
|
91377
|
+
}
|
|
91378
|
+
}
|
|
91379
|
+
|
|
90922
91380
|
// ============================================================================
|
|
90923
91381
|
// Constants
|
|
90924
91382
|
// ============================================================================
|
|
@@ -91171,6 +91629,9 @@ class Clipping extends Group {
|
|
|
91171
91629
|
super();
|
|
91172
91630
|
/** Per-solid stencil/cap units, the unit of screen-size culling. */
|
|
91173
91631
|
this._capUnits = [];
|
|
91632
|
+
/** Shared uniforms of the section hatching on all cap quads. */
|
|
91633
|
+
this._hatchUniforms = createHatchUniforms(1);
|
|
91634
|
+
this._hatchCenter = new Vector3();
|
|
91174
91635
|
/**
|
|
91175
91636
|
* Whether {@link cull} last ran with clipping active. Lets the inactive path
|
|
91176
91637
|
* gate stencils/caps off exactly once, then early-return on later still frames.
|
|
@@ -91234,6 +91695,10 @@ class Clipping extends Group {
|
|
|
91234
91695
|
mesh.material.visible = flag;
|
|
91235
91696
|
}
|
|
91236
91697
|
};
|
|
91698
|
+
/** Switch all cap materials between shaded output and outline id output. */
|
|
91699
|
+
this.setCapIdPass = (flag) => {
|
|
91700
|
+
this._hatchUniforms.uCapIdPass.value = flag ? 1 : 0;
|
|
91701
|
+
};
|
|
91237
91702
|
this.center = center;
|
|
91238
91703
|
this.distance = size / 2;
|
|
91239
91704
|
this.onNormalChange = options.onNormalChange || null;
|
|
@@ -91296,11 +91761,16 @@ class Clipping extends Group {
|
|
|
91296
91761
|
_createStencils(center, size, theme) {
|
|
91297
91762
|
this._planeMeshGroup = new PlaneMeshGroup();
|
|
91298
91763
|
this._planeMeshGroup.name = "PlaneMeshes";
|
|
91764
|
+
// Cap quads are PlaneGeometry(2, 2) scaled by size/2 (PlaneMesh.updateMatrixWorld).
|
|
91765
|
+
this._hatchUniforms.uCapHalfSize.value = 0.5 * size;
|
|
91766
|
+
this._hatchCenter.set(center[0], center[1], center[2]);
|
|
91299
91767
|
// Group the per-(solid,plane) units by solid for screen-size culling. The
|
|
91300
91768
|
// loop below is plane-major (and `objectColors`/`_planeMeshGroup` order must
|
|
91301
91769
|
// stay plane-major for setObjectColorCaps), so accumulate into a Map keyed by
|
|
91302
91770
|
// solid and flatten afterwards — without touching the plane-major structures.
|
|
91303
91771
|
const unitsBySolid = new Map();
|
|
91772
|
+
// Unique id per cap quad (plane × solid) for the cap outline id pass; 0 = none.
|
|
91773
|
+
let capId = 0;
|
|
91304
91774
|
for (let i = 0; i < 3; i++) {
|
|
91305
91775
|
const plane = this.clipPlanes[i];
|
|
91306
91776
|
const otherPlanes = this.clipPlanes.filter((_, j) => j !== i);
|
|
@@ -91320,9 +91790,16 @@ class Clipping extends Group {
|
|
|
91320
91790
|
clippingGroup.add(createStencil(`frontStencil-${i}-${j}`, ClippingMaterials.createFrontStencilMaterial(), group.shapeGeometry, plane));
|
|
91321
91791
|
clippingGroup.add(createStencil(`backStencil-${i}-${j}`, ClippingMaterials.createBackStencilMaterial(), group.shapeGeometry, plane));
|
|
91322
91792
|
group.addClipping(clippingGroup, i);
|
|
91793
|
+
// The outline id pass renders the stencil sequence + caps on CAP_LAYER.
|
|
91794
|
+
for (const stencil of clippingGroup.children) {
|
|
91795
|
+
stencil.layers.enable(CAP_LAYER);
|
|
91796
|
+
}
|
|
91323
91797
|
// Create stencil plane mesh
|
|
91324
91798
|
const planeMaterial = ClippingMaterials.createStencilPlaneMaterial(PLANE_COLORS[theme][i], otherPlanes);
|
|
91799
|
+
capId++;
|
|
91800
|
+
patchHatchMaterial(planeMaterial, this._hatchUniforms, j, capIdColor(capId));
|
|
91325
91801
|
const capMesh = new PlaneMesh(i, plane, center, size, planeMaterial, PLANE_COLORS[theme][i], `StencilPlane-${i}-${j}`);
|
|
91802
|
+
capMesh.layers.enable(CAP_LAYER);
|
|
91326
91803
|
this._planeMeshGroup.add(capMesh);
|
|
91327
91804
|
// Record the cull unit for this solid (one entry per solid, holding
|
|
91328
91805
|
// its up-to-3 per-plane stencil groups + cap quads).
|
|
@@ -91387,6 +91864,10 @@ class Clipping extends Group {
|
|
|
91387
91864
|
this.clipPlanes[index].setConstant(value);
|
|
91388
91865
|
this.reverseClipPlanes[index].setConstant(-value);
|
|
91389
91866
|
}
|
|
91867
|
+
/** Whether any solid has clip caps (the cap outline pass has work to do). */
|
|
91868
|
+
get hasCaps() {
|
|
91869
|
+
return this._capUnits.length > 0;
|
|
91870
|
+
}
|
|
91390
91871
|
/**
|
|
91391
91872
|
* Bound the per-frame stencil/cap draw work to keep large assemblies from
|
|
91392
91873
|
* overrunning the GPU watchdog on clip+rotate (see {@link CAP_CULL_MIN_PX}).
|
|
@@ -91421,6 +91902,9 @@ class Clipping extends Group {
|
|
|
91421
91902
|
return;
|
|
91422
91903
|
}
|
|
91423
91904
|
this._cullActive = true;
|
|
91905
|
+
// Keep the hatch spacing constant on screen (planes pass near the model center).
|
|
91906
|
+
this._hatchUniforms.uHatchSpacing.value =
|
|
91907
|
+
HATCH_SPACING_PX * worldPerPixel(camera, this._hatchCenter, height);
|
|
91424
91908
|
// Camera right vector in world space = column 0 of the camera world matrix.
|
|
91425
91909
|
camera.updateMatrixWorld();
|
|
91426
91910
|
const e = camera.matrixWorld.elements;
|
|
@@ -97350,7 +97834,7 @@ class Tools {
|
|
|
97350
97834
|
}
|
|
97351
97835
|
}
|
|
97352
97836
|
|
|
97353
|
-
const version = "5.0
|
|
97837
|
+
const version = "5.1.0";
|
|
97354
97838
|
|
|
97355
97839
|
/**
|
|
97356
97840
|
* `PickedComponent` over a GPU id-pick result. Drives the shader
|
|
@@ -97576,6 +98060,58 @@ class PickingController {
|
|
|
97576
98060
|
this.setSelectionInput(false);
|
|
97577
98061
|
this.setPickHandler(false);
|
|
97578
98062
|
}
|
|
98063
|
+
/**
|
|
98064
|
+
* Pick the visible component under the cursor with the current topo filter, or
|
|
98065
|
+
* `null` when the cursor is off the canvas or over background.
|
|
98066
|
+
*/
|
|
98067
|
+
pickUnderCursor() {
|
|
98068
|
+
if (this.host.idPicker === null || !this.idHoverInside)
|
|
98069
|
+
return null;
|
|
98070
|
+
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
98071
|
+
const x = this.idHoverClientX - rect.left;
|
|
98072
|
+
const y = this.idHoverClientY - rect.top;
|
|
98073
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height)
|
|
98074
|
+
return null;
|
|
98075
|
+
const topoFilter = pickerTopoFilter(this.host.display.shapeFilterDropDownMenu.currentFilter);
|
|
98076
|
+
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
98077
|
+
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
98078
|
+
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
98079
|
+
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
98080
|
+
if (hit === null || !this.pickVisible(hit.info))
|
|
98081
|
+
return null;
|
|
98082
|
+
return hit;
|
|
98083
|
+
}
|
|
98084
|
+
/**
|
|
98085
|
+
* World-space point on the visible component under the cursor (same pick as the
|
|
98086
|
+
* hover), or `null` when there is none or the GPU lacks the position attachment.
|
|
98087
|
+
*/
|
|
98088
|
+
pointUnderCursor() {
|
|
98089
|
+
if (!this.host.ready)
|
|
98090
|
+
return null;
|
|
98091
|
+
const hit = this.pickUnderCursor();
|
|
98092
|
+
if (hit === null || hit.point === null)
|
|
98093
|
+
return null;
|
|
98094
|
+
return hit.point.clone();
|
|
98095
|
+
}
|
|
98096
|
+
/**
|
|
98097
|
+
* Tree-row hover: HOVER the faces of the tree node at `path` (leaf or group), or
|
|
98098
|
+
* clear when `null`. Drops the canvas hover target first — otherwise the next
|
|
98099
|
+
* render's {@link handleIdHover} (cursor outside the canvas) would release that
|
|
98100
|
+
* stale target and wipe the tree hover with it.
|
|
98101
|
+
*/
|
|
98102
|
+
setTreeHover(path) {
|
|
98103
|
+
if (!this.host.ready || this.host.studioActive)
|
|
98104
|
+
return;
|
|
98105
|
+
const highlight = this.host.rendered.nestedGroup?.highlight ?? null;
|
|
98106
|
+
if (highlight === null)
|
|
98107
|
+
return;
|
|
98108
|
+
this.releaseLastSelected();
|
|
98109
|
+
this.lastObject = null;
|
|
98110
|
+
this.host.display.setStatusLine("");
|
|
98111
|
+
highlight.setHoverPath(path);
|
|
98112
|
+
if (!this.host.hasAnimationLoop)
|
|
98113
|
+
this.host.update(true, false);
|
|
98114
|
+
}
|
|
97579
98115
|
/**
|
|
97580
98116
|
* Whether hover preselection (highlight + status line) is active. Disabled for GDS:
|
|
97581
98117
|
* dense, stacked, instance-unrolled layout data where per-pixel hover flickers
|
|
@@ -97613,28 +98149,12 @@ class PickingController {
|
|
|
97613
98149
|
this.lastObject = null;
|
|
97614
98150
|
this.host.display.setStatusLine("");
|
|
97615
98151
|
};
|
|
97616
|
-
|
|
97617
|
-
|
|
97618
|
-
return;
|
|
97619
|
-
}
|
|
97620
|
-
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
97621
|
-
const x = this.idHoverClientX - rect.left;
|
|
97622
|
-
const y = this.idHoverClientY - rect.top;
|
|
97623
|
-
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
97624
|
-
release();
|
|
97625
|
-
return;
|
|
97626
|
-
}
|
|
97627
|
-
const filter = this.host.display.shapeFilterDropDownMenu.currentFilter;
|
|
97628
|
-
const fromSolid = filter.includes(TopoFilter.solid);
|
|
97629
|
-
const topoFilter = pickerTopoFilter(filter);
|
|
97630
|
-
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
97631
|
-
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
97632
|
-
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
97633
|
-
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
97634
|
-
if (hit === null || !this.pickVisible(hit.info)) {
|
|
98152
|
+
const hit = this.pickUnderCursor();
|
|
98153
|
+
if (hit === null) {
|
|
97635
98154
|
release();
|
|
97636
98155
|
return;
|
|
97637
98156
|
}
|
|
98157
|
+
const fromSolid = this.host.display.shapeFilterDropDownMenu.currentFilter.includes(TopoFilter.solid);
|
|
97638
98158
|
const picked = new IdPicked(hit.info, fromSolid, hit.point, highlight);
|
|
97639
98159
|
if (!picked.equals(this.lastObject)) {
|
|
97640
98160
|
this.releaseLastSelected();
|
|
@@ -108815,6 +109335,10 @@ function decodeInstancedFormat(data) {
|
|
|
108815
109335
|
// =============================================================================
|
|
108816
109336
|
// IMPORTS
|
|
108817
109337
|
// =============================================================================
|
|
109338
|
+
/** Decimals of a point copied with Ctrl/Cmd-C ({@link Viewer.copyPointUnderCursor}). */
|
|
109339
|
+
const COPY_POINT_DECIMALS = 4;
|
|
109340
|
+
/** Delay after the last camera change before the clip cap outline is drawn again. */
|
|
109341
|
+
const CAP_OUTLINE_SETTLE_MS = 150;
|
|
108818
109342
|
/**
|
|
108819
109343
|
* Type guard to check if an Object3D is an IndexedMesh.
|
|
108820
109344
|
*/
|
|
@@ -108925,6 +109449,14 @@ class Viewer {
|
|
|
108925
109449
|
// GPU id-based picker. Created/attached per render() once the scene + compact
|
|
108926
109450
|
// registry exist; drives hover, selection, measure and double-click pick.
|
|
108927
109451
|
this.idPicker = null;
|
|
109452
|
+
/** Outline of the clip caps; created on first use, kept across renders. */
|
|
109453
|
+
this._capOutline = null;
|
|
109454
|
+
/** Camera world + projection matrix of the previous outline frame (motion test). */
|
|
109455
|
+
this._capOutlineCamera = [];
|
|
109456
|
+
/** Pending redraw that adds the outline once the camera has settled. */
|
|
109457
|
+
this._capOutlineSettle = null;
|
|
109458
|
+
/** Whether the attached animation has moved parts (Play pressed or time slider moved). */
|
|
109459
|
+
this._animationStarted = false;
|
|
108928
109460
|
// ---------------------------------------------------------------------------
|
|
108929
109461
|
// Render Loop & Scene Updates
|
|
108930
109462
|
// ---------------------------------------------------------------------------
|
|
@@ -109073,6 +109605,7 @@ class Viewer {
|
|
|
109073
109605
|
}
|
|
109074
109606
|
else {
|
|
109075
109607
|
this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
|
|
109608
|
+
this._renderCapOutline();
|
|
109076
109609
|
}
|
|
109077
109610
|
this.cadTools.update();
|
|
109078
109611
|
this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
|
|
@@ -109326,6 +109859,7 @@ class Viewer {
|
|
|
109326
109859
|
return;
|
|
109327
109860
|
switch (btn) {
|
|
109328
109861
|
case "play":
|
|
109862
|
+
this._animationStarted = true;
|
|
109329
109863
|
if (this.clipAction.paused) {
|
|
109330
109864
|
this.clipAction.paused = false;
|
|
109331
109865
|
}
|
|
@@ -109459,6 +109993,10 @@ class Viewer {
|
|
|
109459
109993
|
return;
|
|
109460
109994
|
}
|
|
109461
109995
|
};
|
|
109996
|
+
/** Tree-row hover: highlight the faces of the node at `path`, or clear on `null`. */
|
|
109997
|
+
this.handleTreeHover = (path) => {
|
|
109998
|
+
this.pickingController.setTreeHover(path);
|
|
109999
|
+
};
|
|
109462
110000
|
/** Clear the current selection (and reset the select tool). */
|
|
109463
110001
|
this.clearSelection = () => {
|
|
109464
110002
|
this.pickingController.clearSelection();
|
|
@@ -110647,6 +111185,7 @@ class Viewer {
|
|
|
110647
111185
|
this.toggleAnimationLoop(true);
|
|
110648
111186
|
}
|
|
110649
111187
|
this.state.set("animationMode", label === "E" ? "explode" : "animation");
|
|
111188
|
+
this._animationStarted = false;
|
|
110650
111189
|
this.clipAction = this.animation.animate(this.rendered.nestedGroup.rootGroup, duration, speed, repeat);
|
|
110651
111190
|
// Reset animation slider to start
|
|
110652
111191
|
this.state.set("animationSliderValue", 0);
|
|
@@ -110665,6 +111204,7 @@ class Viewer {
|
|
|
110665
111204
|
deepDispose(this.animation);
|
|
110666
111205
|
}
|
|
110667
111206
|
this.state.set("animationMode", "none");
|
|
111207
|
+
this._animationStarted = false;
|
|
110668
111208
|
this.toggleAnimationLoop(false);
|
|
110669
111209
|
}
|
|
110670
111210
|
/**
|
|
@@ -110673,6 +111213,8 @@ class Viewer {
|
|
|
110673
111213
|
* @param fraction - relative time between 0 and 1.
|
|
110674
111214
|
*/
|
|
110675
111215
|
setRelativeTime(fraction) {
|
|
111216
|
+
if (fraction !== 0)
|
|
111217
|
+
this._animationStarted = true;
|
|
110676
111218
|
this.animation.setRelativeTime(fraction);
|
|
110677
111219
|
this.state.set("animationSliderValue", fraction * 1000);
|
|
110678
111220
|
}
|
|
@@ -110704,6 +111246,60 @@ class Viewer {
|
|
|
110704
111246
|
setTimeout(() => this.update(true, true), 50);
|
|
110705
111247
|
}
|
|
110706
111248
|
}
|
|
111249
|
+
/**
|
|
111250
|
+
* Draw the outline of the clip caps over the frame (Clip tab active only). Skipped
|
|
111251
|
+
* while the camera moves (drag, wheel, damping, preset views): the outline pass
|
|
111252
|
+
* renders faces and caps a second time, so it is drawn once the camera has been
|
|
111253
|
+
* still for {@link CAP_OUTLINE_SETTLE_MS}.
|
|
111254
|
+
*/
|
|
111255
|
+
_renderCapOutline() {
|
|
111256
|
+
const clipping = this.rendered.clipping;
|
|
111257
|
+
if (!this.renderer.localClippingEnabled || !clipping.hasCaps)
|
|
111258
|
+
return;
|
|
111259
|
+
const camera = this.rendered.camera.getCamera();
|
|
111260
|
+
if (this._cameraMovedSinceLastOutline(camera)) {
|
|
111261
|
+
this._scheduleCapOutline();
|
|
111262
|
+
return;
|
|
111263
|
+
}
|
|
111264
|
+
if (this._capOutline === null)
|
|
111265
|
+
this._capOutline = new CapOutlinePass();
|
|
111266
|
+
const blackEdges = this.state.get("blackEdges") === true;
|
|
111267
|
+
this._capOutline.render(this.renderer, this.rendered.scene, camera, {
|
|
111268
|
+
planes: clipping.clipPlanes,
|
|
111269
|
+
intersection: this.state.get("clipIntersection") === true,
|
|
111270
|
+
occlude: this.state.get("transparent") !== true,
|
|
111271
|
+
color: blackEdges ? 0x000000 : this.state.get("edgeColor"),
|
|
111272
|
+
setCapIdPass: clipping.setCapIdPass,
|
|
111273
|
+
});
|
|
111274
|
+
}
|
|
111275
|
+
/** Whether the camera's world or projection matrix changed since the last call. */
|
|
111276
|
+
_cameraMovedSinceLastOutline(camera) {
|
|
111277
|
+
const now = [
|
|
111278
|
+
...camera.matrixWorld.elements,
|
|
111279
|
+
...camera.projectionMatrix.elements,
|
|
111280
|
+
];
|
|
111281
|
+
const last = this._capOutlineCamera;
|
|
111282
|
+
const moved = last.length !== now.length || now.some((value, i) => value !== last[i]);
|
|
111283
|
+
this._capOutlineCamera = now;
|
|
111284
|
+
return moved;
|
|
111285
|
+
}
|
|
111286
|
+
/** Redraw once the camera has settled, so the skipped outline appears. */
|
|
111287
|
+
_scheduleCapOutline() {
|
|
111288
|
+
if (this._capOutlineSettle !== null)
|
|
111289
|
+
clearTimeout(this._capOutlineSettle);
|
|
111290
|
+
this._capOutlineSettle = setTimeout(() => {
|
|
111291
|
+
this._capOutlineSettle = null;
|
|
111292
|
+
if (this.ready)
|
|
111293
|
+
this.update(true, false);
|
|
111294
|
+
}, CAP_OUTLINE_SETTLE_MS);
|
|
111295
|
+
}
|
|
111296
|
+
/** Cancel a pending outline redraw. */
|
|
111297
|
+
_cancelCapOutline() {
|
|
111298
|
+
if (this._capOutlineSettle !== null)
|
|
111299
|
+
clearTimeout(this._capOutlineSettle);
|
|
111300
|
+
this._capOutlineSettle = null;
|
|
111301
|
+
this._capOutlineCamera = [];
|
|
111302
|
+
}
|
|
110707
111303
|
// ---------------------------------------------------------------------------
|
|
110708
111304
|
// Cleanup & Disposal
|
|
110709
111305
|
// ---------------------------------------------------------------------------
|
|
@@ -110722,6 +111318,8 @@ class Viewer {
|
|
|
110722
111318
|
*/
|
|
110723
111319
|
dispose() {
|
|
110724
111320
|
this.clear();
|
|
111321
|
+
this._capOutline?.dispose();
|
|
111322
|
+
this._capOutline = null;
|
|
110725
111323
|
// Remove all picking listeners (they hold a strong ref to this Viewer via the
|
|
110726
111324
|
// controller's arrow-field handlers; in external-canvas mode the caller owns the
|
|
110727
111325
|
// canvas, so not removing them would leak the disposed Viewer).
|
|
@@ -110767,6 +111365,7 @@ class Viewer {
|
|
|
110767
111365
|
* @public
|
|
110768
111366
|
*/
|
|
110769
111367
|
clear() {
|
|
111368
|
+
this._cancelCapOutline();
|
|
110770
111369
|
if (this._rendered) {
|
|
110771
111370
|
// Drop selection state + hover cache + status line — a PickedComponent may hold
|
|
110772
111371
|
// a HighlightController / ObjectGroup that this clear() disposes; a stale
|
|
@@ -110820,6 +111419,11 @@ class Viewer {
|
|
|
110820
111419
|
deepDispose(this._rendered.camera);
|
|
110821
111420
|
deepDispose(this._rendered.controls);
|
|
110822
111421
|
deepDispose(this._rendered.treeview);
|
|
111422
|
+
// The orientation marker owns its own THREE.Scene (cones, labels, sphere,
|
|
111423
|
+
// axes), not part of the main scene above, so it needs its own dispose —
|
|
111424
|
+
// without it every clear()/render() cycle left 8 geometries and 3 programs
|
|
111425
|
+
// behind on the GL (measured 2026-09-17, working-docs/leak-harness.html).
|
|
111426
|
+
this._rendered.orientationMarker.dispose();
|
|
110823
111427
|
// clear tree view
|
|
110824
111428
|
this.display.clearCadTree();
|
|
110825
111429
|
// clear info
|
|
@@ -110883,7 +111487,7 @@ class Viewer {
|
|
|
110883
111487
|
if (!this.tree) {
|
|
110884
111488
|
throw new Error("Tree not initialized");
|
|
110885
111489
|
}
|
|
110886
|
-
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false);
|
|
111490
|
+
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false, this.handleTreeHover);
|
|
110887
111491
|
this.display.clearCadTree();
|
|
110888
111492
|
const t = treeview.create();
|
|
110889
111493
|
timer.split("created tree");
|
|
@@ -111349,6 +111953,40 @@ class Viewer {
|
|
|
111349
111953
|
get lastSelection() {
|
|
111350
111954
|
return this.pickingController.lastSelection;
|
|
111351
111955
|
}
|
|
111956
|
+
/**
|
|
111957
|
+
* Copy the world-space point under the cursor as `"x, y, z"` to the clipboard and
|
|
111958
|
+
* send it to the host as a `copiedPoint` notification (for hosts that block the
|
|
111959
|
+
* browser clipboard). Does nothing while parts may be displaced from their model
|
|
111960
|
+
* positions — explode mode, an animation once started (Play or time slider), a
|
|
111961
|
+
* z-scale other than 1 — or when no visible component is under the cursor.
|
|
111962
|
+
* @returns true when a point was copied (the caller then consumes the key event).
|
|
111963
|
+
*/
|
|
111964
|
+
copyPointUnderCursor() {
|
|
111965
|
+
if (!this.ready)
|
|
111966
|
+
return false;
|
|
111967
|
+
const mode = this.state.get("animationMode");
|
|
111968
|
+
if (mode === "explode")
|
|
111969
|
+
return false;
|
|
111970
|
+
if (mode === "animation" && this._animationStarted)
|
|
111971
|
+
return false;
|
|
111972
|
+
if (this.zScale !== undefined && this.zScale !== 1)
|
|
111973
|
+
return false;
|
|
111974
|
+
const point = this.pickingController.pointUnderCursor();
|
|
111975
|
+
if (point === null)
|
|
111976
|
+
return false;
|
|
111977
|
+
const coords = toVector3Tuple(point.toArray());
|
|
111978
|
+
const fmt = (v) => {
|
|
111979
|
+
const s = v.toFixed(COPY_POINT_DECIMALS);
|
|
111980
|
+
return Number(s) === 0 ? (0).toFixed(COPY_POINT_DECIMALS) : s;
|
|
111981
|
+
};
|
|
111982
|
+
const text = coords.map(fmt).join(", ");
|
|
111983
|
+
copyText(text, this.display.container);
|
|
111984
|
+
if (this.notifyCallback !== null) {
|
|
111985
|
+
this.notifyCallback({ copiedPoint: { old: null, new: coords } });
|
|
111986
|
+
}
|
|
111987
|
+
this.display.setStatusLine(`copied: ${text}`);
|
|
111988
|
+
return true;
|
|
111989
|
+
}
|
|
111352
111990
|
/** Enable/disable the double-click pick handler (on when no tool is active). */
|
|
111353
111991
|
setPickHandler(flag) {
|
|
111354
111992
|
this.pickingController.setPickHandler(flag);
|
|
@@ -111763,7 +112401,7 @@ class Viewer {
|
|
|
111763
112401
|
this.tree = this.compactTree;
|
|
111764
112402
|
// Dispose old treeview and create new one
|
|
111765
112403
|
deepDispose(this.rendered.treeview);
|
|
111766
|
-
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false);
|
|
112404
|
+
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false, this.handleTreeHover);
|
|
111767
112405
|
this.rendered.treeview = treeview;
|
|
111768
112406
|
this.display.clearCadTree();
|
|
111769
112407
|
const t = treeview.create();
|
|
@@ -113322,6 +113960,33 @@ class Display {
|
|
|
113322
113960
|
this._matEditorDragAbort = null;
|
|
113323
113961
|
this._matEditorInputAbort = null;
|
|
113324
113962
|
this._animWasVisible = false;
|
|
113963
|
+
/**
|
|
113964
|
+
* Ctrl/Cmd-C while the cursor is over the canvas copies the point under it.
|
|
113965
|
+
* Text-entry fields and selected text keep the normal copy.
|
|
113966
|
+
*/
|
|
113967
|
+
this._handleCopyPoint = (e) => {
|
|
113968
|
+
if (!(e instanceof KeyboardEvent))
|
|
113969
|
+
return;
|
|
113970
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey)
|
|
113971
|
+
return;
|
|
113972
|
+
if (e.key.toLowerCase() !== "c")
|
|
113973
|
+
return;
|
|
113974
|
+
const target = e.target;
|
|
113975
|
+
if ((target instanceof HTMLInputElement &&
|
|
113976
|
+
target.type !== "button" &&
|
|
113977
|
+
target.type !== "checkbox") ||
|
|
113978
|
+
target instanceof HTMLTextAreaElement ||
|
|
113979
|
+
(target instanceof HTMLElement && target.isContentEditable)) {
|
|
113980
|
+
return;
|
|
113981
|
+
}
|
|
113982
|
+
const selection = window.getSelection();
|
|
113983
|
+
if (selection !== null && selection.toString() !== "")
|
|
113984
|
+
return;
|
|
113985
|
+
if (this.viewer.copyPointUnderCursor()) {
|
|
113986
|
+
e.preventDefault();
|
|
113987
|
+
e.stopPropagation();
|
|
113988
|
+
}
|
|
113989
|
+
};
|
|
113325
113990
|
// ---------------------------------------------------------------------------
|
|
113326
113991
|
// Toolbar Button Handlers: View Settings
|
|
113327
113992
|
// ---------------------------------------------------------------------------
|
|
@@ -114644,6 +115309,10 @@ class Display {
|
|
|
114644
115309
|
// Focus handling for keyboard shortcuts
|
|
114645
115310
|
listeners.add(this.container, "mousedown", () => this.container.focus());
|
|
114646
115311
|
listeners.add(this.container, "keydown", this._handleKeyboardShortcut);
|
|
115312
|
+
// Ctrl/Cmd-C acts on the point under the cursor, so it must not depend on the
|
|
115313
|
+
// viewer having focus (e.g. after using a host dropdown). Capture phase: runs
|
|
115314
|
+
// before host shortcuts; the key is consumed only when a point was copied.
|
|
115315
|
+
listeners.add(document, "keydown", this._handleCopyPoint, true);
|
|
114647
115316
|
}
|
|
114648
115317
|
/**
|
|
114649
115318
|
* Subscribe to ViewerState changes to keep UI in sync.
|