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
package/dist/three-cad-viewer.js
CHANGED
|
@@ -81835,6 +81835,45 @@ void main() {
|
|
|
81835
81835
|
this.listeners = [];
|
|
81836
81836
|
}
|
|
81837
81837
|
}
|
|
81838
|
+
/**
|
|
81839
|
+
* Copy `text` to the clipboard from inside a user-initiated event handler.
|
|
81840
|
+
*
|
|
81841
|
+
* Tries the synchronous hidden-textarea `execCommand("copy")` first: it needs no
|
|
81842
|
+
* clipboard permission, so it also works in embedding iframes and WebKit views that
|
|
81843
|
+
* refuse `navigator.clipboard`. Falls back to the async Clipboard API.
|
|
81844
|
+
* Focus is restored to the previously focused element.
|
|
81845
|
+
*
|
|
81846
|
+
* @param text - The text to copy.
|
|
81847
|
+
* @param parent - Element the temporary textarea is attached to.
|
|
81848
|
+
* @returns true when the synchronous copy reported success.
|
|
81849
|
+
*/
|
|
81850
|
+
function copyText(text, parent) {
|
|
81851
|
+
const previous = document.activeElement;
|
|
81852
|
+
const area = document.createElement("textarea");
|
|
81853
|
+
area.value = text;
|
|
81854
|
+
area.setAttribute("readonly", "");
|
|
81855
|
+
area.style.position = "fixed";
|
|
81856
|
+
area.style.left = "-9999px";
|
|
81857
|
+
area.style.opacity = "0";
|
|
81858
|
+
parent.appendChild(area);
|
|
81859
|
+
area.select();
|
|
81860
|
+
let copied;
|
|
81861
|
+
try {
|
|
81862
|
+
copied = document.execCommand("copy");
|
|
81863
|
+
}
|
|
81864
|
+
catch {
|
|
81865
|
+
copied = false;
|
|
81866
|
+
}
|
|
81867
|
+
parent.removeChild(area);
|
|
81868
|
+
if (previous instanceof HTMLElement)
|
|
81869
|
+
previous.focus({ preventScroll: true });
|
|
81870
|
+
if (!copied && navigator.clipboard !== undefined) {
|
|
81871
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
81872
|
+
logger.debug("Clipboard write refused:", err);
|
|
81873
|
+
});
|
|
81874
|
+
}
|
|
81875
|
+
return copied;
|
|
81876
|
+
}
|
|
81838
81877
|
|
|
81839
81878
|
/**
|
|
81840
81879
|
* Zebra Analysis Tool for Three.js CAD Viewer
|
|
@@ -85102,7 +85141,7 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85102
85141
|
* renames an anchor must fail LOUDLY here, not silently drop the highlight (build
|
|
85103
85142
|
* would otherwise stay green). One guard test asserts each anchor still exists.
|
|
85104
85143
|
*/
|
|
85105
|
-
function replaceOrThrow(src, anchor, replacement, where) {
|
|
85144
|
+
function replaceOrThrow$1(src, anchor, replacement, where) {
|
|
85106
85145
|
if (!src.includes(anchor)) {
|
|
85107
85146
|
throw new Error(`HighlightController.${where}: shader anchor not found: ${JSON.stringify(anchor)}`);
|
|
85108
85147
|
}
|
|
@@ -85220,6 +85259,23 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85220
85259
|
}
|
|
85221
85260
|
this._applyHover(`s${solidPath}`, ids);
|
|
85222
85261
|
}
|
|
85262
|
+
/**
|
|
85263
|
+
* HOVER every FACE at or below the tree path `prefix` (a leaf or a whole group),
|
|
85264
|
+
* or clear hover when `null`. Drives the tree-row hover highlight.
|
|
85265
|
+
*/
|
|
85266
|
+
setHoverPath(prefix) {
|
|
85267
|
+
if (prefix == null) {
|
|
85268
|
+
this._applyHover("", []);
|
|
85269
|
+
return;
|
|
85270
|
+
}
|
|
85271
|
+
const sub = prefix + "/";
|
|
85272
|
+
const ids = [];
|
|
85273
|
+
for (const info of this.registry.entries()) {
|
|
85274
|
+
if (info.topo === "face" && info.path.startsWith(sub))
|
|
85275
|
+
ids.push(info.id);
|
|
85276
|
+
}
|
|
85277
|
+
this._applyHover(`p${prefix}`, ids);
|
|
85278
|
+
}
|
|
85223
85279
|
/** Set or clear the SELECTED flag for a single component id. */
|
|
85224
85280
|
setSelected(id, flag) {
|
|
85225
85281
|
this._setBit(id, HighlightFlag.SELECTED, flag);
|
|
@@ -85322,7 +85378,7 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85322
85378
|
shader.vertexShader =
|
|
85323
85379
|
HL_VERTEX_HEADER +
|
|
85324
85380
|
"\n" +
|
|
85325
|
-
replaceOrThrow(shader.vertexShader, "void main() {", `void main() {\n ${HL_VERTEX_ASSIGN}`, where);
|
|
85381
|
+
replaceOrThrow$1(shader.vertexShader, "void main() {", `void main() {\n ${HL_VERTEX_ASSIGN}`, where);
|
|
85326
85382
|
shader.fragmentShader = HL_FRAGMENT_HEADER + "\n" + shader.fragmentShader;
|
|
85327
85383
|
// Topo-specific color / size injection.
|
|
85328
85384
|
customize(shader);
|
|
@@ -85337,7 +85393,7 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85337
85393
|
*/
|
|
85338
85394
|
patchFaceMaterial(material) {
|
|
85339
85395
|
this._install(material, "patchFaceMaterial", (shader) => {
|
|
85340
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchFaceMaterial");
|
|
85396
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchFaceMaterial");
|
|
85341
85397
|
});
|
|
85342
85398
|
}
|
|
85343
85399
|
/**
|
|
@@ -85349,9 +85405,9 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85349
85405
|
*/
|
|
85350
85406
|
patchEdgeMaterial(material) {
|
|
85351
85407
|
this._install(material, "patchEdgeMaterial", (shader) => {
|
|
85352
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85408
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "offset *= linewidth;", `uint hlEdgeState = highlightState();
|
|
85353
85409
|
offset *= ${focusSizeExpr("hlEdgeState", EDGE_FOCUS_WIDTH, EDGE_FOCUS_WIDTH - 2, "linewidth")};`, "patchEdgeMaterial");
|
|
85354
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchEdgeMaterial");
|
|
85410
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${HL_COLOR_OVERRIDE}`, "patchEdgeMaterial");
|
|
85355
85411
|
});
|
|
85356
85412
|
}
|
|
85357
85413
|
/**
|
|
@@ -85366,12 +85422,12 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
|
|
|
85366
85422
|
const cull = options.cullUnhighlighted === true;
|
|
85367
85423
|
const none = cull ? "0.0" : "size";
|
|
85368
85424
|
this._install(material, "patchVertexMaterial", (shader) => {
|
|
85369
|
-
shader.vertexShader = replaceOrThrow(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85425
|
+
shader.vertexShader = replaceOrThrow$1(shader.vertexShader, "gl_PointSize = size;", `uint hlPtState = highlightState();
|
|
85370
85426
|
gl_PointSize = ${focusSizeExpr("hlPtState", VERTEX_FOCUS_SIZE, VERTEX_FOCUS_SIZE - 2, none)};`, "patchVertexMaterial");
|
|
85371
85427
|
const discard = cull
|
|
85372
85428
|
? "\n if (highlightState() == 0u) discard;"
|
|
85373
85429
|
: "";
|
|
85374
|
-
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${discard}${HL_COLOR_OVERRIDE}`, "patchVertexMaterial");
|
|
85430
|
+
shader.fragmentShader = replaceOrThrow$1(shader.fragmentShader, "#include <color_fragment>", `#include <color_fragment>${discard}${HL_COLOR_OVERRIDE}`, "patchVertexMaterial");
|
|
85375
85431
|
},
|
|
85376
85432
|
// The cull discard is the variant that must NOT share a program with the
|
|
85377
85433
|
// standalone (always-visible) vertex cloud — the ghost-vertex bug.
|
|
@@ -90164,8 +90220,9 @@ float metalnessFactor = metalness;
|
|
|
90164
90220
|
* @param theme - The UI theme ('light' or 'dark').
|
|
90165
90221
|
* @param linkIcons - Whether icon 0 and 1 are linked.
|
|
90166
90222
|
* @param debug - Enable debug logging.
|
|
90223
|
+
* @param hoverHandler - Callback for label hover (node path, or null on leave).
|
|
90167
90224
|
*/
|
|
90168
|
-
constructor(tree, scrollContainer, objectHandler, pickHandler, updateHandler, notificationHandler, colorGetter, theme, linkIcons, debug = false) {
|
|
90225
|
+
constructor(tree, scrollContainer, objectHandler, pickHandler, updateHandler, notificationHandler, colorGetter, theme, linkIcons, debug = false, hoverHandler = null) {
|
|
90169
90226
|
/************************************************************************************
|
|
90170
90227
|
* Handlers
|
|
90171
90228
|
************************************************************************************/
|
|
@@ -90280,6 +90337,7 @@ float metalnessFactor = metalness;
|
|
|
90280
90337
|
this.theme = theme;
|
|
90281
90338
|
this.linkIcons = linkIcons;
|
|
90282
90339
|
this.debug = debug;
|
|
90340
|
+
this.hoverHandler = hoverHandler;
|
|
90283
90341
|
this.model = null;
|
|
90284
90342
|
this.container = null;
|
|
90285
90343
|
this.lastLabel = null;
|
|
@@ -90477,6 +90535,12 @@ float metalnessFactor = metalness;
|
|
|
90477
90535
|
e.stopPropagation();
|
|
90478
90536
|
this.handleLabelClick(node, e);
|
|
90479
90537
|
};
|
|
90538
|
+
label.onmouseenter = () => {
|
|
90539
|
+
this.hoverHandler?.(this.getNodePath(node));
|
|
90540
|
+
};
|
|
90541
|
+
label.onmouseleave = () => {
|
|
90542
|
+
this.hoverHandler?.(null);
|
|
90543
|
+
};
|
|
90480
90544
|
nodeContent.appendChild(label);
|
|
90481
90545
|
let childrenContainer = null;
|
|
90482
90546
|
if (node.children) {
|
|
@@ -90968,6 +91032,357 @@ float metalnessFactor = metalness;
|
|
|
90968
91032
|
}
|
|
90969
91033
|
};
|
|
90970
91034
|
|
|
91035
|
+
/**
|
|
91036
|
+
* Section hatching for clip caps (CAD mode, shader-only). The per-solid cap quads
|
|
91037
|
+
* draw hatch lines anchored to the plane (stable while rotating/panning) whose
|
|
91038
|
+
* spacing is kept at a constant pixel distance by a per-frame world-per-pixel
|
|
91039
|
+
* uniform. The outline of the caps is drawn separately (see cap-outline.ts); for its
|
|
91040
|
+
* id pass the same cap shader can output the cap's id instead of its color.
|
|
91041
|
+
*
|
|
91042
|
+
* All shared uniforms are plain `{ value }` objects held by reference, so a single
|
|
91043
|
+
* write (e.g. the per-frame hatch spacing) updates every patched material without a
|
|
91044
|
+
* recompile.
|
|
91045
|
+
*/
|
|
91046
|
+
/** Distance between hatch lines in pixels. */
|
|
91047
|
+
const HATCH_SPACING_PX = 8;
|
|
91048
|
+
/** Width of a hatch line in pixels. */
|
|
91049
|
+
const HATCH_LINE_WIDTH_PX = 1;
|
|
91050
|
+
/**
|
|
91051
|
+
* Hatch angle (radians) and spacing factor per solid, cycled by the solid's index
|
|
91052
|
+
* so that neighbouring solids are told apart (drawing convention: alternate the
|
|
91053
|
+
* hatch direction between adjacent parts).
|
|
91054
|
+
*/
|
|
91055
|
+
const HATCH_VARIANTS = [
|
|
91056
|
+
{ angle: Math.PI / 4, scale: 1.0 },
|
|
91057
|
+
{ angle: (3 * Math.PI) / 4, scale: 1.0 },
|
|
91058
|
+
{ angle: Math.PI / 4, scale: 1.6 },
|
|
91059
|
+
{ angle: (3 * Math.PI) / 4, scale: 1.6 },
|
|
91060
|
+
];
|
|
91061
|
+
function createHatchUniforms(capHalfSize) {
|
|
91062
|
+
return {
|
|
91063
|
+
uHatchSpacing: { value: 1 },
|
|
91064
|
+
uCapHalfSize: { value: capHalfSize },
|
|
91065
|
+
uHatchLineWidth: { value: HATCH_LINE_WIDTH_PX },
|
|
91066
|
+
uHatchOn: { value: 1 },
|
|
91067
|
+
uCapIdPass: { value: 0 },
|
|
91068
|
+
};
|
|
91069
|
+
}
|
|
91070
|
+
/**
|
|
91071
|
+
* World units covered by one screen pixel at the given point (exact for an
|
|
91072
|
+
* orthographic camera; at the point's distance for a perspective camera).
|
|
91073
|
+
*/
|
|
91074
|
+
function worldPerPixel(camera, point, height) {
|
|
91075
|
+
if (height <= 0)
|
|
91076
|
+
return 1;
|
|
91077
|
+
if (camera instanceof OrthographicCamera) {
|
|
91078
|
+
return (camera.top - camera.bottom) / camera.zoom / height;
|
|
91079
|
+
}
|
|
91080
|
+
if (camera instanceof PerspectiveCamera) {
|
|
91081
|
+
const dist = camera.position.distanceTo(point);
|
|
91082
|
+
const halfFov = MathUtils.degToRad(camera.fov) / 2;
|
|
91083
|
+
return (2 * dist * Math.tan(halfFov)) / camera.zoom / height;
|
|
91084
|
+
}
|
|
91085
|
+
return 1;
|
|
91086
|
+
}
|
|
91087
|
+
function replaceOrThrow(source, anchor, replacement, where) {
|
|
91088
|
+
if (!source.includes(anchor)) {
|
|
91089
|
+
throw new Error(`${where}: shader anchor "${anchor}" not found`);
|
|
91090
|
+
}
|
|
91091
|
+
return source.replace(anchor, replacement);
|
|
91092
|
+
}
|
|
91093
|
+
/**
|
|
91094
|
+
* Patch a cap quad material to draw section hatching. `index` is the solid's index
|
|
91095
|
+
* among the caps of its plane; it selects the hatch direction and spacing. `capId`
|
|
91096
|
+
* is the cap's unique id color for the outline id pass (see `capIdColor`).
|
|
91097
|
+
*/
|
|
91098
|
+
function patchHatchMaterial(material, uniforms, index, capId) {
|
|
91099
|
+
if (material.userData.hatchPatched === true)
|
|
91100
|
+
return;
|
|
91101
|
+
material.userData.hatchPatched = true;
|
|
91102
|
+
const variant = HATCH_VARIANTS[index % HATCH_VARIANTS.length];
|
|
91103
|
+
const uHatchAngle = { value: variant.angle };
|
|
91104
|
+
const uHatchScale = { value: variant.scale };
|
|
91105
|
+
const uCapId = { value: capId };
|
|
91106
|
+
const prev = material.onBeforeCompile;
|
|
91107
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
91108
|
+
prev.call(material, shader, renderer);
|
|
91109
|
+
shader.uniforms.uHatchSpacing = uniforms.uHatchSpacing;
|
|
91110
|
+
shader.uniforms.uCapHalfSize = uniforms.uCapHalfSize;
|
|
91111
|
+
shader.uniforms.uHatchLineWidth = uniforms.uHatchLineWidth;
|
|
91112
|
+
shader.uniforms.uHatchOn = uniforms.uHatchOn;
|
|
91113
|
+
shader.uniforms.uHatchAngle = uHatchAngle;
|
|
91114
|
+
shader.uniforms.uHatchScale = uHatchScale;
|
|
91115
|
+
shader.uniforms.uCapIdPass = uniforms.uCapIdPass;
|
|
91116
|
+
shader.uniforms.uCapId = uCapId;
|
|
91117
|
+
shader.vertexShader =
|
|
91118
|
+
"uniform float uCapHalfSize;\nvarying vec2 vTcvCapUV;\n" +
|
|
91119
|
+
replaceOrThrow(shader.vertexShader, "#include <begin_vertex>", "#include <begin_vertex>\n vTcvCapUV = position.xy * uCapHalfSize;", "patchHatchMaterial");
|
|
91120
|
+
shader.fragmentShader =
|
|
91121
|
+
"uniform float uHatchSpacing;\nuniform float uHatchLineWidth;\nuniform float uHatchOn;\n" +
|
|
91122
|
+
"uniform float uHatchAngle;\nuniform float uHatchScale;\nvarying vec2 vTcvCapUV;\n" +
|
|
91123
|
+
"uniform float uCapIdPass;\nuniform vec3 uCapId;\n" +
|
|
91124
|
+
shader.fragmentShader;
|
|
91125
|
+
// Hatch the unlit base color, before lighting: the ink (dark lines on light
|
|
91126
|
+
// parts, light lines on dark parts) is then fixed per part, and lines and cap
|
|
91127
|
+
// darken together when the cap turns away from the light.
|
|
91128
|
+
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <color_fragment>",
|
|
91129
|
+
/* glsl */ `#include <color_fragment>
|
|
91130
|
+
if ( uHatchOn > 0.5 ) {
|
|
91131
|
+
vec2 tcvAcross = vec2( - sin( uHatchAngle ), cos( uHatchAngle ) );
|
|
91132
|
+
float t = dot( vTcvCapUV, tcvAcross ) / max( uHatchSpacing * uHatchScale, 1e-9 );
|
|
91133
|
+
float tw = max( fwidth( t ), 1e-6 );
|
|
91134
|
+
float dpx = ( 0.5 - abs( fract( t ) - 0.5 ) ) / tw;
|
|
91135
|
+
float line = 1.0 - smoothstep( 0.5 * uHatchLineWidth - 0.5, 0.5 * uHatchLineWidth + 0.5, dpx );
|
|
91136
|
+
// Seen edge-on the lines crowd below a few pixels apart and would merge into
|
|
91137
|
+
// solid ink: fade them out between 4 and 2 px spacing (1 / tw = px per line).
|
|
91138
|
+
line *= smoothstep( 2.0, 4.0, 1.0 / tw );
|
|
91139
|
+
float lum = dot( diffuseColor.rgb, vec3( 0.2126, 0.7152, 0.0722 ) );
|
|
91140
|
+
vec3 ink = lum > 0.2 ? diffuseColor.rgb * 0.3 : mix( diffuseColor.rgb, vec3( 1.0 ), 0.6 );
|
|
91141
|
+
diffuseColor.rgb = mix( diffuseColor.rgb, ink, line );
|
|
91142
|
+
}`, "patchHatchMaterial");
|
|
91143
|
+
shader.fragmentShader = replaceOrThrow(shader.fragmentShader, "#include <opaque_fragment>", `#include <opaque_fragment>
|
|
91144
|
+
if ( uCapIdPass > 0.5 ) gl_FragColor = vec4( uCapId, 1.0 );`, "patchHatchMaterial");
|
|
91145
|
+
};
|
|
91146
|
+
material.needsUpdate = true;
|
|
91147
|
+
}
|
|
91148
|
+
|
|
91149
|
+
/**
|
|
91150
|
+
* Screen-space outline of the clip caps (CAD mode).
|
|
91151
|
+
*
|
|
91152
|
+
* The caps carry no geometry of their own outline, and the faces next to a cut may
|
|
91153
|
+
* face away from the camera or belong to a touching solid, so the outline is found
|
|
91154
|
+
* in screen space instead:
|
|
91155
|
+
*
|
|
91156
|
+
* 1. Id pass into an offscreen target (color + depth + stencil):
|
|
91157
|
+
* - visible faces as black occluders (face pick layer + override material), so a
|
|
91158
|
+
* cap hidden behind another part gets no outline; face pixels within the line
|
|
91159
|
+
* width of a clip plane write {@link CUT_EDGE_ID} instead, which draws the cut
|
|
91160
|
+
* contour on the faces themselves (needed when the caps are behind the faces),
|
|
91161
|
+
* - the stencil meshes and caps (on {@link CAP_LAYER}) with each cap writing its
|
|
91162
|
+
* own id instead of its shaded color — the same stencil sequence as the main
|
|
91163
|
+
* render, since three orders them identically (material creation order).
|
|
91164
|
+
* 2. Composite over the canvas: a pixel is drawn in the edge color when its id is
|
|
91165
|
+
* larger than a neighbour's id, which yields a line on one side of every boundary
|
|
91166
|
+
* cap↔cap, cap↔surface and cap↔background. Cut-edge pixels are the largest id, so
|
|
91167
|
+
* where they meet a cap only they are drawn — one line, not two.
|
|
91168
|
+
*/
|
|
91169
|
+
/** Render layer of the clip stencil meshes and cap quads (pick layers use 1-3). */
|
|
91170
|
+
const CAP_LAYER = 4;
|
|
91171
|
+
/** Id of face pixels on the cut contour (larger than any cap id). */
|
|
91172
|
+
const CUT_EDGE_ID = 0xffffff;
|
|
91173
|
+
/** Encode a cap id (1..2^24-1) as an exact 8-bit RGB color (0 = no cap). */
|
|
91174
|
+
function capIdColor(id) {
|
|
91175
|
+
return new Vector3((id & 0xff) / 255, ((id >> 8) & 0xff) / 255, ((id >> 16) & 0xff) / 255);
|
|
91176
|
+
}
|
|
91177
|
+
// Occluder: a face fragment lies on the cut contour when it is within uCutWidth
|
|
91178
|
+
// pixels of a clip plane that bounds the visible part, mirroring three's
|
|
91179
|
+
// clipping_planes_fragment (kept fragments have d >= 0). Skipped where the face is
|
|
91180
|
+
// (nearly) parallel to the plane: there d barely changes across the pixel, and a
|
|
91181
|
+
// face lying in the plane would otherwise become one big edge.
|
|
91182
|
+
const CUT_EDGE_GLSL = /* glsl */ `
|
|
91183
|
+
bool tcvCut = false;
|
|
91184
|
+
#if NUM_CLIPPING_PLANES > 0
|
|
91185
|
+
{
|
|
91186
|
+
float tcvPosGrad = length( fwidth( vClipPosition ) );
|
|
91187
|
+
float tcvD[ NUM_CLIPPING_PLANES ];
|
|
91188
|
+
bool tcvNear[ NUM_CLIPPING_PLANES ];
|
|
91189
|
+
for ( int i = 0; i < NUM_CLIPPING_PLANES; i ++ ) {
|
|
91190
|
+
vec4 p = clippingPlanes[ i ];
|
|
91191
|
+
float d = p.w - dot( vClipPosition, p.xyz );
|
|
91192
|
+
// Step of d per pixel along the dominant screen axis — the same 1-pixel
|
|
91193
|
+
// convention as the composite's horizontal/vertical neighbour test.
|
|
91194
|
+
float g = max( abs( dFdx( d ) ), abs( dFdy( d ) ) );
|
|
91195
|
+
tcvD[ i ] = d;
|
|
91196
|
+
tcvNear[ i ] = g > 0.05 * tcvPosGrad && d >= 0.0 && d < uCutWidth * g;
|
|
91197
|
+
}
|
|
91198
|
+
for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {
|
|
91199
|
+
tcvCut = tcvCut || tcvNear[ i ];
|
|
91200
|
+
}
|
|
91201
|
+
#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES
|
|
91202
|
+
// Intersection planes: plane i bounds the visible part only where every
|
|
91203
|
+
// other intersection plane clips.
|
|
91204
|
+
for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {
|
|
91205
|
+
bool othersClip = true;
|
|
91206
|
+
for ( int j = UNION_CLIPPING_PLANES; j < NUM_CLIPPING_PLANES; j ++ ) {
|
|
91207
|
+
if ( j != i && tcvD[ j ] >= 0.0 ) othersClip = false;
|
|
91208
|
+
}
|
|
91209
|
+
if ( othersClip && tcvNear[ i ] ) tcvCut = true;
|
|
91210
|
+
}
|
|
91211
|
+
#endif
|
|
91212
|
+
}
|
|
91213
|
+
#endif
|
|
91214
|
+
`;
|
|
91215
|
+
const COMPOSITE_VERTEX = /* glsl */ `
|
|
91216
|
+
varying vec2 vUv;
|
|
91217
|
+
void main() {
|
|
91218
|
+
vUv = uv;
|
|
91219
|
+
gl_Position = vec4( position.xy, 0.0, 1.0 );
|
|
91220
|
+
}
|
|
91221
|
+
`;
|
|
91222
|
+
const COMPOSITE_FRAGMENT = /* glsl */ `
|
|
91223
|
+
uniform sampler2D tId;
|
|
91224
|
+
uniform vec2 uTexel;
|
|
91225
|
+
uniform float uWidth;
|
|
91226
|
+
uniform vec3 uColor;
|
|
91227
|
+
varying vec2 vUv;
|
|
91228
|
+
|
|
91229
|
+
float idAt( vec2 uv ) {
|
|
91230
|
+
vec3 c = floor( texture2D( tId, uv ).rgb * 255.0 + 0.5 );
|
|
91231
|
+
return c.r + 256.0 * c.g + 65536.0 * c.b;
|
|
91232
|
+
}
|
|
91233
|
+
|
|
91234
|
+
void main() {
|
|
91235
|
+
float c = idAt( vUv );
|
|
91236
|
+
bool edge = c == ${CUT_EDGE_ID.toFixed(1)};
|
|
91237
|
+
for ( int k = 1; k <= 4; k ++ ) {
|
|
91238
|
+
if ( float( k ) > uWidth ) break;
|
|
91239
|
+
vec2 o = uTexel * float( k );
|
|
91240
|
+
edge = edge
|
|
91241
|
+
|| c > idAt( vUv + vec2( o.x, 0.0 ) ) || c > idAt( vUv - vec2( o.x, 0.0 ) )
|
|
91242
|
+
|| c > idAt( vUv + vec2( 0.0, o.y ) ) || c > idAt( vUv - vec2( 0.0, o.y ) );
|
|
91243
|
+
}
|
|
91244
|
+
if ( ! edge ) discard;
|
|
91245
|
+
gl_FragColor = vec4( uColor, 1.0 );
|
|
91246
|
+
#include <colorspace_fragment>
|
|
91247
|
+
}
|
|
91248
|
+
`;
|
|
91249
|
+
class CapOutlinePass {
|
|
91250
|
+
constructor() {
|
|
91251
|
+
this.target = null;
|
|
91252
|
+
/** Width of the cut contour on the faces, in device pixels (= outline width). */
|
|
91253
|
+
this.cutWidth = { value: 1 };
|
|
91254
|
+
this.size = new Vector2();
|
|
91255
|
+
this.savedClearColor = new Color();
|
|
91256
|
+
this.savedViewport = new Vector4();
|
|
91257
|
+
// One depth unit behind the caps (polygonOffset 1/1), so a face coplanar with a
|
|
91258
|
+
// cap does not hide it; same slope factor, so steep faces in front still occlude.
|
|
91259
|
+
this.occluder = new MeshBasicMaterial({
|
|
91260
|
+
color: 0x000000,
|
|
91261
|
+
side: DoubleSide,
|
|
91262
|
+
polygonOffset: true,
|
|
91263
|
+
polygonOffsetFactor: 1,
|
|
91264
|
+
polygonOffsetUnits: 2,
|
|
91265
|
+
});
|
|
91266
|
+
this.occluder.onBeforeCompile = (shader) => {
|
|
91267
|
+
const clipAnchor = "#include <clipping_planes_fragment>";
|
|
91268
|
+
const opaqueAnchor = "#include <opaque_fragment>";
|
|
91269
|
+
for (const anchor of [clipAnchor, opaqueAnchor]) {
|
|
91270
|
+
if (!shader.fragmentShader.includes(anchor)) {
|
|
91271
|
+
throw new Error(`CapOutlinePass: shader anchor "${anchor}" not found`);
|
|
91272
|
+
}
|
|
91273
|
+
}
|
|
91274
|
+
shader.uniforms.uCutWidth = this.cutWidth;
|
|
91275
|
+
shader.fragmentShader =
|
|
91276
|
+
"uniform float uCutWidth;\n" +
|
|
91277
|
+
shader.fragmentShader
|
|
91278
|
+
.replace(clipAnchor, `${clipAnchor}\n${CUT_EDGE_GLSL}`)
|
|
91279
|
+
.replace(opaqueAnchor, `${opaqueAnchor}\n if ( tcvCut ) gl_FragColor = vec4( 1.0 );`);
|
|
91280
|
+
};
|
|
91281
|
+
this.composite = new ShaderMaterial({
|
|
91282
|
+
uniforms: {
|
|
91283
|
+
tId: { value: null },
|
|
91284
|
+
uTexel: { value: new Vector2(1, 1) },
|
|
91285
|
+
uWidth: { value: 1 },
|
|
91286
|
+
uColor: { value: new Color() },
|
|
91287
|
+
},
|
|
91288
|
+
vertexShader: COMPOSITE_VERTEX,
|
|
91289
|
+
fragmentShader: COMPOSITE_FRAGMENT,
|
|
91290
|
+
depthTest: false,
|
|
91291
|
+
depthWrite: false,
|
|
91292
|
+
});
|
|
91293
|
+
this.quad = new Mesh(new PlaneGeometry(2, 2), this.composite);
|
|
91294
|
+
this.quad.frustumCulled = false;
|
|
91295
|
+
this.quadScene = new Scene();
|
|
91296
|
+
this.quadScene.add(this.quad);
|
|
91297
|
+
this.quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
91298
|
+
}
|
|
91299
|
+
ensureTarget(width, height) {
|
|
91300
|
+
if (this.target === null) {
|
|
91301
|
+
this.target = new WebGLRenderTarget(width, height, {
|
|
91302
|
+
minFilter: NearestFilter,
|
|
91303
|
+
magFilter: NearestFilter,
|
|
91304
|
+
generateMipmaps: false,
|
|
91305
|
+
depthBuffer: true,
|
|
91306
|
+
stencilBuffer: true,
|
|
91307
|
+
});
|
|
91308
|
+
}
|
|
91309
|
+
else if (this.target.width !== width || this.target.height !== height) {
|
|
91310
|
+
this.target.setSize(width, height);
|
|
91311
|
+
}
|
|
91312
|
+
return this.target;
|
|
91313
|
+
}
|
|
91314
|
+
/**
|
|
91315
|
+
* Render the id pass and draw the outline over the current (canvas) framebuffer.
|
|
91316
|
+
* Call right after the main scene render.
|
|
91317
|
+
*/
|
|
91318
|
+
render(renderer, scene, camera, options) {
|
|
91319
|
+
renderer.getDrawingBufferSize(this.size);
|
|
91320
|
+
const width = Math.floor(this.size.x);
|
|
91321
|
+
const height = Math.floor(this.size.y);
|
|
91322
|
+
if (width < 1 || height < 1)
|
|
91323
|
+
return;
|
|
91324
|
+
const target = this.ensureTarget(width, height);
|
|
91325
|
+
// Line width in device pixels, shared by the cut contour and the cap outline.
|
|
91326
|
+
const lineWidth = Math.max(1, Math.round(renderer.getPixelRatio()));
|
|
91327
|
+
this.cutWidth.value = lineWidth;
|
|
91328
|
+
const savedMask = camera.layers.mask;
|
|
91329
|
+
const savedOverride = scene.overrideMaterial;
|
|
91330
|
+
const savedBackground = scene.background;
|
|
91331
|
+
const savedTarget = renderer.getRenderTarget();
|
|
91332
|
+
const savedAutoClear = renderer.autoClear;
|
|
91333
|
+
const savedAlpha = renderer.getClearAlpha();
|
|
91334
|
+
renderer.getClearColor(this.savedClearColor);
|
|
91335
|
+
renderer.getViewport(this.savedViewport);
|
|
91336
|
+
try {
|
|
91337
|
+
renderer.autoClear = false;
|
|
91338
|
+
scene.background = null;
|
|
91339
|
+
renderer.setRenderTarget(target);
|
|
91340
|
+
renderer.setClearColor(0x000000, 0); // id 0 = no cap
|
|
91341
|
+
renderer.clear(true, true, true);
|
|
91342
|
+
if (options.occlude) {
|
|
91343
|
+
this.occluder.clippingPlanes = options.planes;
|
|
91344
|
+
this.occluder.clipIntersection = options.intersection;
|
|
91345
|
+
camera.layers.set(PICK_LAYER.FACE);
|
|
91346
|
+
scene.overrideMaterial = this.occluder;
|
|
91347
|
+
renderer.render(scene, camera);
|
|
91348
|
+
scene.overrideMaterial = savedOverride;
|
|
91349
|
+
}
|
|
91350
|
+
options.setCapIdPass(true);
|
|
91351
|
+
try {
|
|
91352
|
+
camera.layers.set(CAP_LAYER);
|
|
91353
|
+
renderer.render(scene, camera);
|
|
91354
|
+
}
|
|
91355
|
+
finally {
|
|
91356
|
+
options.setCapIdPass(false);
|
|
91357
|
+
}
|
|
91358
|
+
renderer.setRenderTarget(savedTarget);
|
|
91359
|
+
renderer.setViewport(this.savedViewport);
|
|
91360
|
+
const u = this.composite.uniforms;
|
|
91361
|
+
u.tId.value = target.texture;
|
|
91362
|
+
u.uTexel.value.set(1 / width, 1 / height);
|
|
91363
|
+
u.uWidth.value = lineWidth;
|
|
91364
|
+
u.uColor.value.set(options.color);
|
|
91365
|
+
renderer.render(this.quadScene, this.quadCamera);
|
|
91366
|
+
}
|
|
91367
|
+
finally {
|
|
91368
|
+
camera.layers.mask = savedMask;
|
|
91369
|
+
scene.overrideMaterial = savedOverride;
|
|
91370
|
+
scene.background = savedBackground;
|
|
91371
|
+
renderer.setRenderTarget(savedTarget);
|
|
91372
|
+
renderer.setClearColor(this.savedClearColor, savedAlpha);
|
|
91373
|
+
renderer.setViewport(this.savedViewport);
|
|
91374
|
+
renderer.autoClear = savedAutoClear;
|
|
91375
|
+
}
|
|
91376
|
+
}
|
|
91377
|
+
dispose() {
|
|
91378
|
+
this.target?.dispose();
|
|
91379
|
+
this.target = null;
|
|
91380
|
+
this.occluder.dispose();
|
|
91381
|
+
this.composite.dispose();
|
|
91382
|
+
this.quad.geometry.dispose();
|
|
91383
|
+
}
|
|
91384
|
+
}
|
|
91385
|
+
|
|
90971
91386
|
// ============================================================================
|
|
90972
91387
|
// Constants
|
|
90973
91388
|
// ============================================================================
|
|
@@ -91220,6 +91635,9 @@ float metalnessFactor = metalness;
|
|
|
91220
91635
|
super();
|
|
91221
91636
|
/** Per-solid stencil/cap units, the unit of screen-size culling. */
|
|
91222
91637
|
this._capUnits = [];
|
|
91638
|
+
/** Shared uniforms of the section hatching on all cap quads. */
|
|
91639
|
+
this._hatchUniforms = createHatchUniforms(1);
|
|
91640
|
+
this._hatchCenter = new Vector3();
|
|
91223
91641
|
/**
|
|
91224
91642
|
* Whether {@link cull} last ran with clipping active. Lets the inactive path
|
|
91225
91643
|
* gate stencils/caps off exactly once, then early-return on later still frames.
|
|
@@ -91283,6 +91701,10 @@ float metalnessFactor = metalness;
|
|
|
91283
91701
|
mesh.material.visible = flag;
|
|
91284
91702
|
}
|
|
91285
91703
|
};
|
|
91704
|
+
/** Switch all cap materials between shaded output and outline id output. */
|
|
91705
|
+
this.setCapIdPass = (flag) => {
|
|
91706
|
+
this._hatchUniforms.uCapIdPass.value = flag ? 1 : 0;
|
|
91707
|
+
};
|
|
91286
91708
|
this.center = center;
|
|
91287
91709
|
this.distance = size / 2;
|
|
91288
91710
|
this.onNormalChange = options.onNormalChange || null;
|
|
@@ -91345,11 +91767,16 @@ float metalnessFactor = metalness;
|
|
|
91345
91767
|
_createStencils(center, size, theme) {
|
|
91346
91768
|
this._planeMeshGroup = new PlaneMeshGroup();
|
|
91347
91769
|
this._planeMeshGroup.name = "PlaneMeshes";
|
|
91770
|
+
// Cap quads are PlaneGeometry(2, 2) scaled by size/2 (PlaneMesh.updateMatrixWorld).
|
|
91771
|
+
this._hatchUniforms.uCapHalfSize.value = 0.5 * size;
|
|
91772
|
+
this._hatchCenter.set(center[0], center[1], center[2]);
|
|
91348
91773
|
// Group the per-(solid,plane) units by solid for screen-size culling. The
|
|
91349
91774
|
// loop below is plane-major (and `objectColors`/`_planeMeshGroup` order must
|
|
91350
91775
|
// stay plane-major for setObjectColorCaps), so accumulate into a Map keyed by
|
|
91351
91776
|
// solid and flatten afterwards — without touching the plane-major structures.
|
|
91352
91777
|
const unitsBySolid = new Map();
|
|
91778
|
+
// Unique id per cap quad (plane × solid) for the cap outline id pass; 0 = none.
|
|
91779
|
+
let capId = 0;
|
|
91353
91780
|
for (let i = 0; i < 3; i++) {
|
|
91354
91781
|
const plane = this.clipPlanes[i];
|
|
91355
91782
|
const otherPlanes = this.clipPlanes.filter((_, j) => j !== i);
|
|
@@ -91369,9 +91796,16 @@ float metalnessFactor = metalness;
|
|
|
91369
91796
|
clippingGroup.add(createStencil(`frontStencil-${i}-${j}`, ClippingMaterials.createFrontStencilMaterial(), group.shapeGeometry, plane));
|
|
91370
91797
|
clippingGroup.add(createStencil(`backStencil-${i}-${j}`, ClippingMaterials.createBackStencilMaterial(), group.shapeGeometry, plane));
|
|
91371
91798
|
group.addClipping(clippingGroup, i);
|
|
91799
|
+
// The outline id pass renders the stencil sequence + caps on CAP_LAYER.
|
|
91800
|
+
for (const stencil of clippingGroup.children) {
|
|
91801
|
+
stencil.layers.enable(CAP_LAYER);
|
|
91802
|
+
}
|
|
91372
91803
|
// Create stencil plane mesh
|
|
91373
91804
|
const planeMaterial = ClippingMaterials.createStencilPlaneMaterial(PLANE_COLORS[theme][i], otherPlanes);
|
|
91805
|
+
capId++;
|
|
91806
|
+
patchHatchMaterial(planeMaterial, this._hatchUniforms, j, capIdColor(capId));
|
|
91374
91807
|
const capMesh = new PlaneMesh(i, plane, center, size, planeMaterial, PLANE_COLORS[theme][i], `StencilPlane-${i}-${j}`);
|
|
91808
|
+
capMesh.layers.enable(CAP_LAYER);
|
|
91375
91809
|
this._planeMeshGroup.add(capMesh);
|
|
91376
91810
|
// Record the cull unit for this solid (one entry per solid, holding
|
|
91377
91811
|
// its up-to-3 per-plane stencil groups + cap quads).
|
|
@@ -91436,6 +91870,10 @@ float metalnessFactor = metalness;
|
|
|
91436
91870
|
this.clipPlanes[index].setConstant(value);
|
|
91437
91871
|
this.reverseClipPlanes[index].setConstant(-value);
|
|
91438
91872
|
}
|
|
91873
|
+
/** Whether any solid has clip caps (the cap outline pass has work to do). */
|
|
91874
|
+
get hasCaps() {
|
|
91875
|
+
return this._capUnits.length > 0;
|
|
91876
|
+
}
|
|
91439
91877
|
/**
|
|
91440
91878
|
* Bound the per-frame stencil/cap draw work to keep large assemblies from
|
|
91441
91879
|
* overrunning the GPU watchdog on clip+rotate (see {@link CAP_CULL_MIN_PX}).
|
|
@@ -91470,6 +91908,9 @@ float metalnessFactor = metalness;
|
|
|
91470
91908
|
return;
|
|
91471
91909
|
}
|
|
91472
91910
|
this._cullActive = true;
|
|
91911
|
+
// Keep the hatch spacing constant on screen (planes pass near the model center).
|
|
91912
|
+
this._hatchUniforms.uHatchSpacing.value =
|
|
91913
|
+
HATCH_SPACING_PX * worldPerPixel(camera, this._hatchCenter, height);
|
|
91473
91914
|
// Camera right vector in world space = column 0 of the camera world matrix.
|
|
91474
91915
|
camera.updateMatrixWorld();
|
|
91475
91916
|
const e = camera.matrixWorld.elements;
|
|
@@ -91828,6 +92269,20 @@ float metalnessFactor = metalness;
|
|
|
91828
92269
|
this.tracks = [];
|
|
91829
92270
|
this.root = null;
|
|
91830
92271
|
}
|
|
92272
|
+
/**
|
|
92273
|
+
* Apply the current animation time to the objects without advancing it, so a
|
|
92274
|
+
* single on-demand render shows the pose (e.g. after {@link setRelativeTime}).
|
|
92275
|
+
*/
|
|
92276
|
+
apply() {
|
|
92277
|
+
this.mixer?.update(0);
|
|
92278
|
+
}
|
|
92279
|
+
/**
|
|
92280
|
+
* Restart the frame-time measurement, so resuming playback after a pause does
|
|
92281
|
+
* not advance the animation by the whole pause.
|
|
92282
|
+
*/
|
|
92283
|
+
resetClock() {
|
|
92284
|
+
this.clock.reset();
|
|
92285
|
+
}
|
|
91831
92286
|
/**
|
|
91832
92287
|
* Update the animation mixer (call each frame when animating).
|
|
91833
92288
|
*/
|
|
@@ -97399,7 +97854,7 @@ float metalnessFactor = metalness;
|
|
|
97399
97854
|
}
|
|
97400
97855
|
}
|
|
97401
97856
|
|
|
97402
|
-
const version = "5.
|
|
97857
|
+
const version = "5.1.1";
|
|
97403
97858
|
|
|
97404
97859
|
/**
|
|
97405
97860
|
* `PickedComponent` over a GPU id-pick result. Drives the shader
|
|
@@ -97625,6 +98080,58 @@ float metalnessFactor = metalness;
|
|
|
97625
98080
|
this.setSelectionInput(false);
|
|
97626
98081
|
this.setPickHandler(false);
|
|
97627
98082
|
}
|
|
98083
|
+
/**
|
|
98084
|
+
* Pick the visible component under the cursor with the current topo filter, or
|
|
98085
|
+
* `null` when the cursor is off the canvas or over background.
|
|
98086
|
+
*/
|
|
98087
|
+
pickUnderCursor() {
|
|
98088
|
+
if (this.host.idPicker === null || !this.idHoverInside)
|
|
98089
|
+
return null;
|
|
98090
|
+
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
98091
|
+
const x = this.idHoverClientX - rect.left;
|
|
98092
|
+
const y = this.idHoverClientY - rect.top;
|
|
98093
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height)
|
|
98094
|
+
return null;
|
|
98095
|
+
const topoFilter = pickerTopoFilter(this.host.display.shapeFilterDropDownMenu.currentFilter);
|
|
98096
|
+
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
98097
|
+
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
98098
|
+
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
98099
|
+
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
98100
|
+
if (hit === null || !this.pickVisible(hit.info))
|
|
98101
|
+
return null;
|
|
98102
|
+
return hit;
|
|
98103
|
+
}
|
|
98104
|
+
/**
|
|
98105
|
+
* World-space point on the visible component under the cursor (same pick as the
|
|
98106
|
+
* hover), or `null` when there is none or the GPU lacks the position attachment.
|
|
98107
|
+
*/
|
|
98108
|
+
pointUnderCursor() {
|
|
98109
|
+
if (!this.host.ready)
|
|
98110
|
+
return null;
|
|
98111
|
+
const hit = this.pickUnderCursor();
|
|
98112
|
+
if (hit === null || hit.point === null)
|
|
98113
|
+
return null;
|
|
98114
|
+
return hit.point.clone();
|
|
98115
|
+
}
|
|
98116
|
+
/**
|
|
98117
|
+
* Tree-row hover: HOVER the faces of the tree node at `path` (leaf or group), or
|
|
98118
|
+
* clear when `null`. Drops the canvas hover target first — otherwise the next
|
|
98119
|
+
* render's {@link handleIdHover} (cursor outside the canvas) would release that
|
|
98120
|
+
* stale target and wipe the tree hover with it.
|
|
98121
|
+
*/
|
|
98122
|
+
setTreeHover(path) {
|
|
98123
|
+
if (!this.host.ready || this.host.studioActive)
|
|
98124
|
+
return;
|
|
98125
|
+
const highlight = this.host.rendered.nestedGroup?.highlight ?? null;
|
|
98126
|
+
if (highlight === null)
|
|
98127
|
+
return;
|
|
98128
|
+
this.releaseLastSelected();
|
|
98129
|
+
this.lastObject = null;
|
|
98130
|
+
this.host.display.setStatusLine("");
|
|
98131
|
+
highlight.setHoverPath(path);
|
|
98132
|
+
if (!this.host.hasAnimationLoop)
|
|
98133
|
+
this.host.update(true, false);
|
|
98134
|
+
}
|
|
97628
98135
|
/**
|
|
97629
98136
|
* Whether hover preselection (highlight + status line) is active. Disabled for GDS:
|
|
97630
98137
|
* dense, stacked, instance-unrolled layout data where per-pixel hover flickers
|
|
@@ -97662,28 +98169,12 @@ float metalnessFactor = metalness;
|
|
|
97662
98169
|
this.lastObject = null;
|
|
97663
98170
|
this.host.display.setStatusLine("");
|
|
97664
98171
|
};
|
|
97665
|
-
|
|
97666
|
-
|
|
97667
|
-
return;
|
|
97668
|
-
}
|
|
97669
|
-
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
97670
|
-
const x = this.idHoverClientX - rect.left;
|
|
97671
|
-
const y = this.idHoverClientY - rect.top;
|
|
97672
|
-
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
97673
|
-
release();
|
|
97674
|
-
return;
|
|
97675
|
-
}
|
|
97676
|
-
const filter = this.host.display.shapeFilterDropDownMenu.currentFilter;
|
|
97677
|
-
const fromSolid = filter.includes(TopoFilter.solid);
|
|
97678
|
-
const topoFilter = pickerTopoFilter(filter);
|
|
97679
|
-
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
97680
|
-
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
97681
|
-
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
97682
|
-
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
97683
|
-
if (hit === null || !this.pickVisible(hit.info)) {
|
|
98172
|
+
const hit = this.pickUnderCursor();
|
|
98173
|
+
if (hit === null) {
|
|
97684
98174
|
release();
|
|
97685
98175
|
return;
|
|
97686
98176
|
}
|
|
98177
|
+
const fromSolid = this.host.display.shapeFilterDropDownMenu.currentFilter.includes(TopoFilter.solid);
|
|
97687
98178
|
const picked = new IdPicked(hit.info, fromSolid, hit.point, highlight);
|
|
97688
98179
|
if (!picked.equals(this.lastObject)) {
|
|
97689
98180
|
this.releaseLastSelected();
|
|
@@ -108864,6 +109355,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108864
109355
|
// =============================================================================
|
|
108865
109356
|
// IMPORTS
|
|
108866
109357
|
// =============================================================================
|
|
109358
|
+
/** Decimals of a point copied with Ctrl/Cmd-C ({@link Viewer.copyPointUnderCursor}). */
|
|
109359
|
+
const COPY_POINT_DECIMALS = 4;
|
|
109360
|
+
/** Delay after the last camera change before the clip cap outline is drawn again. */
|
|
109361
|
+
const CAP_OUTLINE_SETTLE_MS = 150;
|
|
108867
109362
|
/**
|
|
108868
109363
|
* Type guard to check if an Object3D is an IndexedMesh.
|
|
108869
109364
|
*/
|
|
@@ -108954,6 +109449,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108954
109449
|
* @param updateMarker - enforce to redraw orientation marker after every ui activity
|
|
108955
109450
|
*/
|
|
108956
109451
|
constructor(display, options, notifyCallback, pinAsPngCallback = null, updateMarker = true) {
|
|
109452
|
+
/** Reasons for continuous rendering; the render loop runs while any is set. */
|
|
109453
|
+
this._loopReasons = new Set();
|
|
108957
109454
|
// Hide-undo stack: each meta-double-click hide pushes the leaf id + its pre-hide
|
|
108958
109455
|
// state; meta-double-click on empty space pops and restores the last one. Lets a
|
|
108959
109456
|
// hidden object be brought back without the tree (e.g. in Studio, where it's hidden).
|
|
@@ -108974,6 +109471,14 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108974
109471
|
// GPU id-based picker. Created/attached per render() once the scene + compact
|
|
108975
109472
|
// registry exist; drives hover, selection, measure and double-click pick.
|
|
108976
109473
|
this.idPicker = null;
|
|
109474
|
+
/** Outline of the clip caps; created on first use, kept across renders. */
|
|
109475
|
+
this._capOutline = null;
|
|
109476
|
+
/** Camera world + projection matrix of the previous outline frame (motion test). */
|
|
109477
|
+
this._capOutlineCamera = [];
|
|
109478
|
+
/** Pending redraw that adds the outline once the camera has settled. */
|
|
109479
|
+
this._capOutlineSettle = null;
|
|
109480
|
+
/** Whether the attached animation has moved parts (Play pressed or time slider moved). */
|
|
109481
|
+
this._animationStarted = false;
|
|
108977
109482
|
// ---------------------------------------------------------------------------
|
|
108978
109483
|
// Render Loop & Scene Updates
|
|
108979
109484
|
// ---------------------------------------------------------------------------
|
|
@@ -109122,6 +109627,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109122
109627
|
}
|
|
109123
109628
|
else {
|
|
109124
109629
|
this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
|
|
109630
|
+
this._renderCapOutline();
|
|
109125
109631
|
}
|
|
109126
109632
|
this.cadTools.update();
|
|
109127
109633
|
this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
|
|
@@ -109375,16 +109881,20 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109375
109881
|
return;
|
|
109376
109882
|
switch (btn) {
|
|
109377
109883
|
case "play":
|
|
109884
|
+
this._animationStarted = true;
|
|
109378
109885
|
if (this.clipAction.paused) {
|
|
109379
109886
|
this.clipAction.paused = false;
|
|
109380
109887
|
}
|
|
109381
109888
|
this.clipAction.play();
|
|
109889
|
+
this._setAnimationPlaying(true);
|
|
109382
109890
|
break;
|
|
109383
109891
|
case "pause":
|
|
109384
109892
|
this.clipAction.paused = !this.clipAction.paused;
|
|
109893
|
+
this._setAnimationPlaying(!this.clipAction.paused && this.clipAction.isRunning());
|
|
109385
109894
|
break;
|
|
109386
109895
|
case "stop":
|
|
109387
109896
|
this.clipAction.stop();
|
|
109897
|
+
this._setAnimationPlaying(false);
|
|
109388
109898
|
break;
|
|
109389
109899
|
}
|
|
109390
109900
|
};
|
|
@@ -109508,6 +110018,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109508
110018
|
return;
|
|
109509
110019
|
}
|
|
109510
110020
|
};
|
|
110021
|
+
/** Tree-row hover: highlight the faces of the node at `path`, or clear on `null`. */
|
|
110022
|
+
this.handleTreeHover = (path) => {
|
|
110023
|
+
this.pickingController.setTreeHover(path);
|
|
110024
|
+
};
|
|
109511
110025
|
/** Clear the current selection (and reset the select tool). */
|
|
109512
110026
|
this.clearSelection = () => {
|
|
109513
110027
|
this.pickingController.clearSelection();
|
|
@@ -110270,10 +110784,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110270
110784
|
return Promise.resolve({ task: taskId, dataUrl: null });
|
|
110271
110785
|
}
|
|
110272
110786
|
// canvas.toBlob can be very slow when animation loop is off!
|
|
110273
|
-
|
|
110274
|
-
if (!animationLoop) {
|
|
110275
|
-
this.toggleAnimationLoop(true);
|
|
110276
|
-
}
|
|
110787
|
+
this.setLoopReason("capture", true);
|
|
110277
110788
|
this.rendered.orientationMarker.setVisible(false);
|
|
110278
110789
|
this.update(true);
|
|
110279
110790
|
return this.display.captureCanvas({
|
|
@@ -110291,10 +110802,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110291
110802
|
}
|
|
110292
110803
|
},
|
|
110293
110804
|
onComplete: () => {
|
|
110294
|
-
// Restore
|
|
110295
|
-
|
|
110296
|
-
this.toggleAnimationLoop(false);
|
|
110297
|
-
}
|
|
110805
|
+
// Restore the loop to what the other reasons need
|
|
110806
|
+
this.setLoopReason("capture", false);
|
|
110298
110807
|
this.rendered.orientationMarker.setVisible(true);
|
|
110299
110808
|
this.update(true);
|
|
110300
110809
|
},
|
|
@@ -110692,13 +111201,13 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110692
111201
|
return;
|
|
110693
111202
|
}
|
|
110694
111203
|
logger.debug("Animation initialized");
|
|
110695
|
-
if (!this.hasAnimationLoop) {
|
|
110696
|
-
this.toggleAnimationLoop(true);
|
|
110697
|
-
}
|
|
110698
111204
|
this.state.set("animationMode", label === "E" ? "explode" : "animation");
|
|
111205
|
+
this._animationStarted = false;
|
|
110699
111206
|
this.clipAction = this.animation.animate(this.rendered.nestedGroup.rootGroup, duration, speed, repeat);
|
|
110700
111207
|
// Reset animation slider to start
|
|
110701
111208
|
this.state.set("animationSliderValue", 0);
|
|
111209
|
+
// Not playing yet: render on demand only (the loop starts with Play).
|
|
111210
|
+
this._setAnimationPlaying(false);
|
|
110702
111211
|
}
|
|
110703
111212
|
/**
|
|
110704
111213
|
* Check whether animation object exists
|
|
@@ -110714,7 +111223,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110714
111223
|
deepDispose(this.animation);
|
|
110715
111224
|
}
|
|
110716
111225
|
this.state.set("animationMode", "none");
|
|
110717
|
-
this.
|
|
111226
|
+
this._animationStarted = false;
|
|
111227
|
+
this.setLoopReason("animation", false);
|
|
110718
111228
|
}
|
|
110719
111229
|
/**
|
|
110720
111230
|
* Set the animation to a specific relative time (0-1).
|
|
@@ -110722,8 +111232,12 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110722
111232
|
* @param fraction - relative time between 0 and 1.
|
|
110723
111233
|
*/
|
|
110724
111234
|
setRelativeTime(fraction) {
|
|
111235
|
+
if (fraction !== 0)
|
|
111236
|
+
this._animationStarted = true;
|
|
110725
111237
|
this.animation.setRelativeTime(fraction);
|
|
110726
111238
|
this.state.set("animationSliderValue", fraction * 1000);
|
|
111239
|
+
// Setting a time pauses the animation: stop the loop and show the new pose.
|
|
111240
|
+
this._setAnimationPlaying(false);
|
|
110727
111241
|
}
|
|
110728
111242
|
/**
|
|
110729
111243
|
* Get the current relative animation time (0-1).
|
|
@@ -110732,6 +111246,44 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110732
111246
|
getRelativeTime() {
|
|
110733
111247
|
return this.animation.getRelativeTime();
|
|
110734
111248
|
}
|
|
111249
|
+
/**
|
|
111250
|
+
* Add or remove a reason for continuous rendering. The render loop runs while at
|
|
111251
|
+
* least one reason is set; otherwise the viewer renders on demand (camera changes
|
|
111252
|
+
* and explicit updates), so an idle viewer costs no CPU/GPU time.
|
|
111253
|
+
* @param reason - why continuous rendering is needed.
|
|
111254
|
+
* @param flag - whether the reason applies.
|
|
111255
|
+
*/
|
|
111256
|
+
setLoopReason(reason, flag) {
|
|
111257
|
+
if (flag) {
|
|
111258
|
+
this._loopReasons.add(reason);
|
|
111259
|
+
}
|
|
111260
|
+
else {
|
|
111261
|
+
this._loopReasons.delete(reason);
|
|
111262
|
+
}
|
|
111263
|
+
const run = this._loopReasons.size > 0;
|
|
111264
|
+
if (run !== this.hasAnimationLoop)
|
|
111265
|
+
this.toggleAnimationLoop(run);
|
|
111266
|
+
}
|
|
111267
|
+
/**
|
|
111268
|
+
* Run the render loop only while the animation plays. When it stops (pause,
|
|
111269
|
+
* stop, slider), render once so the final pose is shown.
|
|
111270
|
+
*/
|
|
111271
|
+
_setAnimationPlaying(playing) {
|
|
111272
|
+
if (playing) {
|
|
111273
|
+
// Resume without jumping ahead by the time spent paused.
|
|
111274
|
+
this.animation.resetClock();
|
|
111275
|
+
}
|
|
111276
|
+
this.setLoopReason("animation", playing);
|
|
111277
|
+
if (!playing && this._rendered !== null) {
|
|
111278
|
+
this.animation.apply();
|
|
111279
|
+
this.update(true, false);
|
|
111280
|
+
}
|
|
111281
|
+
}
|
|
111282
|
+
/**
|
|
111283
|
+
* Start or stop the render loop directly. Prefer {@link setLoopReason}, which keeps
|
|
111284
|
+
* the loop running while any other reason still needs it.
|
|
111285
|
+
* @param flag - whether the render loop should run.
|
|
111286
|
+
*/
|
|
110735
111287
|
toggleAnimationLoop(flag) {
|
|
110736
111288
|
if (flag) {
|
|
110737
111289
|
this.continueAnimation = true;
|
|
@@ -110753,6 +111305,60 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110753
111305
|
setTimeout(() => this.update(true, true), 50);
|
|
110754
111306
|
}
|
|
110755
111307
|
}
|
|
111308
|
+
/**
|
|
111309
|
+
* Draw the outline of the clip caps over the frame (Clip tab active only). Skipped
|
|
111310
|
+
* while the camera moves (drag, wheel, damping, preset views): the outline pass
|
|
111311
|
+
* renders faces and caps a second time, so it is drawn once the camera has been
|
|
111312
|
+
* still for {@link CAP_OUTLINE_SETTLE_MS}.
|
|
111313
|
+
*/
|
|
111314
|
+
_renderCapOutline() {
|
|
111315
|
+
const clipping = this.rendered.clipping;
|
|
111316
|
+
if (!this.renderer.localClippingEnabled || !clipping.hasCaps)
|
|
111317
|
+
return;
|
|
111318
|
+
const camera = this.rendered.camera.getCamera();
|
|
111319
|
+
if (this._cameraMovedSinceLastOutline(camera)) {
|
|
111320
|
+
this._scheduleCapOutline();
|
|
111321
|
+
return;
|
|
111322
|
+
}
|
|
111323
|
+
if (this._capOutline === null)
|
|
111324
|
+
this._capOutline = new CapOutlinePass();
|
|
111325
|
+
const blackEdges = this.state.get("blackEdges") === true;
|
|
111326
|
+
this._capOutline.render(this.renderer, this.rendered.scene, camera, {
|
|
111327
|
+
planes: clipping.clipPlanes,
|
|
111328
|
+
intersection: this.state.get("clipIntersection") === true,
|
|
111329
|
+
occlude: this.state.get("transparent") !== true,
|
|
111330
|
+
color: blackEdges ? 0x000000 : this.state.get("edgeColor"),
|
|
111331
|
+
setCapIdPass: clipping.setCapIdPass,
|
|
111332
|
+
});
|
|
111333
|
+
}
|
|
111334
|
+
/** Whether the camera's world or projection matrix changed since the last call. */
|
|
111335
|
+
_cameraMovedSinceLastOutline(camera) {
|
|
111336
|
+
const now = [
|
|
111337
|
+
...camera.matrixWorld.elements,
|
|
111338
|
+
...camera.projectionMatrix.elements,
|
|
111339
|
+
];
|
|
111340
|
+
const last = this._capOutlineCamera;
|
|
111341
|
+
const moved = last.length !== now.length || now.some((value, i) => value !== last[i]);
|
|
111342
|
+
this._capOutlineCamera = now;
|
|
111343
|
+
return moved;
|
|
111344
|
+
}
|
|
111345
|
+
/** Redraw once the camera has settled, so the skipped outline appears. */
|
|
111346
|
+
_scheduleCapOutline() {
|
|
111347
|
+
if (this._capOutlineSettle !== null)
|
|
111348
|
+
clearTimeout(this._capOutlineSettle);
|
|
111349
|
+
this._capOutlineSettle = setTimeout(() => {
|
|
111350
|
+
this._capOutlineSettle = null;
|
|
111351
|
+
if (this.ready)
|
|
111352
|
+
this.update(true, false);
|
|
111353
|
+
}, CAP_OUTLINE_SETTLE_MS);
|
|
111354
|
+
}
|
|
111355
|
+
/** Cancel a pending outline redraw. */
|
|
111356
|
+
_cancelCapOutline() {
|
|
111357
|
+
if (this._capOutlineSettle !== null)
|
|
111358
|
+
clearTimeout(this._capOutlineSettle);
|
|
111359
|
+
this._capOutlineSettle = null;
|
|
111360
|
+
this._capOutlineCamera = [];
|
|
111361
|
+
}
|
|
110756
111362
|
// ---------------------------------------------------------------------------
|
|
110757
111363
|
// Cleanup & Disposal
|
|
110758
111364
|
// ---------------------------------------------------------------------------
|
|
@@ -110771,6 +111377,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110771
111377
|
*/
|
|
110772
111378
|
dispose() {
|
|
110773
111379
|
this.clear();
|
|
111380
|
+
this._capOutline?.dispose();
|
|
111381
|
+
this._capOutline = null;
|
|
110774
111382
|
// Remove all picking listeners (they hold a strong ref to this Viewer via the
|
|
110775
111383
|
// controller's arrow-field handlers; in external-canvas mode the caller owns the
|
|
110776
111384
|
// canvas, so not removing them would leak the disposed Viewer).
|
|
@@ -110816,6 +111424,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110816
111424
|
* @public
|
|
110817
111425
|
*/
|
|
110818
111426
|
clear() {
|
|
111427
|
+
this._cancelCapOutline();
|
|
110819
111428
|
if (this._rendered) {
|
|
110820
111429
|
// Drop selection state + hover cache + status line — a PickedComponent may hold
|
|
110821
111430
|
// a HighlightController / ObjectGroup that this clear() disposes; a stale
|
|
@@ -110825,6 +111434,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110825
111434
|
// Hide the topo filter (+ detach its shortcuts) for a clean cleared canvas.
|
|
110826
111435
|
this.display.shapeFilterDropDownMenu.show(false);
|
|
110827
111436
|
// stop animation
|
|
111437
|
+
this._loopReasons.clear();
|
|
110828
111438
|
this.hasAnimationLoop = false;
|
|
110829
111439
|
this.continueAnimation = false;
|
|
110830
111440
|
// remove change listener if exists
|
|
@@ -110937,7 +111547,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110937
111547
|
if (!this.tree) {
|
|
110938
111548
|
throw new Error("Tree not initialized");
|
|
110939
111549
|
}
|
|
110940
|
-
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);
|
|
111550
|
+
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);
|
|
110941
111551
|
this.display.clearCadTree();
|
|
110942
111552
|
const t = treeview.create();
|
|
110943
111553
|
timer.split("created tree");
|
|
@@ -111403,6 +112013,40 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111403
112013
|
get lastSelection() {
|
|
111404
112014
|
return this.pickingController.lastSelection;
|
|
111405
112015
|
}
|
|
112016
|
+
/**
|
|
112017
|
+
* Copy the world-space point under the cursor as `"x, y, z"` to the clipboard and
|
|
112018
|
+
* send it to the host as a `copiedPoint` notification (for hosts that block the
|
|
112019
|
+
* browser clipboard). Does nothing while parts may be displaced from their model
|
|
112020
|
+
* positions — explode mode, an animation once started (Play or time slider), a
|
|
112021
|
+
* z-scale other than 1 — or when no visible component is under the cursor.
|
|
112022
|
+
* @returns true when a point was copied (the caller then consumes the key event).
|
|
112023
|
+
*/
|
|
112024
|
+
copyPointUnderCursor() {
|
|
112025
|
+
if (!this.ready)
|
|
112026
|
+
return false;
|
|
112027
|
+
const mode = this.state.get("animationMode");
|
|
112028
|
+
if (mode === "explode")
|
|
112029
|
+
return false;
|
|
112030
|
+
if (mode === "animation" && this._animationStarted)
|
|
112031
|
+
return false;
|
|
112032
|
+
if (this.zScale !== undefined && this.zScale !== 1)
|
|
112033
|
+
return false;
|
|
112034
|
+
const point = this.pickingController.pointUnderCursor();
|
|
112035
|
+
if (point === null)
|
|
112036
|
+
return false;
|
|
112037
|
+
const coords = toVector3Tuple(point.toArray());
|
|
112038
|
+
const fmt = (v) => {
|
|
112039
|
+
const s = v.toFixed(COPY_POINT_DECIMALS);
|
|
112040
|
+
return Number(s) === 0 ? (0).toFixed(COPY_POINT_DECIMALS) : s;
|
|
112041
|
+
};
|
|
112042
|
+
const text = coords.map(fmt).join(", ");
|
|
112043
|
+
copyText(text, this.display.container);
|
|
112044
|
+
if (this.notifyCallback !== null) {
|
|
112045
|
+
this.notifyCallback({ copiedPoint: { old: null, new: coords } });
|
|
112046
|
+
}
|
|
112047
|
+
this.display.setStatusLine(`copied: ${text}`);
|
|
112048
|
+
return true;
|
|
112049
|
+
}
|
|
111406
112050
|
/** Enable/disable the double-click pick handler (on when no tool is active). */
|
|
111407
112051
|
setPickHandler(flag) {
|
|
111408
112052
|
this.pickingController.setPickHandler(flag);
|
|
@@ -111817,7 +112461,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111817
112461
|
this.tree = this.compactTree;
|
|
111818
112462
|
// Dispose old treeview and create new one
|
|
111819
112463
|
deepDispose(this.rendered.treeview);
|
|
111820
|
-
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);
|
|
112464
|
+
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);
|
|
111821
112465
|
this.rendered.treeview = treeview;
|
|
111822
112466
|
this.display.clearCadTree();
|
|
111823
112467
|
const t = treeview.create();
|
|
@@ -113376,6 +114020,33 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
113376
114020
|
this._matEditorDragAbort = null;
|
|
113377
114021
|
this._matEditorInputAbort = null;
|
|
113378
114022
|
this._animWasVisible = false;
|
|
114023
|
+
/**
|
|
114024
|
+
* Ctrl/Cmd-C while the cursor is over the canvas copies the point under it.
|
|
114025
|
+
* Text-entry fields and selected text keep the normal copy.
|
|
114026
|
+
*/
|
|
114027
|
+
this._handleCopyPoint = (e) => {
|
|
114028
|
+
if (!(e instanceof KeyboardEvent))
|
|
114029
|
+
return;
|
|
114030
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey)
|
|
114031
|
+
return;
|
|
114032
|
+
if (e.key.toLowerCase() !== "c")
|
|
114033
|
+
return;
|
|
114034
|
+
const target = e.target;
|
|
114035
|
+
if ((target instanceof HTMLInputElement &&
|
|
114036
|
+
target.type !== "button" &&
|
|
114037
|
+
target.type !== "checkbox") ||
|
|
114038
|
+
target instanceof HTMLTextAreaElement ||
|
|
114039
|
+
(target instanceof HTMLElement && target.isContentEditable)) {
|
|
114040
|
+
return;
|
|
114041
|
+
}
|
|
114042
|
+
const selection = window.getSelection();
|
|
114043
|
+
if (selection !== null && selection.toString() !== "")
|
|
114044
|
+
return;
|
|
114045
|
+
if (this.viewer.copyPointUnderCursor()) {
|
|
114046
|
+
e.preventDefault();
|
|
114047
|
+
e.stopPropagation();
|
|
114048
|
+
}
|
|
114049
|
+
};
|
|
113379
114050
|
// ---------------------------------------------------------------------------
|
|
113380
114051
|
// Toolbar Button Handlers: View Settings
|
|
113381
114052
|
// ---------------------------------------------------------------------------
|
|
@@ -113458,7 +114129,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
113458
114129
|
if (flag && this.viewer.isStudioActive) {
|
|
113459
114130
|
return;
|
|
113460
114131
|
}
|
|
113461
|
-
this.viewer.
|
|
114132
|
+
this.viewer.setLoopReason("tool", flag);
|
|
113462
114133
|
if (flag) {
|
|
113463
114134
|
// Delegate state mutations to Viewer
|
|
113464
114135
|
this.viewer.activateTool(name, true);
|
|
@@ -114698,6 +115369,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
114698
115369
|
// Focus handling for keyboard shortcuts
|
|
114699
115370
|
listeners.add(this.container, "mousedown", () => this.container.focus());
|
|
114700
115371
|
listeners.add(this.container, "keydown", this._handleKeyboardShortcut);
|
|
115372
|
+
// Ctrl/Cmd-C acts on the point under the cursor, so it must not depend on the
|
|
115373
|
+
// viewer having focus (e.g. after using a host dropdown). Capture phase: runs
|
|
115374
|
+
// before host shortcuts; the key is consumed only when a point was copied.
|
|
115375
|
+
listeners.add(document, "keydown", this._handleCopyPoint, true);
|
|
114701
115376
|
}
|
|
114702
115377
|
/**
|
|
114703
115378
|
* Subscribe to ViewerState changes to keep UI in sync.
|