@cadview/core 0.2.0 → 0.3.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/index.cjs CHANGED
@@ -2697,7 +2697,7 @@ function drawMText(ctx, entity, pixelSize) {
2697
2697
 
2698
2698
  // src/renderer/entities/draw-insert.ts
2699
2699
  var MAX_INSERT_DEPTH = 100;
2700
- function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2700
+ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0, stats) {
2701
2701
  if (depth > MAX_INSERT_DEPTH) return;
2702
2702
  const block = doc.blocks.get(entity.blockName);
2703
2703
  if (!block) return;
@@ -2722,9 +2722,9 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2722
2722
  ctx.fillStyle = color;
2723
2723
  ctx.lineWidth = adjustedPixelSize;
2724
2724
  if (blockEntity.type === "INSERT") {
2725
- drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1);
2725
+ drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1, stats);
2726
2726
  } else {
2727
- drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize);
2727
+ drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, stats);
2728
2728
  }
2729
2729
  }
2730
2730
  ctx.restore();
@@ -2733,7 +2733,7 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2733
2733
  }
2734
2734
 
2735
2735
  // src/renderer/entities/draw-dimension.ts
2736
- function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2736
+ function drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats) {
2737
2737
  if (entity.blockName) {
2738
2738
  const block = doc.blocks.get(entity.blockName);
2739
2739
  if (block) {
@@ -2742,7 +2742,7 @@ function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2742
2742
  ctx.strokeStyle = color;
2743
2743
  ctx.fillStyle = color;
2744
2744
  ctx.lineWidth = pixelSize;
2745
- drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize);
2745
+ drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize, stats);
2746
2746
  }
2747
2747
  return;
2748
2748
  }
@@ -2819,7 +2819,11 @@ function drawPoint(ctx, entity, pixelSize) {
2819
2819
  }
2820
2820
 
2821
2821
  // src/renderer/entities/draw-entity.ts
2822
- function drawEntity(ctx, entity, doc, vt, theme, pixelSize) {
2822
+ function drawEntity(ctx, entity, doc, vt, theme, pixelSize, stats) {
2823
+ if (stats) {
2824
+ stats.drawCalls++;
2825
+ stats.byType[entity.type] = (stats.byType[entity.type] ?? 0) + 1;
2826
+ }
2823
2827
  switch (entity.type) {
2824
2828
  case "LINE":
2825
2829
  drawLine(ctx, entity);
@@ -2849,10 +2853,10 @@ function drawEntity(ctx, entity, doc, vt, theme, pixelSize) {
2849
2853
  drawMText(ctx, entity, pixelSize);
2850
2854
  break;
2851
2855
  case "INSERT":
2852
- drawInsert(ctx, entity, doc, vt, theme, pixelSize);
2856
+ drawInsert(ctx, entity, doc, vt, theme, pixelSize, 0, stats);
2853
2857
  break;
2854
2858
  case "DIMENSION":
2855
- drawDimension(ctx, entity, doc, vt, theme, pixelSize);
2859
+ drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats);
2856
2860
  break;
2857
2861
  case "HATCH":
2858
2862
  drawHatch(ctx, entity);
@@ -2899,6 +2903,12 @@ var CanvasRenderer = class {
2899
2903
  render(doc, vt, theme, visibleLayers, selectedEntityIndex) {
2900
2904
  const ctx = this.ctx;
2901
2905
  const dpr = window.devicePixelRatio || 1;
2906
+ const stats = {
2907
+ entitiesDrawn: 0,
2908
+ entitiesSkipped: 0,
2909
+ drawCalls: 0,
2910
+ byType: {}
2911
+ };
2902
2912
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2903
2913
  ctx.fillStyle = THEMES[theme].backgroundColor;
2904
2914
  ctx.fillRect(0, 0, this.width, this.height);
@@ -2908,13 +2918,20 @@ var CanvasRenderer = class {
2908
2918
  ctx.lineJoin = "round";
2909
2919
  for (let i = 0; i < doc.entities.length; i++) {
2910
2920
  const entity = doc.entities[i];
2911
- if (!entity.visible) continue;
2912
- if (!visibleLayers.has(entity.layer)) continue;
2921
+ if (!entity.visible) {
2922
+ stats.entitiesSkipped++;
2923
+ continue;
2924
+ }
2925
+ if (!visibleLayers.has(entity.layer)) {
2926
+ stats.entitiesSkipped++;
2927
+ continue;
2928
+ }
2913
2929
  const color = resolveEntityColor(entity, doc.layers, theme);
2914
2930
  ctx.strokeStyle = color;
2915
2931
  ctx.fillStyle = color;
2916
2932
  ctx.lineWidth = pixelSize;
2917
- drawEntity(ctx, entity, doc, vt, theme, pixelSize);
2933
+ stats.entitiesDrawn++;
2934
+ drawEntity(ctx, entity, doc, vt, theme, pixelSize, stats);
2918
2935
  }
2919
2936
  if (selectedEntityIndex >= 0 && selectedEntityIndex < doc.entities.length) {
2920
2937
  const selEntity = doc.entities[selectedEntityIndex];
@@ -2924,6 +2941,7 @@ var CanvasRenderer = class {
2924
2941
  ctx.lineWidth = pixelSize * 3;
2925
2942
  drawEntity(ctx, selEntity, doc, vt, theme, pixelSize);
2926
2943
  }
2944
+ return stats;
2927
2945
  }
2928
2946
  renderEmpty(theme) {
2929
2947
  const ctx = this.ctx;
@@ -2936,6 +2954,136 @@ var CanvasRenderer = class {
2936
2954
  }
2937
2955
  };
2938
2956
 
2957
+ // src/renderer/debug-overlay.ts
2958
+ var DEFAULT_DEBUG_OPTIONS = {
2959
+ showFps: true,
2960
+ showRenderStats: true,
2961
+ showDocumentInfo: true,
2962
+ showTimings: true,
2963
+ showCamera: true,
2964
+ position: "top-left"
2965
+ };
2966
+ function resolveDebugOptions(input) {
2967
+ return { ...DEFAULT_DEBUG_OPTIONS, ...input };
2968
+ }
2969
+ function formatBytes(bytes) {
2970
+ if (bytes === 0) return "0 B";
2971
+ if (bytes < 1024) return `${bytes} B`;
2972
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2973
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2974
+ }
2975
+ function formatZoom(scale) {
2976
+ if (scale >= 1) return `${scale.toFixed(2)}x`;
2977
+ return `1:${(1 / scale).toFixed(1)}`;
2978
+ }
2979
+ var FONT = "11px monospace";
2980
+ var LINE_HEIGHT = 15;
2981
+ var SEPARATOR_HEIGHT = 8;
2982
+ var PADDING = 8;
2983
+ var MARGIN = 10;
2984
+ function renderDebugOverlay(ctx, stats, theme, options, canvasWidth, canvasHeight) {
2985
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
2986
+ const sections = [];
2987
+ if (options.showFps) {
2988
+ sections.push([
2989
+ `FPS: ${stats.fps} Frame: ${stats.frameTime.toFixed(1)}ms`
2990
+ ]);
2991
+ }
2992
+ if (options.showRenderStats) {
2993
+ const total = stats.renderStats.entitiesDrawn + stats.renderStats.entitiesSkipped;
2994
+ const lines = [
2995
+ `Drawn: ${stats.renderStats.entitiesDrawn} / ${total} Calls: ${stats.renderStats.drawCalls}`
2996
+ ];
2997
+ const types = Object.entries(stats.renderStats.byType).sort(([, a], [, b]) => b - a).slice(0, 6).map(([type, count]) => `${type}: ${count}`).join(" ");
2998
+ if (types) lines.push(types);
2999
+ sections.push(lines);
3000
+ }
3001
+ if (options.showDocumentInfo) {
3002
+ const lines = [
3003
+ `Layers: ${stats.visibleLayerCount} / ${stats.layerCount} Blocks: ${stats.blockCount}`
3004
+ ];
3005
+ if (stats.dxfVersion) lines.push(`DXF: ${stats.dxfVersion}`);
3006
+ if (stats.fileName) lines.push(`File: ${stats.fileName}`);
3007
+ if (stats.fileSize > 0) lines.push(`Size: ${formatBytes(stats.fileSize)}`);
3008
+ sections.push(lines);
3009
+ }
3010
+ if (options.showTimings) {
3011
+ const parts = [];
3012
+ if (stats.parseTime > 0) parts.push(`Parse: ${stats.parseTime.toFixed(0)}ms`);
3013
+ if (stats.spatialIndexBuildTime > 0) parts.push(`Index: ${stats.spatialIndexBuildTime.toFixed(0)}ms`);
3014
+ if (stats.totalLoadTime > 0) parts.push(`Load: ${stats.totalLoadTime.toFixed(0)}ms`);
3015
+ if (parts.length > 0) {
3016
+ sections.push([parts.join(" ")]);
3017
+ }
3018
+ }
3019
+ if (options.showCamera) {
3020
+ const b = stats.viewportBounds;
3021
+ sections.push([
3022
+ `Zoom: ${formatZoom(stats.zoom)} Pixel: ${stats.pixelSize.toFixed(2)}`,
3023
+ `View: [${b.minX.toFixed(0)}, ${b.minY.toFixed(0)}] \u2192 [${b.maxX.toFixed(0)}, ${b.maxY.toFixed(0)}]`
3024
+ ]);
3025
+ }
3026
+ if (sections.length === 0) return;
3027
+ ctx.font = FONT;
3028
+ const rows = [];
3029
+ for (let s = 0; s < sections.length; s++) {
3030
+ if (s > 0) rows.push({ text: "", isSeparator: true });
3031
+ for (const line of sections[s]) {
3032
+ rows.push({ text: line, isSeparator: false });
3033
+ }
3034
+ }
3035
+ let maxWidth = 0;
3036
+ for (const row of rows) {
3037
+ if (!row.isSeparator) {
3038
+ const w = ctx.measureText(row.text).width;
3039
+ if (w > maxWidth) maxWidth = w;
3040
+ }
3041
+ }
3042
+ const panelWidth = maxWidth + PADDING * 2;
3043
+ let panelHeight = PADDING * 2;
3044
+ for (const row of rows) {
3045
+ panelHeight += row.isSeparator ? SEPARATOR_HEIGHT : LINE_HEIGHT;
3046
+ }
3047
+ let x;
3048
+ let y;
3049
+ switch (options.position) {
3050
+ case "top-left":
3051
+ x = MARGIN;
3052
+ y = MARGIN;
3053
+ break;
3054
+ case "top-right":
3055
+ x = canvasWidth - panelWidth - MARGIN;
3056
+ y = MARGIN;
3057
+ break;
3058
+ case "bottom-left":
3059
+ x = MARGIN;
3060
+ y = canvasHeight - panelHeight - MARGIN;
3061
+ break;
3062
+ case "bottom-right":
3063
+ x = canvasWidth - panelWidth - MARGIN;
3064
+ y = canvasHeight - panelHeight - MARGIN;
3065
+ break;
3066
+ }
3067
+ const config = THEMES[theme];
3068
+ ctx.fillStyle = theme === "dark" ? "rgba(0, 0, 0, 0.75)" : "rgba(255, 255, 255, 0.85)";
3069
+ ctx.fillRect(x, y, panelWidth, panelHeight);
3070
+ ctx.strokeStyle = theme === "dark" ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)";
3071
+ ctx.lineWidth = 1;
3072
+ ctx.strokeRect(x + 0.5, y + 0.5, panelWidth - 1, panelHeight - 1);
3073
+ ctx.fillStyle = theme === "dark" ? config.defaultEntityColor : "rgba(0, 0, 0, 0.85)";
3074
+ ctx.textAlign = "left";
3075
+ ctx.textBaseline = "top";
3076
+ let cursorY = y + PADDING;
3077
+ for (const row of rows) {
3078
+ if (row.isSeparator) {
3079
+ cursorY += SEPARATOR_HEIGHT;
3080
+ } else {
3081
+ ctx.fillText(row.text, x + PADDING, cursorY);
3082
+ cursorY += LINE_HEIGHT;
3083
+ }
3084
+ }
3085
+ }
3086
+
2939
3087
  // src/viewer/layers.ts
2940
3088
  var LayerManager = class {
2941
3089
  layers = /* @__PURE__ */ new Map();
@@ -3726,6 +3874,19 @@ var CadViewer = class {
3726
3874
  loadGeneration = 0;
3727
3875
  mouseScreenX = 0;
3728
3876
  mouseScreenY = 0;
3877
+ // Debug mode state
3878
+ debugEnabled = false;
3879
+ debugOptions;
3880
+ lastRenderStats = null;
3881
+ lastDebugStats = null;
3882
+ frameTimestamps = [];
3883
+ lastDoRenderTime = 0;
3884
+ lastFrameTime = 0;
3885
+ parseTime = 0;
3886
+ spatialIndexBuildTime = 0;
3887
+ loadedFileName = null;
3888
+ loadedFileSize = 0;
3889
+ debugRafId = 0;
3729
3890
  constructor(canvas, options) {
3730
3891
  this.canvas = canvas;
3731
3892
  this.options = {
@@ -3738,6 +3899,14 @@ var CadViewer = class {
3738
3899
  initialTool: options?.initialTool ?? "pan"
3739
3900
  };
3740
3901
  this.formatConverters = options?.formatConverters ?? [];
3902
+ if (options?.debug) {
3903
+ this.debugEnabled = true;
3904
+ this.debugOptions = resolveDebugOptions(
3905
+ typeof options.debug === "boolean" ? void 0 : options.debug
3906
+ );
3907
+ } else {
3908
+ this.debugOptions = resolveDebugOptions();
3909
+ }
3741
3910
  this.renderer = new CanvasRenderer(canvas);
3742
3911
  this.camera = new Camera(this.options);
3743
3912
  this.layerManager = new LayerManager();
@@ -3750,6 +3919,9 @@ var CadViewer = class {
3750
3919
  this.resizeObserver.observe(canvas);
3751
3920
  canvas.style.cursor = this.getCursorForTool(this.currentTool);
3752
3921
  this.requestRender();
3922
+ if (this.debugEnabled) {
3923
+ this.startDebugLoop();
3924
+ }
3753
3925
  }
3754
3926
  // === Loading ===
3755
3927
  /**
@@ -3788,11 +3960,15 @@ var CadViewer = class {
3788
3960
  async loadFile(file) {
3789
3961
  this.guardDestroyed();
3790
3962
  const generation = ++this.loadGeneration;
3963
+ this.loadedFileName = file.name;
3964
+ this.loadedFileSize = file.size;
3791
3965
  const buffer = await file.arrayBuffer();
3792
3966
  if (this.destroyed || generation !== this.loadGeneration) return;
3793
3967
  const dxfString = await this.runConverters(buffer);
3794
3968
  if (this.destroyed || generation !== this.loadGeneration) return;
3969
+ const t0 = performance.now();
3795
3970
  this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3971
+ this.parseTime = performance.now() - t0;
3796
3972
  this.onDocumentLoaded();
3797
3973
  }
3798
3974
  /**
@@ -3803,9 +3979,13 @@ var CadViewer = class {
3803
3979
  async loadBuffer(buffer) {
3804
3980
  this.guardDestroyed();
3805
3981
  const generation = ++this.loadGeneration;
3982
+ this.loadedFileName = null;
3983
+ this.loadedFileSize = buffer.byteLength;
3806
3984
  const dxfString = await this.runConverters(buffer);
3807
3985
  if (this.destroyed || generation !== this.loadGeneration) return;
3986
+ const t0 = performance.now();
3808
3987
  this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3988
+ this.parseTime = performance.now() - t0;
3809
3989
  this.onDocumentLoaded();
3810
3990
  }
3811
3991
  /**
@@ -3815,6 +3995,9 @@ var CadViewer = class {
3815
3995
  loadDocument(doc) {
3816
3996
  this.guardDestroyed();
3817
3997
  ++this.loadGeneration;
3998
+ this.loadedFileName = null;
3999
+ this.loadedFileSize = 0;
4000
+ this.parseTime = 0;
3818
4001
  if (!doc || !Array.isArray(doc.entities) || !(doc.layers instanceof Map)) {
3819
4002
  throw new Error("CadViewer: invalid DxfDocument \u2014 expected entities array and layers Map.");
3820
4003
  }
@@ -3827,7 +4010,11 @@ var CadViewer = class {
3827
4010
  loadString(dxf) {
3828
4011
  this.guardDestroyed();
3829
4012
  ++this.loadGeneration;
4013
+ this.loadedFileName = null;
4014
+ this.loadedFileSize = dxf.length;
4015
+ const t0 = performance.now();
3830
4016
  this.doc = parseDxf(dxf);
4017
+ this.parseTime = performance.now() - t0;
3831
4018
  this.onDocumentLoaded();
3832
4019
  }
3833
4020
  /**
@@ -3837,7 +4024,11 @@ var CadViewer = class {
3837
4024
  loadArrayBuffer(buffer) {
3838
4025
  this.guardDestroyed();
3839
4026
  ++this.loadGeneration;
4027
+ this.loadedFileName = null;
4028
+ this.loadedFileSize = buffer.byteLength;
4029
+ const t0 = performance.now();
3840
4030
  this.doc = parseDxf(buffer);
4031
+ this.parseTime = performance.now() - t0;
3841
4032
  this.onDocumentLoaded();
3842
4033
  }
3843
4034
  /**
@@ -3856,7 +4047,9 @@ var CadViewer = class {
3856
4047
  onDocumentLoaded() {
3857
4048
  if (!this.doc) return;
3858
4049
  this.layerManager.setLayers(this.doc.layers);
4050
+ const t0 = performance.now();
3859
4051
  this.spatialIndex.build(this.doc.entities);
4052
+ this.spatialIndexBuildTime = performance.now() - t0;
3860
4053
  this.selectedEntityIndex = -1;
3861
4054
  this.measureTool.deactivate();
3862
4055
  if (this.currentTool === "measure") {
@@ -3984,6 +4177,7 @@ var CadViewer = class {
3984
4177
  }
3985
4178
  destroy() {
3986
4179
  this.destroyed = true;
4180
+ this.stopDebugLoop();
3987
4181
  this.inputHandler.destroy();
3988
4182
  this.resizeObserver.disconnect();
3989
4183
  this.renderer.destroy();
@@ -3995,7 +4189,12 @@ var CadViewer = class {
3995
4189
  // === Internal (called by InputHandler) ===
3996
4190
  /** @internal */
3997
4191
  requestRender() {
3998
- if (this.renderPending || this.destroyed) return;
4192
+ if (this.destroyed) return;
4193
+ if (this.debugRafId) {
4194
+ this.doRender();
4195
+ return;
4196
+ }
4197
+ if (this.renderPending) return;
3999
4198
  this.renderPending = true;
4000
4199
  requestAnimationFrame(() => {
4001
4200
  this.renderPending = false;
@@ -4008,13 +4207,24 @@ var CadViewer = class {
4008
4207
  this.renderer.renderEmpty(this.options.theme);
4009
4208
  return;
4010
4209
  }
4011
- this.renderer.render(
4210
+ const renderStart = performance.now();
4211
+ const stats = this.renderer.render(
4012
4212
  this.doc,
4013
4213
  this.camera.getTransform(),
4014
4214
  this.options.theme,
4015
4215
  this.layerManager.getVisibleLayerNames(),
4016
4216
  this.selectedEntityIndex
4017
4217
  );
4218
+ this.lastFrameTime = performance.now() - renderStart;
4219
+ this.lastRenderStats = stats;
4220
+ const now = performance.now();
4221
+ if (now - this.lastDoRenderTime >= 3) {
4222
+ this.frameTimestamps.push(now);
4223
+ }
4224
+ this.lastDoRenderTime = now;
4225
+ while (this.frameTimestamps.length > 0 && this.frameTimestamps[0] < now - 1e3) {
4226
+ this.frameTimestamps.shift();
4227
+ }
4018
4228
  if (this.currentTool === "measure" && this.measureTool.state.phase !== "idle") {
4019
4229
  const ctx = this.renderer.getContext();
4020
4230
  renderMeasureOverlay(
@@ -4026,7 +4236,104 @@ var CadViewer = class {
4026
4236
  this.options.theme
4027
4237
  );
4028
4238
  }
4239
+ if (this.debugEnabled) {
4240
+ const ctx = this.renderer.getContext();
4241
+ const debugStats = this.buildDebugStats();
4242
+ this.lastDebugStats = debugStats;
4243
+ renderDebugOverlay(
4244
+ ctx,
4245
+ debugStats,
4246
+ this.options.theme,
4247
+ this.debugOptions,
4248
+ this.renderer.getWidth(),
4249
+ this.renderer.getHeight()
4250
+ );
4251
+ }
4252
+ }
4253
+ // === Debug Mode ===
4254
+ /**
4255
+ * Enable or disable the debug overlay.
4256
+ * Pass `true` for defaults, `false` to disable, or an object for granular control.
4257
+ */
4258
+ setDebug(debug) {
4259
+ this.guardDestroyed();
4260
+ if (typeof debug === "boolean") {
4261
+ this.debugEnabled = debug;
4262
+ } else {
4263
+ this.debugEnabled = true;
4264
+ this.debugOptions = resolveDebugOptions(debug);
4265
+ }
4266
+ if (this.debugEnabled) {
4267
+ this.startDebugLoop();
4268
+ } else {
4269
+ this.stopDebugLoop();
4270
+ this.requestRender();
4271
+ }
4272
+ }
4273
+ startDebugLoop() {
4274
+ if (this.debugRafId) return;
4275
+ const loop = () => {
4276
+ if (!this.debugEnabled || this.destroyed) {
4277
+ this.debugRafId = 0;
4278
+ return;
4279
+ }
4280
+ this.doRender();
4281
+ this.debugRafId = requestAnimationFrame(loop);
4282
+ };
4283
+ this.debugRafId = requestAnimationFrame(loop);
4284
+ }
4285
+ stopDebugLoop() {
4286
+ if (this.debugRafId) {
4287
+ cancelAnimationFrame(this.debugRafId);
4288
+ this.debugRafId = 0;
4289
+ }
4290
+ }
4291
+ /**
4292
+ * Get the latest debug stats snapshot, or null if debug mode is off.
4293
+ */
4294
+ getDebugStats() {
4295
+ return this.debugEnabled ? this.lastDebugStats : null;
4296
+ }
4297
+ buildDebugStats() {
4298
+ const vt = this.camera.getTransform();
4299
+ const w = this.renderer.getWidth();
4300
+ const h = this.renderer.getHeight();
4301
+ const bounds = this.computeViewportBounds(vt, w, h);
4302
+ return {
4303
+ fps: this.frameTimestamps.length,
4304
+ frameTime: this.lastFrameTime,
4305
+ renderStats: this.lastRenderStats ?? {
4306
+ entitiesDrawn: 0,
4307
+ entitiesSkipped: 0,
4308
+ drawCalls: 0,
4309
+ byType: {}
4310
+ },
4311
+ entityCount: this.doc?.entities.length ?? 0,
4312
+ layerCount: this.doc?.layers.size ?? 0,
4313
+ visibleLayerCount: this.layerManager.getVisibleLayerNames().size,
4314
+ blockCount: this.doc?.blocks.size ?? 0,
4315
+ parseTime: this.parseTime,
4316
+ spatialIndexBuildTime: this.spatialIndexBuildTime,
4317
+ totalLoadTime: this.parseTime + this.spatialIndexBuildTime,
4318
+ zoom: vt.scale,
4319
+ pixelSize: vt.scale > 0 ? 1 / vt.scale : 0,
4320
+ viewportBounds: bounds,
4321
+ fileName: this.loadedFileName,
4322
+ fileSize: this.loadedFileSize,
4323
+ dxfVersion: this.doc?.header.acadVersion ?? null
4324
+ };
4325
+ }
4326
+ computeViewportBounds(vt, w, h) {
4327
+ const [x1, y1] = screenToWorld(vt, 0, h);
4328
+ const [x2, y2] = screenToWorld(vt, w, 0);
4329
+ return {
4330
+ minX: Math.min(x1, x2),
4331
+ minY: Math.min(y1, y2),
4332
+ maxX: Math.max(x1, x2),
4333
+ maxY: Math.max(y1, y2)
4334
+ };
4029
4335
  }
4336
+ // === Internal (called by InputHandler) ===
4030
4337
  /** @internal */
4031
4338
  handlePan(dx, dy) {
4032
4339
  this.camera.pan(dx, dy);
@@ -4129,6 +4436,7 @@ exports.findSnaps = findSnaps;
4129
4436
  exports.fitToView = fitToView;
4130
4437
  exports.hitTest = hitTest;
4131
4438
  exports.parseDxf = parseDxf;
4439
+ exports.renderDebugOverlay = renderDebugOverlay;
4132
4440
  exports.renderMeasureOverlay = renderMeasureOverlay;
4133
4441
  exports.resolveEntityColor = resolveEntityColor;
4134
4442
  exports.screenToWorld = screenToWorld;