three-cad-viewer 5.0.7 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/picking-controller.d.ts +17 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/viewer.d.ts +32 -0
- package/dist/rendering/cap-outline.d.ts +60 -0
- package/dist/rendering/clip-style.d.ts +49 -0
- package/dist/rendering/highlight.d.ts +5 -0
- package/dist/scene/clipping.d.ts +7 -0
- package/dist/three-cad-viewer.esm.js +651 -30
- 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 +651 -30
- 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;
|
|
@@ -97393,7 +97834,7 @@ class Tools {
|
|
|
97393
97834
|
}
|
|
97394
97835
|
}
|
|
97395
97836
|
|
|
97396
|
-
const version = "5.0
|
|
97837
|
+
const version = "5.1.0";
|
|
97397
97838
|
|
|
97398
97839
|
/**
|
|
97399
97840
|
* `PickedComponent` over a GPU id-pick result. Drives the shader
|
|
@@ -97619,6 +98060,58 @@ class PickingController {
|
|
|
97619
98060
|
this.setSelectionInput(false);
|
|
97620
98061
|
this.setPickHandler(false);
|
|
97621
98062
|
}
|
|
98063
|
+
/**
|
|
98064
|
+
* Pick the visible component under the cursor with the current topo filter, or
|
|
98065
|
+
* `null` when the cursor is off the canvas or over background.
|
|
98066
|
+
*/
|
|
98067
|
+
pickUnderCursor() {
|
|
98068
|
+
if (this.host.idPicker === null || !this.idHoverInside)
|
|
98069
|
+
return null;
|
|
98070
|
+
const rect = this.host.renderer.domElement.getBoundingClientRect();
|
|
98071
|
+
const x = this.idHoverClientX - rect.left;
|
|
98072
|
+
const y = this.idHoverClientY - rect.top;
|
|
98073
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height)
|
|
98074
|
+
return null;
|
|
98075
|
+
const topoFilter = pickerTopoFilter(this.host.display.shapeFilterDropDownMenu.currentFilter);
|
|
98076
|
+
const hit = this.host.idPicker.pickAt(x, y, topoFilter === undefined ? {} : { topoFilter });
|
|
98077
|
+
// The vertex/edge pick layers stay active even when the owning solid is hidden
|
|
98078
|
+
// (visibility lives on the mesh material, not the pick layer), so gate the pick on
|
|
98079
|
+
// visibility — otherwise a hidden component could be hovered AND selected.
|
|
98080
|
+
if (hit === null || !this.pickVisible(hit.info))
|
|
98081
|
+
return null;
|
|
98082
|
+
return hit;
|
|
98083
|
+
}
|
|
98084
|
+
/**
|
|
98085
|
+
* World-space point on the visible component under the cursor (same pick as the
|
|
98086
|
+
* hover), or `null` when there is none or the GPU lacks the position attachment.
|
|
98087
|
+
*/
|
|
98088
|
+
pointUnderCursor() {
|
|
98089
|
+
if (!this.host.ready)
|
|
98090
|
+
return null;
|
|
98091
|
+
const hit = this.pickUnderCursor();
|
|
98092
|
+
if (hit === null || hit.point === null)
|
|
98093
|
+
return null;
|
|
98094
|
+
return hit.point.clone();
|
|
98095
|
+
}
|
|
98096
|
+
/**
|
|
98097
|
+
* Tree-row hover: HOVER the faces of the tree node at `path` (leaf or group), or
|
|
98098
|
+
* clear when `null`. Drops the canvas hover target first — otherwise the next
|
|
98099
|
+
* render's {@link handleIdHover} (cursor outside the canvas) would release that
|
|
98100
|
+
* stale target and wipe the tree hover with it.
|
|
98101
|
+
*/
|
|
98102
|
+
setTreeHover(path) {
|
|
98103
|
+
if (!this.host.ready || this.host.studioActive)
|
|
98104
|
+
return;
|
|
98105
|
+
const highlight = this.host.rendered.nestedGroup?.highlight ?? null;
|
|
98106
|
+
if (highlight === null)
|
|
98107
|
+
return;
|
|
98108
|
+
this.releaseLastSelected();
|
|
98109
|
+
this.lastObject = null;
|
|
98110
|
+
this.host.display.setStatusLine("");
|
|
98111
|
+
highlight.setHoverPath(path);
|
|
98112
|
+
if (!this.host.hasAnimationLoop)
|
|
98113
|
+
this.host.update(true, false);
|
|
98114
|
+
}
|
|
97622
98115
|
/**
|
|
97623
98116
|
* Whether hover preselection (highlight + status line) is active. Disabled for GDS:
|
|
97624
98117
|
* dense, stacked, instance-unrolled layout data where per-pixel hover flickers
|
|
@@ -97656,28 +98149,12 @@ class PickingController {
|
|
|
97656
98149
|
this.lastObject = null;
|
|
97657
98150
|
this.host.display.setStatusLine("");
|
|
97658
98151
|
};
|
|
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)) {
|
|
98152
|
+
const hit = this.pickUnderCursor();
|
|
98153
|
+
if (hit === null) {
|
|
97678
98154
|
release();
|
|
97679
98155
|
return;
|
|
97680
98156
|
}
|
|
98157
|
+
const fromSolid = this.host.display.shapeFilterDropDownMenu.currentFilter.includes(TopoFilter.solid);
|
|
97681
98158
|
const picked = new IdPicked(hit.info, fromSolid, hit.point, highlight);
|
|
97682
98159
|
if (!picked.equals(this.lastObject)) {
|
|
97683
98160
|
this.releaseLastSelected();
|
|
@@ -108858,6 +109335,10 @@ function decodeInstancedFormat(data) {
|
|
|
108858
109335
|
// =============================================================================
|
|
108859
109336
|
// IMPORTS
|
|
108860
109337
|
// =============================================================================
|
|
109338
|
+
/** Decimals of a point copied with Ctrl/Cmd-C ({@link Viewer.copyPointUnderCursor}). */
|
|
109339
|
+
const COPY_POINT_DECIMALS = 4;
|
|
109340
|
+
/** Delay after the last camera change before the clip cap outline is drawn again. */
|
|
109341
|
+
const CAP_OUTLINE_SETTLE_MS = 150;
|
|
108861
109342
|
/**
|
|
108862
109343
|
* Type guard to check if an Object3D is an IndexedMesh.
|
|
108863
109344
|
*/
|
|
@@ -108968,6 +109449,14 @@ class Viewer {
|
|
|
108968
109449
|
// GPU id-based picker. Created/attached per render() once the scene + compact
|
|
108969
109450
|
// registry exist; drives hover, selection, measure and double-click pick.
|
|
108970
109451
|
this.idPicker = null;
|
|
109452
|
+
/** Outline of the clip caps; created on first use, kept across renders. */
|
|
109453
|
+
this._capOutline = null;
|
|
109454
|
+
/** Camera world + projection matrix of the previous outline frame (motion test). */
|
|
109455
|
+
this._capOutlineCamera = [];
|
|
109456
|
+
/** Pending redraw that adds the outline once the camera has settled. */
|
|
109457
|
+
this._capOutlineSettle = null;
|
|
109458
|
+
/** Whether the attached animation has moved parts (Play pressed or time slider moved). */
|
|
109459
|
+
this._animationStarted = false;
|
|
108971
109460
|
// ---------------------------------------------------------------------------
|
|
108972
109461
|
// Render Loop & Scene Updates
|
|
108973
109462
|
// ---------------------------------------------------------------------------
|
|
@@ -109116,6 +109605,7 @@ class Viewer {
|
|
|
109116
109605
|
}
|
|
109117
109606
|
else {
|
|
109118
109607
|
this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
|
|
109608
|
+
this._renderCapOutline();
|
|
109119
109609
|
}
|
|
109120
109610
|
this.cadTools.update();
|
|
109121
109611
|
this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
|
|
@@ -109369,6 +109859,7 @@ class Viewer {
|
|
|
109369
109859
|
return;
|
|
109370
109860
|
switch (btn) {
|
|
109371
109861
|
case "play":
|
|
109862
|
+
this._animationStarted = true;
|
|
109372
109863
|
if (this.clipAction.paused) {
|
|
109373
109864
|
this.clipAction.paused = false;
|
|
109374
109865
|
}
|
|
@@ -109502,6 +109993,10 @@ class Viewer {
|
|
|
109502
109993
|
return;
|
|
109503
109994
|
}
|
|
109504
109995
|
};
|
|
109996
|
+
/** Tree-row hover: highlight the faces of the node at `path`, or clear on `null`. */
|
|
109997
|
+
this.handleTreeHover = (path) => {
|
|
109998
|
+
this.pickingController.setTreeHover(path);
|
|
109999
|
+
};
|
|
109505
110000
|
/** Clear the current selection (and reset the select tool). */
|
|
109506
110001
|
this.clearSelection = () => {
|
|
109507
110002
|
this.pickingController.clearSelection();
|
|
@@ -110690,6 +111185,7 @@ class Viewer {
|
|
|
110690
111185
|
this.toggleAnimationLoop(true);
|
|
110691
111186
|
}
|
|
110692
111187
|
this.state.set("animationMode", label === "E" ? "explode" : "animation");
|
|
111188
|
+
this._animationStarted = false;
|
|
110693
111189
|
this.clipAction = this.animation.animate(this.rendered.nestedGroup.rootGroup, duration, speed, repeat);
|
|
110694
111190
|
// Reset animation slider to start
|
|
110695
111191
|
this.state.set("animationSliderValue", 0);
|
|
@@ -110708,6 +111204,7 @@ class Viewer {
|
|
|
110708
111204
|
deepDispose(this.animation);
|
|
110709
111205
|
}
|
|
110710
111206
|
this.state.set("animationMode", "none");
|
|
111207
|
+
this._animationStarted = false;
|
|
110711
111208
|
this.toggleAnimationLoop(false);
|
|
110712
111209
|
}
|
|
110713
111210
|
/**
|
|
@@ -110716,6 +111213,8 @@ class Viewer {
|
|
|
110716
111213
|
* @param fraction - relative time between 0 and 1.
|
|
110717
111214
|
*/
|
|
110718
111215
|
setRelativeTime(fraction) {
|
|
111216
|
+
if (fraction !== 0)
|
|
111217
|
+
this._animationStarted = true;
|
|
110719
111218
|
this.animation.setRelativeTime(fraction);
|
|
110720
111219
|
this.state.set("animationSliderValue", fraction * 1000);
|
|
110721
111220
|
}
|
|
@@ -110747,6 +111246,60 @@ class Viewer {
|
|
|
110747
111246
|
setTimeout(() => this.update(true, true), 50);
|
|
110748
111247
|
}
|
|
110749
111248
|
}
|
|
111249
|
+
/**
|
|
111250
|
+
* Draw the outline of the clip caps over the frame (Clip tab active only). Skipped
|
|
111251
|
+
* while the camera moves (drag, wheel, damping, preset views): the outline pass
|
|
111252
|
+
* renders faces and caps a second time, so it is drawn once the camera has been
|
|
111253
|
+
* still for {@link CAP_OUTLINE_SETTLE_MS}.
|
|
111254
|
+
*/
|
|
111255
|
+
_renderCapOutline() {
|
|
111256
|
+
const clipping = this.rendered.clipping;
|
|
111257
|
+
if (!this.renderer.localClippingEnabled || !clipping.hasCaps)
|
|
111258
|
+
return;
|
|
111259
|
+
const camera = this.rendered.camera.getCamera();
|
|
111260
|
+
if (this._cameraMovedSinceLastOutline(camera)) {
|
|
111261
|
+
this._scheduleCapOutline();
|
|
111262
|
+
return;
|
|
111263
|
+
}
|
|
111264
|
+
if (this._capOutline === null)
|
|
111265
|
+
this._capOutline = new CapOutlinePass();
|
|
111266
|
+
const blackEdges = this.state.get("blackEdges") === true;
|
|
111267
|
+
this._capOutline.render(this.renderer, this.rendered.scene, camera, {
|
|
111268
|
+
planes: clipping.clipPlanes,
|
|
111269
|
+
intersection: this.state.get("clipIntersection") === true,
|
|
111270
|
+
occlude: this.state.get("transparent") !== true,
|
|
111271
|
+
color: blackEdges ? 0x000000 : this.state.get("edgeColor"),
|
|
111272
|
+
setCapIdPass: clipping.setCapIdPass,
|
|
111273
|
+
});
|
|
111274
|
+
}
|
|
111275
|
+
/** Whether the camera's world or projection matrix changed since the last call. */
|
|
111276
|
+
_cameraMovedSinceLastOutline(camera) {
|
|
111277
|
+
const now = [
|
|
111278
|
+
...camera.matrixWorld.elements,
|
|
111279
|
+
...camera.projectionMatrix.elements,
|
|
111280
|
+
];
|
|
111281
|
+
const last = this._capOutlineCamera;
|
|
111282
|
+
const moved = last.length !== now.length || now.some((value, i) => value !== last[i]);
|
|
111283
|
+
this._capOutlineCamera = now;
|
|
111284
|
+
return moved;
|
|
111285
|
+
}
|
|
111286
|
+
/** Redraw once the camera has settled, so the skipped outline appears. */
|
|
111287
|
+
_scheduleCapOutline() {
|
|
111288
|
+
if (this._capOutlineSettle !== null)
|
|
111289
|
+
clearTimeout(this._capOutlineSettle);
|
|
111290
|
+
this._capOutlineSettle = setTimeout(() => {
|
|
111291
|
+
this._capOutlineSettle = null;
|
|
111292
|
+
if (this.ready)
|
|
111293
|
+
this.update(true, false);
|
|
111294
|
+
}, CAP_OUTLINE_SETTLE_MS);
|
|
111295
|
+
}
|
|
111296
|
+
/** Cancel a pending outline redraw. */
|
|
111297
|
+
_cancelCapOutline() {
|
|
111298
|
+
if (this._capOutlineSettle !== null)
|
|
111299
|
+
clearTimeout(this._capOutlineSettle);
|
|
111300
|
+
this._capOutlineSettle = null;
|
|
111301
|
+
this._capOutlineCamera = [];
|
|
111302
|
+
}
|
|
110750
111303
|
// ---------------------------------------------------------------------------
|
|
110751
111304
|
// Cleanup & Disposal
|
|
110752
111305
|
// ---------------------------------------------------------------------------
|
|
@@ -110765,6 +111318,8 @@ class Viewer {
|
|
|
110765
111318
|
*/
|
|
110766
111319
|
dispose() {
|
|
110767
111320
|
this.clear();
|
|
111321
|
+
this._capOutline?.dispose();
|
|
111322
|
+
this._capOutline = null;
|
|
110768
111323
|
// Remove all picking listeners (they hold a strong ref to this Viewer via the
|
|
110769
111324
|
// controller's arrow-field handlers; in external-canvas mode the caller owns the
|
|
110770
111325
|
// canvas, so not removing them would leak the disposed Viewer).
|
|
@@ -110810,6 +111365,7 @@ class Viewer {
|
|
|
110810
111365
|
* @public
|
|
110811
111366
|
*/
|
|
110812
111367
|
clear() {
|
|
111368
|
+
this._cancelCapOutline();
|
|
110813
111369
|
if (this._rendered) {
|
|
110814
111370
|
// Drop selection state + hover cache + status line — a PickedComponent may hold
|
|
110815
111371
|
// a HighlightController / ObjectGroup that this clear() disposes; a stale
|
|
@@ -110931,7 +111487,7 @@ class Viewer {
|
|
|
110931
111487
|
if (!this.tree) {
|
|
110932
111488
|
throw new Error("Tree not initialized");
|
|
110933
111489
|
}
|
|
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);
|
|
111490
|
+
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false, this.handleTreeHover);
|
|
110935
111491
|
this.display.clearCadTree();
|
|
110936
111492
|
const t = treeview.create();
|
|
110937
111493
|
timer.split("created tree");
|
|
@@ -111397,6 +111953,40 @@ class Viewer {
|
|
|
111397
111953
|
get lastSelection() {
|
|
111398
111954
|
return this.pickingController.lastSelection;
|
|
111399
111955
|
}
|
|
111956
|
+
/**
|
|
111957
|
+
* Copy the world-space point under the cursor as `"x, y, z"` to the clipboard and
|
|
111958
|
+
* send it to the host as a `copiedPoint` notification (for hosts that block the
|
|
111959
|
+
* browser clipboard). Does nothing while parts may be displaced from their model
|
|
111960
|
+
* positions — explode mode, an animation once started (Play or time slider), a
|
|
111961
|
+
* z-scale other than 1 — or when no visible component is under the cursor.
|
|
111962
|
+
* @returns true when a point was copied (the caller then consumes the key event).
|
|
111963
|
+
*/
|
|
111964
|
+
copyPointUnderCursor() {
|
|
111965
|
+
if (!this.ready)
|
|
111966
|
+
return false;
|
|
111967
|
+
const mode = this.state.get("animationMode");
|
|
111968
|
+
if (mode === "explode")
|
|
111969
|
+
return false;
|
|
111970
|
+
if (mode === "animation" && this._animationStarted)
|
|
111971
|
+
return false;
|
|
111972
|
+
if (this.zScale !== undefined && this.zScale !== 1)
|
|
111973
|
+
return false;
|
|
111974
|
+
const point = this.pickingController.pointUnderCursor();
|
|
111975
|
+
if (point === null)
|
|
111976
|
+
return false;
|
|
111977
|
+
const coords = toVector3Tuple(point.toArray());
|
|
111978
|
+
const fmt = (v) => {
|
|
111979
|
+
const s = v.toFixed(COPY_POINT_DECIMALS);
|
|
111980
|
+
return Number(s) === 0 ? (0).toFixed(COPY_POINT_DECIMALS) : s;
|
|
111981
|
+
};
|
|
111982
|
+
const text = coords.map(fmt).join(", ");
|
|
111983
|
+
copyText(text, this.display.container);
|
|
111984
|
+
if (this.notifyCallback !== null) {
|
|
111985
|
+
this.notifyCallback({ copiedPoint: { old: null, new: coords } });
|
|
111986
|
+
}
|
|
111987
|
+
this.display.setStatusLine(`copied: ${text}`);
|
|
111988
|
+
return true;
|
|
111989
|
+
}
|
|
111400
111990
|
/** Enable/disable the double-click pick handler (on when no tool is active). */
|
|
111401
111991
|
setPickHandler(flag) {
|
|
111402
111992
|
this.pickingController.setPickHandler(flag);
|
|
@@ -111811,7 +112401,7 @@ class Viewer {
|
|
|
111811
112401
|
this.tree = this.compactTree;
|
|
111812
112402
|
// Dispose old treeview and create new one
|
|
111813
112403
|
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);
|
|
112404
|
+
const treeview = new TreeView(this.tree, this.display.cadTreeScrollContainer, this.setObject, this.handlePick, this.update, this.notifyStates, this.getNodeColor, this.state.get("theme"), this.state.get("newTreeBehavior"), false, this.handleTreeHover);
|
|
111815
112405
|
this.rendered.treeview = treeview;
|
|
111816
112406
|
this.display.clearCadTree();
|
|
111817
112407
|
const t = treeview.create();
|
|
@@ -113370,6 +113960,33 @@ class Display {
|
|
|
113370
113960
|
this._matEditorDragAbort = null;
|
|
113371
113961
|
this._matEditorInputAbort = null;
|
|
113372
113962
|
this._animWasVisible = false;
|
|
113963
|
+
/**
|
|
113964
|
+
* Ctrl/Cmd-C while the cursor is over the canvas copies the point under it.
|
|
113965
|
+
* Text-entry fields and selected text keep the normal copy.
|
|
113966
|
+
*/
|
|
113967
|
+
this._handleCopyPoint = (e) => {
|
|
113968
|
+
if (!(e instanceof KeyboardEvent))
|
|
113969
|
+
return;
|
|
113970
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey)
|
|
113971
|
+
return;
|
|
113972
|
+
if (e.key.toLowerCase() !== "c")
|
|
113973
|
+
return;
|
|
113974
|
+
const target = e.target;
|
|
113975
|
+
if ((target instanceof HTMLInputElement &&
|
|
113976
|
+
target.type !== "button" &&
|
|
113977
|
+
target.type !== "checkbox") ||
|
|
113978
|
+
target instanceof HTMLTextAreaElement ||
|
|
113979
|
+
(target instanceof HTMLElement && target.isContentEditable)) {
|
|
113980
|
+
return;
|
|
113981
|
+
}
|
|
113982
|
+
const selection = window.getSelection();
|
|
113983
|
+
if (selection !== null && selection.toString() !== "")
|
|
113984
|
+
return;
|
|
113985
|
+
if (this.viewer.copyPointUnderCursor()) {
|
|
113986
|
+
e.preventDefault();
|
|
113987
|
+
e.stopPropagation();
|
|
113988
|
+
}
|
|
113989
|
+
};
|
|
113373
113990
|
// ---------------------------------------------------------------------------
|
|
113374
113991
|
// Toolbar Button Handlers: View Settings
|
|
113375
113992
|
// ---------------------------------------------------------------------------
|
|
@@ -114692,6 +115309,10 @@ class Display {
|
|
|
114692
115309
|
// Focus handling for keyboard shortcuts
|
|
114693
115310
|
listeners.add(this.container, "mousedown", () => this.container.focus());
|
|
114694
115311
|
listeners.add(this.container, "keydown", this._handleKeyboardShortcut);
|
|
115312
|
+
// Ctrl/Cmd-C acts on the point under the cursor, so it must not depend on the
|
|
115313
|
+
// viewer having focus (e.g. after using a host dropdown). Capture phase: runs
|
|
115314
|
+
// before host shortcuts; the key is consumed only when a point was copied.
|
|
115315
|
+
listeners.add(document, "keydown", this._handleCopyPoint, true);
|
|
114695
115316
|
}
|
|
114696
115317
|
/**
|
|
114697
115318
|
* Subscribe to ViewerState changes to keep UI in sync.
|