three-cad-viewer 5.0.7 → 5.1.1
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 +57 -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/scene/animation.d.ts +10 -0
- package/dist/scene/clipping.d.ts +7 -0
- package/dist/three-cad-viewer.esm.js +718 -43
- 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 +718 -43
- 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
|
|
@@ -85096,7 +85135,7 @@ function focusSizeExpr(stateVar, hover, selected, none) {
|
|
|
85096
85135
|
* renames an anchor must fail LOUDLY here, not silently drop the highlight (build
|
|
85097
85136
|
* would otherwise stay green). One guard test asserts each anchor still exists.
|
|
85098
85137
|
*/
|
|
85099
|
-
function replaceOrThrow(src, anchor, replacement, where) {
|
|
85138
|
+
function replaceOrThrow$1(src, anchor, replacement, where) {
|
|
85100
85139
|
if (!src.includes(anchor)) {
|
|
85101
85140
|
throw new Error(`HighlightController.${where}: shader anchor not found: ${JSON.stringify(anchor)}`);
|
|
85102
85141
|
}
|
|
@@ -85214,6 +85253,23 @@ class HighlightController {
|
|
|
85214
85253
|
}
|
|
85215
85254
|
this._applyHover(`s${solidPath}`, ids);
|
|
85216
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
|
+
}
|
|
85217
85273
|
/** Set or clear the SELECTED flag for a single component id. */
|
|
85218
85274
|
setSelected(id, flag) {
|
|
85219
85275
|
this._setBit(id, HighlightFlag.SELECTED, flag);
|
|
@@ -85316,7 +85372,7 @@ class HighlightController {
|
|
|
85316
85372
|
shader.vertexShader =
|
|
85317
85373
|
HL_VERTEX_HEADER +
|
|
85318
85374
|
"\n" +
|
|
85319
|
-
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);
|
|
85320
85376
|
shader.fragmentShader = HL_FRAGMENT_HEADER + "\n" + shader.fragmentShader;
|
|
85321
85377
|
// Topo-specific color / size injection.
|
|
85322
85378
|
customize(shader);
|
|
@@ -85331,7 +85387,7 @@ class HighlightController {
|
|
|
85331
85387
|
*/
|
|
85332
85388
|
patchFaceMaterial(material) {
|
|
85333
85389
|
this._install(material, "patchFaceMaterial", (shader) => {
|
|
85334
|
-
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");
|
|
85335
85391
|
});
|
|
85336
85392
|
}
|
|
85337
85393
|
/**
|
|
@@ -85343,9 +85399,9 @@ class HighlightController {
|
|
|
85343
85399
|
*/
|
|
85344
85400
|
patchEdgeMaterial(material) {
|
|
85345
85401
|
this._install(material, "patchEdgeMaterial", (shader) => {
|
|
85346
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85402
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85347
85403
|
offset *= ${focusSizeExpr("hlEdgeState", EDGE_FOCUS_WIDTH, EDGE_FOCUS_WIDTH - 2, "linewidth")};`, "patchEdgeMaterial");
|
|
85348
|
-
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");
|
|
85349
85405
|
});
|
|
85350
85406
|
}
|
|
85351
85407
|
/**
|
|
@@ -85360,12 +85416,12 @@ class HighlightController {
|
|
|
85360
85416
|
const cull = options.cullUnhighlighted === true;
|
|
85361
85417
|
const none = cull ? "0.0" : "size";
|
|
85362
85418
|
this._install(material, "patchVertexMaterial", (shader) => {
|
|
85363
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85419
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85364
85420
|
gl_PointSize = ${focusSizeExpr("hlPtState", VERTEX_FOCUS_SIZE, VERTEX_FOCUS_SIZE - 2, none)};`, "patchVertexMaterial");
|
|
85365
85421
|
const discard = cull
|
|
85366
85422
|
? "\n if (highlightState() == 0u) discard;"
|
|
85367
85423
|
: "";
|
|
85368
|
-
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");
|
|
85369
85425
|
},
|
|
85370
85426
|
// The cull discard is the variant that must NOT share a program with the
|
|
85371
85427
|
// standalone (always-visible) vertex cloud — the ghost-vertex bug.
|
|
@@ -90158,8 +90214,9 @@ class TreeView {
|
|
|
90158
90214
|
* @param theme - The UI theme ('light' or 'dark').
|
|
90159
90215
|
* @param linkIcons - Whether icon 0 and 1 are linked.
|
|
90160
90216
|
* @param debug - Enable debug logging.
|
|
90217
|
+
* @param hoverHandler - Callback for label hover (node path, or null on leave).
|
|
90161
90218
|
*/
|
|
90162
|
-
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) {
|
|
90163
90220
|
/************************************************************************************
|
|
90164
90221
|
* Handlers
|
|
90165
90222
|
************************************************************************************/
|
|
@@ -90274,6 +90331,7 @@ class TreeView {
|
|
|
90274
90331
|
this.theme = theme;
|
|
90275
90332
|
this.linkIcons = linkIcons;
|
|
90276
90333
|
this.debug = debug;
|
|
90334
|
+
this.hoverHandler = hoverHandler;
|
|
90277
90335
|
this.model = null;
|
|
90278
90336
|
this.container = null;
|
|
90279
90337
|
this.lastLabel = null;
|
|
@@ -90471,6 +90529,12 @@ class TreeView {
|
|
|
90471
90529
|
e.stopPropagation();
|
|
90472
90530
|
this.handleLabelClick(node, e);
|
|
90473
90531
|
};
|
|
90532
|
+
label.onmouseenter = () => {
|
|
90533
|
+
this.hoverHandler?.(this.getNodePath(node));
|
|
90534
|
+
};
|
|
90535
|
+
label.onmouseleave = () => {
|
|
90536
|
+
this.hoverHandler?.(null);
|
|
90537
|
+
};
|
|
90474
90538
|
nodeContent.appendChild(label);
|
|
90475
90539
|
let childrenContainer = null;
|
|
90476
90540
|
if (node.children) {
|
|
@@ -90962,6 +91026,357 @@ let Timer$1 = class Timer {
|
|
|
90962
91026
|
}
|
|
90963
91027
|
};
|
|
90964
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
|
+
|
|
90965
91380
|
// ============================================================================
|
|
90966
91381
|
// Constants
|
|
90967
91382
|
// ============================================================================
|
|
@@ -91214,6 +91629,9 @@ class Clipping extends Group {
|
|
|
91214
91629
|
super();
|
|
91215
91630
|
/** Per-solid stencil/cap units, the unit of screen-size culling. */
|
|
91216
91631
|
this._capUnits = [];
|
|
91632
|
+
/** Shared uniforms of the section hatching on all cap quads. */
|
|
91633
|
+
this._hatchUniforms = createHatchUniforms(1);
|
|
91634
|
+
this._hatchCenter = new Vector3();
|
|
91217
91635
|
/**
|
|
91218
91636
|
* Whether {@link cull} last ran with clipping active. Lets the inactive path
|
|
91219
91637
|
* gate stencils/caps off exactly once, then early-return on later still frames.
|
|
@@ -91277,6 +91695,10 @@ class Clipping extends Group {
|
|
|
91277
91695
|
mesh.material.visible = flag;
|
|
91278
91696
|
}
|
|
91279
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
|
+
};
|
|
91280
91702
|
this.center = center;
|
|
91281
91703
|
this.distance = size / 2;
|
|
91282
91704
|
this.onNormalChange = options.onNormalChange || null;
|
|
@@ -91339,11 +91761,16 @@ class Clipping extends Group {
|
|
|
91339
91761
|
_createStencils(center, size, theme) {
|
|
91340
91762
|
this._planeMeshGroup = new PlaneMeshGroup();
|
|
91341
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]);
|
|
91342
91767
|
// Group the per-(solid,plane) units by solid for screen-size culling. The
|
|
91343
91768
|
// loop below is plane-major (and `objectColors`/`_planeMeshGroup` order must
|
|
91344
91769
|
// stay plane-major for setObjectColorCaps), so accumulate into a Map keyed by
|
|
91345
91770
|
// solid and flatten afterwards — without touching the plane-major structures.
|
|
91346
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;
|
|
91347
91774
|
for (let i = 0; i < 3; i++) {
|
|
91348
91775
|
const plane = this.clipPlanes[i];
|
|
91349
91776
|
const otherPlanes = this.clipPlanes.filter((_, j) => j !== i);
|
|
@@ -91363,9 +91790,16 @@ class Clipping extends Group {
|
|
|
91363
91790
|
clippingGroup.add(createStencil(`frontStencil-${i}-${j}`, ClippingMaterials.createFrontStencilMaterial(), group.shapeGeometry, plane));
|
|
91364
91791
|
clippingGroup.add(createStencil(`backStencil-${i}-${j}`, ClippingMaterials.createBackStencilMaterial(), group.shapeGeometry, plane));
|
|
91365
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
|
+
}
|
|
91366
91797
|
// Create stencil plane mesh
|
|
91367
91798
|
const planeMaterial = ClippingMaterials.createStencilPlaneMaterial(PLANE_COLORS[theme][i], otherPlanes);
|
|
91799
|
+
capId++;
|
|
91800
|
+
patchHatchMaterial(planeMaterial, this._hatchUniforms, j, capIdColor(capId));
|
|
91368
91801
|
const capMesh = new PlaneMesh(i, plane, center, size, planeMaterial, PLANE_COLORS[theme][i], `StencilPlane-${i}-${j}`);
|
|
91802
|
+
capMesh.layers.enable(CAP_LAYER);
|
|
91369
91803
|
this._planeMeshGroup.add(capMesh);
|
|
91370
91804
|
// Record the cull unit for this solid (one entry per solid, holding
|
|
91371
91805
|
// its up-to-3 per-plane stencil groups + cap quads).
|
|
@@ -91430,6 +91864,10 @@ class Clipping extends Group {
|
|
|
91430
91864
|
this.clipPlanes[index].setConstant(value);
|
|
91431
91865
|
this.reverseClipPlanes[index].setConstant(-value);
|
|
91432
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
|
+
}
|
|
91433
91871
|
/**
|
|
91434
91872
|
* Bound the per-frame stencil/cap draw work to keep large assemblies from
|
|
91435
91873
|
* overrunning the GPU watchdog on clip+rotate (see {@link CAP_CULL_MIN_PX}).
|
|
@@ -91464,6 +91902,9 @@ class Clipping extends Group {
|
|
|
91464
91902
|
return;
|
|
91465
91903
|
}
|
|
91466
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);
|
|
91467
91908
|
// Camera right vector in world space = column 0 of the camera world matrix.
|
|
91468
91909
|
camera.updateMatrixWorld();
|
|
91469
91910
|
const e = camera.matrixWorld.elements;
|
|
@@ -91822,6 +92263,20 @@ class Animation {
|
|
|
91822
92263
|
this.tracks = [];
|
|
91823
92264
|
this.root = null;
|
|
91824
92265
|
}
|
|
92266
|
+
/**
|
|
92267
|
+
* Apply the current animation time to the objects without advancing it, so a
|
|
92268
|
+
* single on-demand render shows the pose (e.g. after {@link setRelativeTime}).
|
|
92269
|
+
*/
|
|
92270
|
+
apply() {
|
|
92271
|
+
this.mixer?.update(0);
|
|
92272
|
+
}
|
|
92273
|
+
/**
|
|
92274
|
+
* Restart the frame-time measurement, so resuming playback after a pause does
|
|
92275
|
+
* not advance the animation by the whole pause.
|
|
92276
|
+
*/
|
|
92277
|
+
resetClock() {
|
|
92278
|
+
this.clock.reset();
|
|
92279
|
+
}
|
|
91825
92280
|
/**
|
|
91826
92281
|
* Update the animation mixer (call each frame when animating).
|
|
91827
92282
|
*/
|
|
@@ -97393,7 +97848,7 @@ class Tools {
|
|
|
97393
97848
|
}
|
|
97394
97849
|
}
|
|
97395
97850
|
|
|
97396
|
-
const version = "5.
|
|
97851
|
+
const version = "5.1.1";
|
|
97397
97852
|
|
|
97398
97853
|
/**
|
|
97399
97854
|
* `PickedComponent` over a GPU id-pick result. Drives the shader
|
|
@@ -97619,6 +98074,58 @@ class PickingController {
|
|
|
97619
98074
|
this.setSelectionInput(false);
|
|
97620
98075
|
this.setPickHandler(false);
|
|
97621
98076
|
}
|
|
98077
|
+
/**
|
|
98078
|
+
* Pick the visible component under the cursor with the current topo filter, or
|
|
98079
|
+
* `null` when the cursor is off the canvas or over background.
|
|
98080
|
+
*/
|
|
98081
|
+
pickUnderCursor() {
|
|
98082
|
+
if (this.host.idPicker === null || !this.idHoverInside)
|
|
98083
|
+
return null;
|
|
98084
|
+
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
98085
|
+
const x = this.idHoverClientX - rect.left;
|
|
98086
|
+
const y = this.idHoverClientY - rect.top;
|
|
98087
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height)
|
|
98088
|
+
return null;
|
|
98089
|
+
const topoFilter = pickerTopoFilter(this.host.display.shapeFilterDropDownMenu.currentFilter);
|
|
98090
|
+
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
98091
|
+
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
98092
|
+
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
98093
|
+
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
98094
|
+
if (hit === null || !this.pickVisible(hit.info))
|
|
98095
|
+
return null;
|
|
98096
|
+
return hit;
|
|
98097
|
+
}
|
|
98098
|
+
/**
|
|
98099
|
+
* World-space point on the visible component under the cursor (same pick as the
|
|
98100
|
+
* hover), or `null` when there is none or the GPU lacks the position attachment.
|
|
98101
|
+
*/
|
|
98102
|
+
pointUnderCursor() {
|
|
98103
|
+
if (!this.host.ready)
|
|
98104
|
+
return null;
|
|
98105
|
+
const hit = this.pickUnderCursor();
|
|
98106
|
+
if (hit === null || hit.point === null)
|
|
98107
|
+
return null;
|
|
98108
|
+
return hit.point.clone();
|
|
98109
|
+
}
|
|
98110
|
+
/**
|
|
98111
|
+
* Tree-row hover: HOVER the faces of the tree node at `path` (leaf or group), or
|
|
98112
|
+
* clear when `null`. Drops the canvas hover target first — otherwise the next
|
|
98113
|
+
* render's {@link handleIdHover} (cursor outside the canvas) would release that
|
|
98114
|
+
* stale target and wipe the tree hover with it.
|
|
98115
|
+
*/
|
|
98116
|
+
setTreeHover(path) {
|
|
98117
|
+
if (!this.host.ready || this.host.studioActive)
|
|
98118
|
+
return;
|
|
98119
|
+
const highlight = this.host.rendered.nestedGroup?.highlight ?? null;
|
|
98120
|
+
if (highlight === null)
|
|
98121
|
+
return;
|
|
98122
|
+
this.releaseLastSelected();
|
|
98123
|
+
this.lastObject = null;
|
|
98124
|
+
this.host.display.setStatusLine("");
|
|
98125
|
+
highlight.setHoverPath(path);
|
|
98126
|
+
if (!this.host.hasAnimationLoop)
|
|
98127
|
+
this.host.update(true, false);
|
|
98128
|
+
}
|
|
97622
98129
|
/**
|
|
97623
98130
|
* Whether hover preselection (highlight + status line) is active. Disabled for GDS:
|
|
97624
98131
|
* dense, stacked, instance-unrolled layout data where per-pixel hover flickers
|
|
@@ -97656,28 +98163,12 @@ class PickingController {
|
|
|
97656
98163
|
this.lastObject = null;
|
|
97657
98164
|
this.host.display.setStatusLine("");
|
|
97658
98165
|
};
|
|
97659
|
-
|
|
97660
|
-
|
|
97661
|
-
return;
|
|
97662
|
-
}
|
|
97663
|
-
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
97664
|
-
const x = this.idHoverClientX - rect.left;
|
|
97665
|
-
const y = this.idHoverClientY - rect.top;
|
|
97666
|
-
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
97667
|
-
release();
|
|
97668
|
-
return;
|
|
97669
|
-
}
|
|
97670
|
-
const filter = this.host.display.shapeFilterDropDownMenu.currentFilter;
|
|
97671
|
-
const fromSolid = filter.includes(TopoFilter.solid);
|
|
97672
|
-
const topoFilter = pickerTopoFilter(filter);
|
|
97673
|
-
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
97674
|
-
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
97675
|
-
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
97676
|
-
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
97677
|
-
if (hit === null || !this.pickVisible(hit.info)) {
|
|
98166
|
+
const hit = this.pickUnderCursor();
|
|
98167
|
+
if (hit === null) {
|
|
97678
98168
|
release();
|
|
97679
98169
|
return;
|
|
97680
98170
|
}
|
|
98171
|
+
const fromSolid = this.host.display.shapeFilterDropDownMenu.currentFilter.includes(TopoFilter.solid);
|
|
97681
98172
|
const picked = new IdPicked(hit.info, fromSolid, hit.point, highlight);
|
|
97682
98173
|
if (!picked.equals(this.lastObject)) {
|
|
97683
98174
|
this.releaseLastSelected();
|
|
@@ -108858,6 +109349,10 @@ function decodeInstancedFormat(data) {
|
|
|
108858
109349
|
// =============================================================================
|
|
108859
109350
|
// IMPORTS
|
|
108860
109351
|
// =============================================================================
|
|
109352
|
+
/** Decimals of a point copied with Ctrl/Cmd-C ({@link Viewer.copyPointUnderCursor}). */
|
|
109353
|
+
const COPY_POINT_DECIMALS = 4;
|
|
109354
|
+
/** Delay after the last camera change before the clip cap outline is drawn again. */
|
|
109355
|
+
const CAP_OUTLINE_SETTLE_MS = 150;
|
|
108861
109356
|
/**
|
|
108862
109357
|
* Type guard to check if an Object3D is an IndexedMesh.
|
|
108863
109358
|
*/
|
|
@@ -108948,6 +109443,8 @@ class Viewer {
|
|
|
108948
109443
|
* @param updateMarker - enforce to redraw orientation marker after every ui activity
|
|
108949
109444
|
*/
|
|
108950
109445
|
constructor(display, options, notifyCallback, pinAsPngCallback = null, updateMarker = true) {
|
|
109446
|
+
/** Reasons for continuous rendering; the render loop runs while any is set. */
|
|
109447
|
+
this._loopReasons = new Set();
|
|
108951
109448
|
// Hide-undo stack: each meta-double-click hide pushes the leaf id + its pre-hide
|
|
108952
109449
|
// state; meta-double-click on empty space pops and restores the last one. Lets a
|
|
108953
109450
|
// hidden object be brought back without the tree (e.g. in Studio, where it's hidden).
|
|
@@ -108968,6 +109465,14 @@ class Viewer {
|
|
|
108968
109465
|
// GPU id-based picker. Created/attached per render() once the scene + compact
|
|
108969
109466
|
// registry exist; drives hover, selection, measure and double-click pick.
|
|
108970
109467
|
this.idPicker = null;
|
|
109468
|
+
/** Outline of the clip caps; created on first use, kept across renders. */
|
|
109469
|
+
this._capOutline = null;
|
|
109470
|
+
/** Camera world + projection matrix of the previous outline frame (motion test). */
|
|
109471
|
+
this._capOutlineCamera = [];
|
|
109472
|
+
/** Pending redraw that adds the outline once the camera has settled. */
|
|
109473
|
+
this._capOutlineSettle = null;
|
|
109474
|
+
/** Whether the attached animation has moved parts (Play pressed or time slider moved). */
|
|
109475
|
+
this._animationStarted = false;
|
|
108971
109476
|
// ---------------------------------------------------------------------------
|
|
108972
109477
|
// Render Loop & Scene Updates
|
|
108973
109478
|
// ---------------------------------------------------------------------------
|
|
@@ -109116,6 +109621,7 @@ class Viewer {
|
|
|
109116
109621
|
}
|
|
109117
109622
|
else {
|
|
109118
109623
|
this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
|
|
109624
|
+
this._renderCapOutline();
|
|
109119
109625
|
}
|
|
109120
109626
|
this.cadTools.update();
|
|
109121
109627
|
this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
|
|
@@ -109369,16 +109875,20 @@ class Viewer {
|
|
|
109369
109875
|
return;
|
|
109370
109876
|
switch (btn) {
|
|
109371
109877
|
case "play":
|
|
109878
|
+
this._animationStarted = true;
|
|
109372
109879
|
if (this.clipAction.paused) {
|
|
109373
109880
|
this.clipAction.paused = false;
|
|
109374
109881
|
}
|
|
109375
109882
|
this.clipAction.play();
|
|
109883
|
+
this._setAnimationPlaying(true);
|
|
109376
109884
|
break;
|
|
109377
109885
|
case "pause":
|
|
109378
109886
|
this.clipAction.paused = !this.clipAction.paused;
|
|
109887
|
+
this._setAnimationPlaying(!this.clipAction.paused && this.clipAction.isRunning());
|
|
109379
109888
|
break;
|
|
109380
109889
|
case "stop":
|
|
109381
109890
|
this.clipAction.stop();
|
|
109891
|
+
this._setAnimationPlaying(false);
|
|
109382
109892
|
break;
|
|
109383
109893
|
}
|
|
109384
109894
|
};
|
|
@@ -109502,6 +110012,10 @@ class Viewer {
|
|
|
109502
110012
|
return;
|
|
109503
110013
|
}
|
|
109504
110014
|
};
|
|
110015
|
+
/** Tree-row hover: highlight the faces of the node at `path`, or clear on `null`. */
|
|
110016
|
+
this.handleTreeHover = (path) => {
|
|
110017
|
+
this.pickingController.setTreeHover(path);
|
|
110018
|
+
};
|
|
109505
110019
|
/** Clear the current selection (and reset the select tool). */
|
|
109506
110020
|
this.clearSelection = () => {
|
|
109507
110021
|
this.pickingController.clearSelection();
|
|
@@ -110264,10 +110778,7 @@ class Viewer {
|
|
|
110264
110778
|
return Promise.resolve({ task: taskId, dataUrl: null });
|
|
110265
110779
|
}
|
|
110266
110780
|
// canvas.toBlob can be very slow when animation loop is off!
|
|
110267
|
-
|
|
110268
|
-
if (!animationLoop) {
|
|
110269
|
-
this.toggleAnimationLoop(true);
|
|
110270
|
-
}
|
|
110781
|
+
this.setLoopReason("capture", true);
|
|
110271
110782
|
this.rendered.orientationMarker.setVisible(false);
|
|
110272
110783
|
this.update(true);
|
|
110273
110784
|
return this.display.captureCanvas({
|
|
@@ -110285,10 +110796,8 @@ class Viewer {
|
|
|
110285
110796
|
}
|
|
110286
110797
|
},
|
|
110287
110798
|
onComplete: () => {
|
|
110288
|
-
// Restore
|
|
110289
|
-
|
|
110290
|
-
this.toggleAnimationLoop(false);
|
|
110291
|
-
}
|
|
110799
|
+
// Restore the loop to what the other reasons need
|
|
110800
|
+
this.setLoopReason("capture", false);
|
|
110292
110801
|
this.rendered.orientationMarker.setVisible(true);
|
|
110293
110802
|
this.update(true);
|
|
110294
110803
|
},
|
|
@@ -110686,13 +111195,13 @@ class Viewer {
|
|
|
110686
111195
|
return;
|
|
110687
111196
|
}
|
|
110688
111197
|
logger.debug("Animation initialized");
|
|
110689
|
-
if (!this.hasAnimationLoop) {
|
|
110690
|
-
this.toggleAnimationLoop(true);
|
|
110691
|
-
}
|
|
110692
111198
|
this.state.set("animationMode", label === "E" ? "explode" : "animation");
|
|
111199
|
+
this._animationStarted = false;
|
|
110693
111200
|
this.clipAction = this.animation.animate(this.rendered.nestedGroup.rootGroup, duration, speed, repeat);
|
|
110694
111201
|
// Reset animation slider to start
|
|
110695
111202
|
this.state.set("animationSliderValue", 0);
|
|
111203
|
+
// Not playing yet: render on demand only (the loop starts with Play).
|
|
111204
|
+
this._setAnimationPlaying(false);
|
|
110696
111205
|
}
|
|
110697
111206
|
/**
|
|
110698
111207
|
* Check whether animation object exists
|
|
@@ -110708,7 +111217,8 @@ class Viewer {
|
|
|
110708
111217
|
deepDispose(this.animation);
|
|
110709
111218
|
}
|
|
110710
111219
|
this.state.set("animationMode", "none");
|
|
110711
|
-
this.
|
|
111220
|
+
this._animationStarted = false;
|
|
111221
|
+
this.setLoopReason("animation", false);
|
|
110712
111222
|
}
|
|
110713
111223
|
/**
|
|
110714
111224
|
* Set the animation to a specific relative time (0-1).
|
|
@@ -110716,8 +111226,12 @@ class Viewer {
|
|
|
110716
111226
|
* @param fraction - relative time between 0 and 1.
|
|
110717
111227
|
*/
|
|
110718
111228
|
setRelativeTime(fraction) {
|
|
111229
|
+
if (fraction !== 0)
|
|
111230
|
+
this._animationStarted = true;
|
|
110719
111231
|
this.animation.setRelativeTime(fraction);
|
|
110720
111232
|
this.state.set("animationSliderValue", fraction * 1000);
|
|
111233
|
+
// Setting a time pauses the animation: stop the loop and show the new pose.
|
|
111234
|
+
this._setAnimationPlaying(false);
|
|
110721
111235
|
}
|
|
110722
111236
|
/**
|
|
110723
111237
|
* Get the current relative animation time (0-1).
|
|
@@ -110726,6 +111240,44 @@ class Viewer {
|
|
|
110726
111240
|
getRelativeTime() {
|
|
110727
111241
|
return this.animation.getRelativeTime();
|
|
110728
111242
|
}
|
|
111243
|
+
/**
|
|
111244
|
+
* Add or remove a reason for continuous rendering. The render loop runs while at
|
|
111245
|
+
* least one reason is set; otherwise the viewer renders on demand (camera changes
|
|
111246
|
+
* and explicit updates), so an idle viewer costs no CPU/GPU time.
|
|
111247
|
+
* @param reason - why continuous rendering is needed.
|
|
111248
|
+
* @param flag - whether the reason applies.
|
|
111249
|
+
*/
|
|
111250
|
+
setLoopReason(reason, flag) {
|
|
111251
|
+
if (flag) {
|
|
111252
|
+
this._loopReasons.add(reason);
|
|
111253
|
+
}
|
|
111254
|
+
else {
|
|
111255
|
+
this._loopReasons.delete(reason);
|
|
111256
|
+
}
|
|
111257
|
+
const run = this._loopReasons.size > 0;
|
|
111258
|
+
if (run !== this.hasAnimationLoop)
|
|
111259
|
+
this.toggleAnimationLoop(run);
|
|
111260
|
+
}
|
|
111261
|
+
/**
|
|
111262
|
+
* Run the render loop only while the animation plays. When it stops (pause,
|
|
111263
|
+
* stop, slider), render once so the final pose is shown.
|
|
111264
|
+
*/
|
|
111265
|
+
_setAnimationPlaying(playing) {
|
|
111266
|
+
if (playing) {
|
|
111267
|
+
// Resume without jumping ahead by the time spent paused.
|
|
111268
|
+
this.animation.resetClock();
|
|
111269
|
+
}
|
|
111270
|
+
this.setLoopReason("animation", playing);
|
|
111271
|
+
if (!playing && this._rendered !== null) {
|
|
111272
|
+
this.animation.apply();
|
|
111273
|
+
this.update(true, false);
|
|
111274
|
+
}
|
|
111275
|
+
}
|
|
111276
|
+
/**
|
|
111277
|
+
* Start or stop the render loop directly. Prefer {@link setLoopReason}, which keeps
|
|
111278
|
+
* the loop running while any other reason still needs it.
|
|
111279
|
+
* @param flag - whether the render loop should run.
|
|
111280
|
+
*/
|
|
110729
111281
|
toggleAnimationLoop(flag) {
|
|
110730
111282
|
if (flag) {
|
|
110731
111283
|
this.continueAnimation = true;
|
|
@@ -110747,6 +111299,60 @@ class Viewer {
|
|
|
110747
111299
|
setTimeout(() => this.update(true, true), 50);
|
|
110748
111300
|
}
|
|
110749
111301
|
}
|
|
111302
|
+
/**
|
|
111303
|
+
* Draw the outline of the clip caps over the frame (Clip tab active only). Skipped
|
|
111304
|
+
* while the camera moves (drag, wheel, damping, preset views): the outline pass
|
|
111305
|
+
* renders faces and caps a second time, so it is drawn once the camera has been
|
|
111306
|
+
* still for {@link CAP_OUTLINE_SETTLE_MS}.
|
|
111307
|
+
*/
|
|
111308
|
+
_renderCapOutline() {
|
|
111309
|
+
const clipping = this.rendered.clipping;
|
|
111310
|
+
if (!this.renderer.localClippingEnabled || !clipping.hasCaps)
|
|
111311
|
+
return;
|
|
111312
|
+
const camera = this.rendered.camera.getCamera();
|
|
111313
|
+
if (this._cameraMovedSinceLastOutline(camera)) {
|
|
111314
|
+
this._scheduleCapOutline();
|
|
111315
|
+
return;
|
|
111316
|
+
}
|
|
111317
|
+
if (this._capOutline === null)
|
|
111318
|
+
this._capOutline = new CapOutlinePass();
|
|
111319
|
+
const blackEdges = this.state.get("blackEdges") === true;
|
|
111320
|
+
this._capOutline.render(this.renderer, this.rendered.scene, camera, {
|
|
111321
|
+
planes: clipping.clipPlanes,
|
|
111322
|
+
intersection: this.state.get("clipIntersection") === true,
|
|
111323
|
+
occlude: this.state.get("transparent") !== true,
|
|
111324
|
+
color: blackEdges ? 0x000000 : this.state.get("edgeColor"),
|
|
111325
|
+
setCapIdPass: clipping.setCapIdPass,
|
|
111326
|
+
});
|
|
111327
|
+
}
|
|
111328
|
+
/** Whether the camera's world or projection matrix changed since the last call. */
|
|
111329
|
+
_cameraMovedSinceLastOutline(camera) {
|
|
111330
|
+
const now = [
|
|
111331
|
+
...camera.matrixWorld.elements,
|
|
111332
|
+
...camera.projectionMatrix.elements,
|
|
111333
|
+
];
|
|
111334
|
+
const last = this._capOutlineCamera;
|
|
111335
|
+
const moved = last.length !== now.length || now.some((value, i) => value !== last[i]);
|
|
111336
|
+
this._capOutlineCamera = now;
|
|
111337
|
+
return moved;
|
|
111338
|
+
}
|
|
111339
|
+
/** Redraw once the camera has settled, so the skipped outline appears. */
|
|
111340
|
+
_scheduleCapOutline() {
|
|
111341
|
+
if (this._capOutlineSettle !== null)
|
|
111342
|
+
clearTimeout(this._capOutlineSettle);
|
|
111343
|
+
this._capOutlineSettle = setTimeout(() => {
|
|
111344
|
+
this._capOutlineSettle = null;
|
|
111345
|
+
if (this.ready)
|
|
111346
|
+
this.update(true, false);
|
|
111347
|
+
}, CAP_OUTLINE_SETTLE_MS);
|
|
111348
|
+
}
|
|
111349
|
+
/** Cancel a pending outline redraw. */
|
|
111350
|
+
_cancelCapOutline() {
|
|
111351
|
+
if (this._capOutlineSettle !== null)
|
|
111352
|
+
clearTimeout(this._capOutlineSettle);
|
|
111353
|
+
this._capOutlineSettle = null;
|
|
111354
|
+
this._capOutlineCamera = [];
|
|
111355
|
+
}
|
|
110750
111356
|
// ---------------------------------------------------------------------------
|
|
110751
111357
|
// Cleanup & Disposal
|
|
110752
111358
|
// ---------------------------------------------------------------------------
|
|
@@ -110765,6 +111371,8 @@ class Viewer {
|
|
|
110765
111371
|
*/
|
|
110766
111372
|
dispose() {
|
|
110767
111373
|
this.clear();
|
|
111374
|
+
this._capOutline?.dispose();
|
|
111375
|
+
this._capOutline = null;
|
|
110768
111376
|
// Remove all picking listeners (they hold a strong ref to this Viewer via the
|
|
110769
111377
|
// controller's arrow-field handlers; in external-canvas mode the caller owns the
|
|
110770
111378
|
// canvas, so not removing them would leak the disposed Viewer).
|
|
@@ -110810,6 +111418,7 @@ class Viewer {
|
|
|
110810
111418
|
* @public
|
|
110811
111419
|
*/
|
|
110812
111420
|
clear() {
|
|
111421
|
+
this._cancelCapOutline();
|
|
110813
111422
|
if (this._rendered) {
|
|
110814
111423
|
// Drop selection state + hover cache + status line — a PickedComponent may hold
|
|
110815
111424
|
// a HighlightController / ObjectGroup that this clear() disposes; a stale
|
|
@@ -110819,6 +111428,7 @@ class Viewer {
|
|
|
110819
111428
|
// Hide the topo filter (+ detach its shortcuts) for a clean cleared canvas.
|
|
110820
111429
|
this.display.shapeFilterDropDownMenu.show(false);
|
|
110821
111430
|
// stop animation
|
|
111431
|
+
this._loopReasons.clear();
|
|
110822
111432
|
this.hasAnimationLoop = false;
|
|
110823
111433
|
this.continueAnimation = false;
|
|
110824
111434
|
// remove change listener if exists
|
|
@@ -110931,7 +111541,7 @@ class Viewer {
|
|
|
110931
111541
|
if (!this.tree) {
|
|
110932
111542
|
throw new Error("Tree not initialized");
|
|
110933
111543
|
}
|
|
110934
|
-
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);
|
|
111544
|
+
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);
|
|
110935
111545
|
this.display.clearCadTree();
|
|
110936
111546
|
const t = treeview.create();
|
|
110937
111547
|
timer.split("created tree");
|
|
@@ -111397,6 +112007,40 @@ class Viewer {
|
|
|
111397
112007
|
get lastSelection() {
|
|
111398
112008
|
return this.pickingController.lastSelection;
|
|
111399
112009
|
}
|
|
112010
|
+
/**
|
|
112011
|
+
* Copy the world-space point under the cursor as `"x, y, z"` to the clipboard and
|
|
112012
|
+
* send it to the host as a `copiedPoint` notification (for hosts that block the
|
|
112013
|
+
* browser clipboard). Does nothing while parts may be displaced from their model
|
|
112014
|
+
* positions — explode mode, an animation once started (Play or time slider), a
|
|
112015
|
+
* z-scale other than 1 — or when no visible component is under the cursor.
|
|
112016
|
+
* @returns true when a point was copied (the caller then consumes the key event).
|
|
112017
|
+
*/
|
|
112018
|
+
copyPointUnderCursor() {
|
|
112019
|
+
if (!this.ready)
|
|
112020
|
+
return false;
|
|
112021
|
+
const mode = this.state.get("animationMode");
|
|
112022
|
+
if (mode === "explode")
|
|
112023
|
+
return false;
|
|
112024
|
+
if (mode === "animation" && this._animationStarted)
|
|
112025
|
+
return false;
|
|
112026
|
+
if (this.zScale !== undefined && this.zScale !== 1)
|
|
112027
|
+
return false;
|
|
112028
|
+
const point = this.pickingController.pointUnderCursor();
|
|
112029
|
+
if (point === null)
|
|
112030
|
+
return false;
|
|
112031
|
+
const coords = toVector3Tuple(point.toArray());
|
|
112032
|
+
const fmt = (v) => {
|
|
112033
|
+
const s = v.toFixed(COPY_POINT_DECIMALS);
|
|
112034
|
+
return Number(s) === 0 ? (0).toFixed(COPY_POINT_DECIMALS) : s;
|
|
112035
|
+
};
|
|
112036
|
+
const text = coords.map(fmt).join(", ");
|
|
112037
|
+
copyText(text, this.display.container);
|
|
112038
|
+
if (this.notifyCallback !== null) {
|
|
112039
|
+
this.notifyCallback({ copiedPoint: { old: null, new: coords } });
|
|
112040
|
+
}
|
|
112041
|
+
this.display.setStatusLine(`copied: ${text}`);
|
|
112042
|
+
return true;
|
|
112043
|
+
}
|
|
111400
112044
|
/** Enable/disable the double-click pick handler (on when no tool is active). */
|
|
111401
112045
|
setPickHandler(flag) {
|
|
111402
112046
|
this.pickingController.setPickHandler(flag);
|
|
@@ -111811,7 +112455,7 @@ class Viewer {
|
|
|
111811
112455
|
this.tree = this.compactTree;
|
|
111812
112456
|
// Dispose old treeview and create new one
|
|
111813
112457
|
deepDispose(this.rendered.treeview);
|
|
111814
|
-
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);
|
|
112458
|
+
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);
|
|
111815
112459
|
this.rendered.treeview = treeview;
|
|
111816
112460
|
this.display.clearCadTree();
|
|
111817
112461
|
const t = treeview.create();
|
|
@@ -113370,6 +114014,33 @@ class Display {
|
|
|
113370
114014
|
this._matEditorDragAbort = null;
|
|
113371
114015
|
this._matEditorInputAbort = null;
|
|
113372
114016
|
this._animWasVisible = false;
|
|
114017
|
+
/**
|
|
114018
|
+
* Ctrl/Cmd-C while the cursor is over the canvas copies the point under it.
|
|
114019
|
+
* Text-entry fields and selected text keep the normal copy.
|
|
114020
|
+
*/
|
|
114021
|
+
this._handleCopyPoint = (e) => {
|
|
114022
|
+
if (!(e instanceof KeyboardEvent))
|
|
114023
|
+
return;
|
|
114024
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey)
|
|
114025
|
+
return;
|
|
114026
|
+
if (e.key.toLowerCase() !== "c")
|
|
114027
|
+
return;
|
|
114028
|
+
const target = e.target;
|
|
114029
|
+
if ((target instanceof HTMLInputElement &&
|
|
114030
|
+
target.type !== "button" &&
|
|
114031
|
+
target.type !== "checkbox") ||
|
|
114032
|
+
target instanceof HTMLTextAreaElement ||
|
|
114033
|
+
(target instanceof HTMLElement && target.isContentEditable)) {
|
|
114034
|
+
return;
|
|
114035
|
+
}
|
|
114036
|
+
const selection = window.getSelection();
|
|
114037
|
+
if (selection !== null && selection.toString() !== "")
|
|
114038
|
+
return;
|
|
114039
|
+
if (this.viewer.copyPointUnderCursor()) {
|
|
114040
|
+
e.preventDefault();
|
|
114041
|
+
e.stopPropagation();
|
|
114042
|
+
}
|
|
114043
|
+
};
|
|
113373
114044
|
// ---------------------------------------------------------------------------
|
|
113374
114045
|
// Toolbar Button Handlers: View Settings
|
|
113375
114046
|
// ---------------------------------------------------------------------------
|
|
@@ -113452,7 +114123,7 @@ class Display {
|
|
|
113452
114123
|
if (flag && this.viewer.isStudioActive) {
|
|
113453
114124
|
return;
|
|
113454
114125
|
}
|
|
113455
|
-
this.viewer.
|
|
114126
|
+
this.viewer.setLoopReason("tool", flag);
|
|
113456
114127
|
if (flag) {
|
|
113457
114128
|
// Delegate state mutations to Viewer
|
|
113458
114129
|
this.viewer.activateTool(name, true);
|
|
@@ -114692,6 +115363,10 @@ class Display {
|
|
|
114692
115363
|
// Focus handling for keyboard shortcuts
|
|
114693
115364
|
listeners.add(this.container, "mousedown", () => this.container.focus());
|
|
114694
115365
|
listeners.add(this.container, "keydown", this._handleKeyboardShortcut);
|
|
115366
|
+
// Ctrl/Cmd-C acts on the point under the cursor, so it must not depend on the
|
|
115367
|
+
// viewer having focus (e.g. after using a host dropdown). Capture phase: runs
|
|
115368
|
+
// before host shortcuts; the key is consumed only when a point was copied.
|
|
115369
|
+
listeners.add(document, "keydown", this._handleCopyPoint, true);
|
|
114695
115370
|
}
|
|
114696
115371
|
/**
|
|
114697
115372
|
* Subscribe to ViewerState changes to keep UI in sync.
|