three-cad-viewer 5.0.5 → 5.0.7

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.
@@ -84005,7 +84005,10 @@ void main() {
84005
84005
  * `Uint32BufferAttribute` under this name; the pick pass reads it as an **integer**
84006
84006
  * attribute (`gpuType = THREE.IntType`, GLSL3 `in uint`) so ids stay exact past
84007
84007
  * 2^24. The geometry side must set `gpuType = THREE.IntType` on the attribute for
84008
- * the pick shader to read it.
84008
+ * the pick shader to read it. Between the stages the id travels as two float
84009
+ * varyings (16-bit halves), deliberately NOT as a `flat uint` — see the note in
84010
+ * highlight.ts: a `flat` varying costs ~25x the scene's buffer bytes in graphics
84011
+ * memory under WebKit's ANGLE-on-Metal backend.
84009
84012
  */
84010
84013
  const COMPONENT_ID_ATTRIBUTE = "componentId";
84011
84014
  /**
@@ -84303,8 +84306,9 @@ void main() {
84303
84306
  */
84304
84307
  const PICK_POINT_SIZE = 3;
84305
84308
  /**
84306
- * Shared GLSL3 MRT fragment shader for all pick materials (face/vertex/edge): packs
84307
- * the flat `vId` into attachment 0 (byte order MUST match `packId()`: low byte in R)
84309
+ * Shared GLSL3 MRT fragment shader for all pick materials (face/vertex/edge):
84310
+ * reassembles the id from its two interpolated halves and packs it into attachment 0
84311
+ * (byte order MUST match `packId()`: low byte in R)
84308
84312
  * and writes the interpolated world position into attachment 1 (RGBA32F; `w=1` marks
84309
84313
  * "has position"). The fragment clip chunk only `discard`s (non-ALPHA_TO_COVERAGE),
84310
84314
  * so no `diffuseColor` symbol is required.
@@ -84312,13 +84316,15 @@ void main() {
84312
84316
  const PICK_FRAGMENT_SHADER = /* glsl */ `
84313
84317
  #include <clipping_planes_pars_fragment>
84314
84318
 
84315
- flat in uint vId;
84319
+ in float vIdLo;
84320
+ in float vIdHi;
84316
84321
  in vec3 vWorldPos;
84317
84322
  layout(location = 0) out vec4 fragId;
84318
84323
  layout(location = 1) out vec4 fragPos;
84319
84324
 
84320
84325
  void main() {
84321
84326
  #include <clipping_planes_fragment>
84327
+ uint vId = ( uint( vIdHi + 0.5 ) << 16 ) | uint( vIdLo + 0.5 );
84322
84328
  fragId = vec4(
84323
84329
  float( vId & 0xFFu ) / 255.0,
84324
84330
  float( ( vId >> 8 ) & 0xFFu ) / 255.0,
@@ -84336,19 +84342,24 @@ void main() {
84336
84342
  function createFacePickMaterial(options = {}) {
84337
84343
  // GLSL3 (WebGL2). For a (non-Raw) ShaderMaterial three.js auto-injects
84338
84344
  // `in vec3 position;`, `uniform mat4 modelMatrix/modelViewMatrix/projectionMatrix`,
84339
- // so we declare ONLY the custom integer attribute + flat varying. Integer
84340
- // varyings MUST be `flat`. Clip chunks use legacy `varying`/`attribute` keywords
84345
+ // so we declare ONLY the custom integer attribute + the two id-half varyings
84346
+ // (float, interpolated — an integer varying would have to be `flat`, which is
84347
+ // the ANGLE-on-Metal memory trap described in highlight.ts; all vertices of a
84348
+ // primitive carry the same id, so interpolation + rounding is exact). Clip
84349
+ // chunks use legacy `varying`/`attribute` keywords
84341
84350
  // which three remaps via `#define` under GLSL3; the vertex chunk reads a local
84342
84351
  // `vec4 mvPosition`, which we compute before the include.
84343
84352
  const vertexShader = /* glsl */ `
84344
84353
  #include <clipping_planes_pars_vertex>
84345
84354
 
84346
84355
  in uint componentId;
84347
- flat out uint vId;
84356
+ out float vIdLo;
84357
+ out float vIdHi;
84348
84358
  out vec3 vWorldPos;
84349
84359
 
84350
84360
  void main() {
84351
- vId = componentId;
84361
+ vIdLo = float( componentId & 0xFFFFu );
84362
+ vIdHi = float( componentId >> 16 );
84352
84363
  vec4 worldPos = modelMatrix * vec4( position, 1.0 );
84353
84364
  vWorldPos = worldPos.xyz;
84354
84365
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
@@ -84387,13 +84398,15 @@ void main() {
84387
84398
  #include <clipping_planes_pars_vertex>
84388
84399
 
84389
84400
  in uint componentId;
84390
- flat out uint vId;
84401
+ out float vIdLo;
84402
+ out float vIdHi;
84391
84403
  out vec3 vWorldPos;
84392
84404
  uniform float uPickSize;
84393
84405
  uniform float uDepthBias;
84394
84406
 
84395
84407
  void main() {
84396
- vId = componentId;
84408
+ vIdLo = float( componentId & 0xFFFFu );
84409
+ vIdHi = float( componentId >> 16 );
84397
84410
  vec4 worldPos = modelMatrix * vec4( position, 1.0 );
84398
84411
  vWorldPos = worldPos.xyz;
84399
84412
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
@@ -84453,7 +84466,8 @@ void main() {
84453
84466
  in vec3 instanceEnd;
84454
84467
  in uint componentId;
84455
84468
 
84456
- flat out uint vId;
84469
+ out float vIdLo;
84470
+ out float vIdHi;
84457
84471
  out vec3 vWorldPos;
84458
84472
 
84459
84473
  void trimSegment( const in vec4 start, inout vec4 end ) {
@@ -84465,7 +84479,8 @@ void main() {
84465
84479
  }
84466
84480
 
84467
84481
  void main() {
84468
- vId = componentId;
84482
+ vIdLo = float( componentId & 0xFFFFu );
84483
+ vIdHi = float( componentId >> 16 );
84469
84484
 
84470
84485
  // World-space segment endpoints → approximate hit point along the edge.
84471
84486
  vWorldPos = ( position.y < 0.5 )
@@ -85003,20 +85018,41 @@ void main() {
85003
85018
  //
85004
85019
  // three upgrades stock materials to GLSL ES 3.00 on WebGL2 via `#define attribute
85005
85020
  // in` / `#define varying out|in` macros (WebGLProgram.js), so writing `attribute` /
85006
- // `flat varying` here is converted automatically; `usampler2D` / `texelFetch` /
85021
+ // `varying` here is converted automatically; `usampler2D` / `texelFetch` /
85007
85022
  // integer attributes are then available.
85023
+ //
85024
+ // The component id crosses to the fragment stage as TWO ordinary (interpolated)
85025
+ // float varyings holding its 16-bit halves — NOT as a `flat uint`. A `flat`
85026
+ // varying is what an integer varying would require, and it is exactly what must
85027
+ // be avoided here: under WebKit's ANGLE-on-Metal backend, every draw whose
85028
+ // program has a `flat` varying makes ANGLE allocate a converted copy of the
85029
+ // draw's index data (>= 64 KB each, pooled, never returned to the GL on
85030
+ // deleteBuffer). With this varying on every visible material that was ~25x the
85031
+ // scene's vertex/index bytes in graphics memory (26 MB of buffers -> 812 MB;
85032
+ // 100 MB -> 2.3 GB) and it is charged to the page's WebContent process in the
85033
+ // system WebKit, which killed build123d Studio at 16 GB after a few large
85034
+ // shows. Without `flat` the same scene takes 63 MB / 160 MB. Measured 2026-09-17
85035
+ // (working-docs/leak-harness.html + probe2.html, Playwright WebKit and Safari).
85036
+ //
85037
+ // Interpolating floats is exact for the id's purpose: every vertex of a face,
85038
+ // every vertex of an instanced segment and a point carry the SAME id, so the
85039
+ // interpolated value equals it up to rounding, and `uint(x + 0.5)` recovers it.
85040
+ // Two 16-bit halves (both < 65536, exactly representable in float32) keep the
85041
+ // full 32-bit id range that `applyComponentIds` promises.
85008
85042
  // ---------------------------------------------------------------------------
85009
85043
  /**
85010
85044
  * Shared state-fetch GLSL, injected into BOTH stages: the vertex stage needs it for
85011
85045
  * widening / point size, the fragment for color. Declares the sampler + texWidth +
85012
- * the flat varying and a helper returning the component's {@link HighlightFlag} bits
85013
- * (0 for background / nothing).
85046
+ * the two id-half varyings and a helper returning the component's
85047
+ * {@link HighlightFlag} bits (0 for background / nothing).
85014
85048
  */
85015
85049
  const HL_STATE_GLSL = `
85016
- flat varying uint vHighlightId;
85050
+ varying float vHighlightIdLo;
85051
+ varying float vHighlightIdHi;
85017
85052
  uniform highp usampler2D ${U_HIGHLIGHT_STATE};
85018
85053
  uniform int ${U_HIGHLIGHT_TEX_WIDTH};
85019
85054
  uint highlightState() {
85055
+ uint vHighlightId = (uint(vHighlightIdHi + 0.5) << 16) | uint(vHighlightIdLo + 0.5);
85020
85056
  if (vHighlightId == 0u) return 0u;
85021
85057
  ivec2 hlUv = ivec2(
85022
85058
  int(vHighlightId) % ${U_HIGHLIGHT_TEX_WIDTH},
@@ -85031,8 +85067,8 @@ uint highlightState() {
85031
85067
  const HL_VERTEX_HEADER = `
85032
85068
  attribute uint ${COMPONENT_ID_ATTRIBUTE};
85033
85069
  ${HL_STATE_GLSL}`;
85034
- /** Vertex main: forward the id. Injected right after `void main() {`. */
85035
- const HL_VERTEX_ASSIGN = `vHighlightId = ${COMPONENT_ID_ATTRIBUTE};`;
85070
+ /** Vertex main: forward the id as two 16-bit halves. Injected right after `void main() {`. */
85071
+ const HL_VERTEX_ASSIGN = `vHighlightIdLo = float(${COMPONENT_ID_ATTRIBUTE} & 0xFFFFu); vHighlightIdHi = float(${COMPONENT_ID_ATTRIBUTE} >> 16);`;
85036
85072
  /** Fragment header: the shared state fetch + the two highlight colors. */
85037
85073
  const HL_FRAGMENT_HEADER = `
85038
85074
  ${HL_STATE_GLSL}
@@ -85282,7 +85318,7 @@ uniform vec3 ${U_HIGHLIGHT_HOVER_COLOR};`;
85282
85318
  this.uniforms.uHighlightSelectedColor;
85283
85319
  shader.uniforms[U_HIGHLIGHT_HOVER_COLOR] =
85284
85320
  this.uniforms.uHighlightHoverColor;
85285
- // Common: forward the component id as a flat varying.
85321
+ // Common: forward the component id (as two interpolated float halves).
85286
85322
  shader.vertexShader =
85287
85323
  HL_VERTEX_HEADER +
85288
85324
  "\n" +
@@ -88814,7 +88850,14 @@ float metalnessFactor = metalness;
88814
88850
  group.name = `GridHelper-${i}`;
88815
88851
  group.add(new GridHelper(this.size, 2 * this.ticks, this.colors[this.theme][i === 0 ? 1 : i === 1 ? 0 : 2], this.colors[this.theme][i === 0 ? 0 : i === 1 ? 2 : 1], this.theme == "dark" ? 0x7777777 : 0xbbbbbb));
88816
88852
  let label;
88817
- for (let x = -this.size / 2; x <= this.size / 2; x += this.delta / 2) {
88853
+ // A grid of no size has no labels, and asking for them hangs the tab:
88854
+ // `niceBounds` answers [0, 0, 0] for a bounding box whose largest extent
88855
+ // is zero - which is what an empty model gives it - so `delta` is zero
88856
+ // and the loop below steps by zero, never advancing and never throwing.
88857
+ // Guarded here rather than in `niceBounds`, whose answer is right: there
88858
+ // is nothing to place ticks on.
88859
+ const step = this.delta / 2;
88860
+ for (let x = -this.size / 2; step > 0 && x <= this.size / 2; x += step) {
88818
88861
  if (Math.abs(x) < 1e-6) {
88819
88862
  continue;
88820
88863
  } // skip center label
@@ -97356,7 +97399,7 @@ float metalnessFactor = metalness;
97356
97399
  }
97357
97400
  }
97358
97401
 
97359
- const version = "5.0.5";
97402
+ const version = "5.0.7";
97360
97403
 
97361
97404
  /**
97362
97405
  * `PickedComponent` over a GPU id-pick result. Drives the shader
@@ -110826,6 +110869,11 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
110826
110869
  deepDispose(this._rendered.camera);
110827
110870
  deepDispose(this._rendered.controls);
110828
110871
  deepDispose(this._rendered.treeview);
110872
+ // The orientation marker owns its own THREE.Scene (cones, labels, sphere,
110873
+ // axes), not part of the main scene above, so it needs its own dispose —
110874
+ // without it every clear()/render() cycle left 8 geometries and 3 programs
110875
+ // behind on the GL (measured 2026-09-17, working-docs/leak-harness.html).
110876
+ this._rendered.orientationMarker.dispose();
110829
110877
  // clear tree view
110830
110878
  this.display.clearCadTree();
110831
110879
  // clear info
@@ -112532,12 +112580,21 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
112532
112580
  this.renderer.setSize(cadWidth, height);
112533
112581
  // Resize the id pick target to match the canvas
112534
112582
  this.idPicker?.setSize(cadWidth, height);
112535
- // Adapt display dimensions
112583
+ // Adapt display dimensions. `glass` and `tools` are part of the sizes:
112584
+ // setSizes widens the toolbar and the body by the tree only when the tree
112585
+ // sits beside the canvas (`tools && !glass`), and updates the tree and
112586
+ // info heights only outside glass mode. Leaving them out meant neither
112587
+ // branch could ever be taken from a resize, so a non-glass viewer got a
112588
+ // toolbar and a body of `cadWidth + 2` with a `treeWidth + cadWidth` row
112589
+ // inside them - measured as a 550px toolbar over 802px of content - and a
112590
+ // tree that kept its old height. `glassMode` passes both and was right.
112536
112591
  this.display.setSizes({
112537
112592
  treeWidth: treeWidth,
112538
112593
  treeHeight: this.state.get("treeHeight"),
112539
112594
  cadWidth: cadWidth,
112540
112595
  height: height,
112596
+ glass: glass,
112597
+ tools: this.state.get("tools"),
112541
112598
  });
112542
112599
  // Set glass state - subscription will update UI
112543
112600
  this.state.set("glass", glass);