@cadview/core 0.1.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.js CHANGED
@@ -1545,7 +1545,7 @@ function decodeInput(input) {
1545
1545
  const bytes = new Uint8Array(input);
1546
1546
  const sentinelBytes = new TextDecoder("ascii").decode(bytes.slice(0, BINARY_DXF_SENTINEL.length));
1547
1547
  if (sentinelBytes === BINARY_DXF_SENTINEL) {
1548
- throw new Error("Binary DXF format is not supported. Please export as ASCII DXF.");
1548
+ throw new DxfParseError("Binary DXF format is not supported. Please export as ASCII DXF.");
1549
1549
  }
1550
1550
  let text = new TextDecoder("utf-8").decode(input);
1551
1551
  const versionMatch = text.match(/\$ACADVER[\s\S]*?\n\s*1\s*\n\s*(\S+)/);
@@ -1616,7 +1616,7 @@ function parseDxf(input) {
1616
1616
  try {
1617
1617
  text = decodeInput(input);
1618
1618
  } catch (err) {
1619
- if (err instanceof Error && err.message.includes("Binary DXF")) {
1619
+ if (err instanceof DxfParseError) {
1620
1620
  throw err;
1621
1621
  }
1622
1622
  throw new DxfParseError("Failed to decode DXF input.", err);
@@ -2479,23 +2479,25 @@ function deBoor(degree, controlPoints, knots, t, weights) {
2479
2479
  const denom = knots[i + degree - r + 1] - knots[i];
2480
2480
  if (Math.abs(denom) < 1e-10) continue;
2481
2481
  const alpha = (t - knots[i]) / denom;
2482
+ const dj = d[j];
2483
+ const djPrev = d[j - 1];
2482
2484
  if (weights) {
2483
2485
  const w0 = w[j - 1] * (1 - alpha);
2484
2486
  const w1 = w[j] * alpha;
2485
2487
  const wSum = w0 + w1;
2486
2488
  if (Math.abs(wSum) < 1e-10) continue;
2487
- d[j].x = (d[j - 1].x * w0 + d[j].x * w1) / wSum;
2488
- d[j].y = (d[j - 1].y * w0 + d[j].y * w1) / wSum;
2489
- d[j].z = (d[j - 1].z * w0 + d[j].z * w1) / wSum;
2489
+ dj.x = (djPrev.x * w0 + dj.x * w1) / wSum;
2490
+ dj.y = (djPrev.y * w0 + dj.y * w1) / wSum;
2491
+ dj.z = (djPrev.z * w0 + dj.z * w1) / wSum;
2490
2492
  w[j] = wSum;
2491
2493
  } else {
2492
- d[j].x = (1 - alpha) * d[j - 1].x + alpha * d[j].x;
2493
- d[j].y = (1 - alpha) * d[j - 1].y + alpha * d[j].y;
2494
- d[j].z = (1 - alpha) * d[j - 1].z + alpha * d[j].z;
2494
+ dj.x = (1 - alpha) * djPrev.x + alpha * dj.x;
2495
+ dj.y = (1 - alpha) * djPrev.y + alpha * dj.y;
2496
+ dj.z = (1 - alpha) * djPrev.z + alpha * dj.z;
2495
2497
  }
2496
2498
  }
2497
2499
  }
2498
- return d[degree];
2500
+ return d[degree] ?? { x: 0, y: 0, z: 0 };
2499
2501
  }
2500
2502
  function fitPointsToPolyline(fitPoints) {
2501
2503
  if (fitPoints.length < 2) return fitPoints.map((p) => ({ x: p.x, y: p.y }));
@@ -2689,7 +2691,7 @@ function drawMText(ctx, entity, pixelSize) {
2689
2691
 
2690
2692
  // src/renderer/entities/draw-insert.ts
2691
2693
  var MAX_INSERT_DEPTH = 100;
2692
- function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2694
+ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0, stats) {
2693
2695
  if (depth > MAX_INSERT_DEPTH) return;
2694
2696
  const block = doc.blocks.get(entity.blockName);
2695
2697
  if (!block) return;
@@ -2714,9 +2716,9 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2714
2716
  ctx.fillStyle = color;
2715
2717
  ctx.lineWidth = adjustedPixelSize;
2716
2718
  if (blockEntity.type === "INSERT") {
2717
- drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1);
2719
+ drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1, stats);
2718
2720
  } else {
2719
- drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize);
2721
+ drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, stats);
2720
2722
  }
2721
2723
  }
2722
2724
  ctx.restore();
@@ -2725,7 +2727,7 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2725
2727
  }
2726
2728
 
2727
2729
  // src/renderer/entities/draw-dimension.ts
2728
- function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2730
+ function drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats) {
2729
2731
  if (entity.blockName) {
2730
2732
  const block = doc.blocks.get(entity.blockName);
2731
2733
  if (block) {
@@ -2734,7 +2736,7 @@ function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2734
2736
  ctx.strokeStyle = color;
2735
2737
  ctx.fillStyle = color;
2736
2738
  ctx.lineWidth = pixelSize;
2737
- drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize);
2739
+ drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize, stats);
2738
2740
  }
2739
2741
  return;
2740
2742
  }
@@ -2811,7 +2813,11 @@ function drawPoint(ctx, entity, pixelSize) {
2811
2813
  }
2812
2814
 
2813
2815
  // src/renderer/entities/draw-entity.ts
2814
- function drawEntity(ctx, entity, doc, vt, theme, pixelSize) {
2816
+ function drawEntity(ctx, entity, doc, vt, theme, pixelSize, stats) {
2817
+ if (stats) {
2818
+ stats.drawCalls++;
2819
+ stats.byType[entity.type] = (stats.byType[entity.type] ?? 0) + 1;
2820
+ }
2815
2821
  switch (entity.type) {
2816
2822
  case "LINE":
2817
2823
  drawLine(ctx, entity);
@@ -2841,10 +2847,10 @@ function drawEntity(ctx, entity, doc, vt, theme, pixelSize) {
2841
2847
  drawMText(ctx, entity, pixelSize);
2842
2848
  break;
2843
2849
  case "INSERT":
2844
- drawInsert(ctx, entity, doc, vt, theme, pixelSize);
2850
+ drawInsert(ctx, entity, doc, vt, theme, pixelSize, 0, stats);
2845
2851
  break;
2846
2852
  case "DIMENSION":
2847
- drawDimension(ctx, entity, doc, vt, theme, pixelSize);
2853
+ drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats);
2848
2854
  break;
2849
2855
  case "HATCH":
2850
2856
  drawHatch(ctx, entity);
@@ -2891,6 +2897,12 @@ var CanvasRenderer = class {
2891
2897
  render(doc, vt, theme, visibleLayers, selectedEntityIndex) {
2892
2898
  const ctx = this.ctx;
2893
2899
  const dpr = window.devicePixelRatio || 1;
2900
+ const stats = {
2901
+ entitiesDrawn: 0,
2902
+ entitiesSkipped: 0,
2903
+ drawCalls: 0,
2904
+ byType: {}
2905
+ };
2894
2906
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2895
2907
  ctx.fillStyle = THEMES[theme].backgroundColor;
2896
2908
  ctx.fillRect(0, 0, this.width, this.height);
@@ -2900,13 +2912,20 @@ var CanvasRenderer = class {
2900
2912
  ctx.lineJoin = "round";
2901
2913
  for (let i = 0; i < doc.entities.length; i++) {
2902
2914
  const entity = doc.entities[i];
2903
- if (!entity.visible) continue;
2904
- if (!visibleLayers.has(entity.layer)) continue;
2915
+ if (!entity.visible) {
2916
+ stats.entitiesSkipped++;
2917
+ continue;
2918
+ }
2919
+ if (!visibleLayers.has(entity.layer)) {
2920
+ stats.entitiesSkipped++;
2921
+ continue;
2922
+ }
2905
2923
  const color = resolveEntityColor(entity, doc.layers, theme);
2906
2924
  ctx.strokeStyle = color;
2907
2925
  ctx.fillStyle = color;
2908
2926
  ctx.lineWidth = pixelSize;
2909
- drawEntity(ctx, entity, doc, vt, theme, pixelSize);
2927
+ stats.entitiesDrawn++;
2928
+ drawEntity(ctx, entity, doc, vt, theme, pixelSize, stats);
2910
2929
  }
2911
2930
  if (selectedEntityIndex >= 0 && selectedEntityIndex < doc.entities.length) {
2912
2931
  const selEntity = doc.entities[selectedEntityIndex];
@@ -2916,6 +2935,7 @@ var CanvasRenderer = class {
2916
2935
  ctx.lineWidth = pixelSize * 3;
2917
2936
  drawEntity(ctx, selEntity, doc, vt, theme, pixelSize);
2918
2937
  }
2938
+ return stats;
2919
2939
  }
2920
2940
  renderEmpty(theme) {
2921
2941
  const ctx = this.ctx;
@@ -2928,6 +2948,136 @@ var CanvasRenderer = class {
2928
2948
  }
2929
2949
  };
2930
2950
 
2951
+ // src/renderer/debug-overlay.ts
2952
+ var DEFAULT_DEBUG_OPTIONS = {
2953
+ showFps: true,
2954
+ showRenderStats: true,
2955
+ showDocumentInfo: true,
2956
+ showTimings: true,
2957
+ showCamera: true,
2958
+ position: "top-left"
2959
+ };
2960
+ function resolveDebugOptions(input) {
2961
+ return { ...DEFAULT_DEBUG_OPTIONS, ...input };
2962
+ }
2963
+ function formatBytes(bytes) {
2964
+ if (bytes === 0) return "0 B";
2965
+ if (bytes < 1024) return `${bytes} B`;
2966
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2967
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2968
+ }
2969
+ function formatZoom(scale) {
2970
+ if (scale >= 1) return `${scale.toFixed(2)}x`;
2971
+ return `1:${(1 / scale).toFixed(1)}`;
2972
+ }
2973
+ var FONT = "11px monospace";
2974
+ var LINE_HEIGHT = 15;
2975
+ var SEPARATOR_HEIGHT = 8;
2976
+ var PADDING = 8;
2977
+ var MARGIN = 10;
2978
+ function renderDebugOverlay(ctx, stats, theme, options, canvasWidth, canvasHeight) {
2979
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
2980
+ const sections = [];
2981
+ if (options.showFps) {
2982
+ sections.push([
2983
+ `FPS: ${stats.fps} Frame: ${stats.frameTime.toFixed(1)}ms`
2984
+ ]);
2985
+ }
2986
+ if (options.showRenderStats) {
2987
+ const total = stats.renderStats.entitiesDrawn + stats.renderStats.entitiesSkipped;
2988
+ const lines = [
2989
+ `Drawn: ${stats.renderStats.entitiesDrawn} / ${total} Calls: ${stats.renderStats.drawCalls}`
2990
+ ];
2991
+ const types = Object.entries(stats.renderStats.byType).sort(([, a], [, b]) => b - a).slice(0, 6).map(([type, count]) => `${type}: ${count}`).join(" ");
2992
+ if (types) lines.push(types);
2993
+ sections.push(lines);
2994
+ }
2995
+ if (options.showDocumentInfo) {
2996
+ const lines = [
2997
+ `Layers: ${stats.visibleLayerCount} / ${stats.layerCount} Blocks: ${stats.blockCount}`
2998
+ ];
2999
+ if (stats.dxfVersion) lines.push(`DXF: ${stats.dxfVersion}`);
3000
+ if (stats.fileName) lines.push(`File: ${stats.fileName}`);
3001
+ if (stats.fileSize > 0) lines.push(`Size: ${formatBytes(stats.fileSize)}`);
3002
+ sections.push(lines);
3003
+ }
3004
+ if (options.showTimings) {
3005
+ const parts = [];
3006
+ if (stats.parseTime > 0) parts.push(`Parse: ${stats.parseTime.toFixed(0)}ms`);
3007
+ if (stats.spatialIndexBuildTime > 0) parts.push(`Index: ${stats.spatialIndexBuildTime.toFixed(0)}ms`);
3008
+ if (stats.totalLoadTime > 0) parts.push(`Load: ${stats.totalLoadTime.toFixed(0)}ms`);
3009
+ if (parts.length > 0) {
3010
+ sections.push([parts.join(" ")]);
3011
+ }
3012
+ }
3013
+ if (options.showCamera) {
3014
+ const b = stats.viewportBounds;
3015
+ sections.push([
3016
+ `Zoom: ${formatZoom(stats.zoom)} Pixel: ${stats.pixelSize.toFixed(2)}`,
3017
+ `View: [${b.minX.toFixed(0)}, ${b.minY.toFixed(0)}] \u2192 [${b.maxX.toFixed(0)}, ${b.maxY.toFixed(0)}]`
3018
+ ]);
3019
+ }
3020
+ if (sections.length === 0) return;
3021
+ ctx.font = FONT;
3022
+ const rows = [];
3023
+ for (let s = 0; s < sections.length; s++) {
3024
+ if (s > 0) rows.push({ text: "", isSeparator: true });
3025
+ for (const line of sections[s]) {
3026
+ rows.push({ text: line, isSeparator: false });
3027
+ }
3028
+ }
3029
+ let maxWidth = 0;
3030
+ for (const row of rows) {
3031
+ if (!row.isSeparator) {
3032
+ const w = ctx.measureText(row.text).width;
3033
+ if (w > maxWidth) maxWidth = w;
3034
+ }
3035
+ }
3036
+ const panelWidth = maxWidth + PADDING * 2;
3037
+ let panelHeight = PADDING * 2;
3038
+ for (const row of rows) {
3039
+ panelHeight += row.isSeparator ? SEPARATOR_HEIGHT : LINE_HEIGHT;
3040
+ }
3041
+ let x;
3042
+ let y;
3043
+ switch (options.position) {
3044
+ case "top-left":
3045
+ x = MARGIN;
3046
+ y = MARGIN;
3047
+ break;
3048
+ case "top-right":
3049
+ x = canvasWidth - panelWidth - MARGIN;
3050
+ y = MARGIN;
3051
+ break;
3052
+ case "bottom-left":
3053
+ x = MARGIN;
3054
+ y = canvasHeight - panelHeight - MARGIN;
3055
+ break;
3056
+ case "bottom-right":
3057
+ x = canvasWidth - panelWidth - MARGIN;
3058
+ y = canvasHeight - panelHeight - MARGIN;
3059
+ break;
3060
+ }
3061
+ const config = THEMES[theme];
3062
+ ctx.fillStyle = theme === "dark" ? "rgba(0, 0, 0, 0.75)" : "rgba(255, 255, 255, 0.85)";
3063
+ ctx.fillRect(x, y, panelWidth, panelHeight);
3064
+ ctx.strokeStyle = theme === "dark" ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)";
3065
+ ctx.lineWidth = 1;
3066
+ ctx.strokeRect(x + 0.5, y + 0.5, panelWidth - 1, panelHeight - 1);
3067
+ ctx.fillStyle = theme === "dark" ? config.defaultEntityColor : "rgba(0, 0, 0, 0.85)";
3068
+ ctx.textAlign = "left";
3069
+ ctx.textBaseline = "top";
3070
+ let cursorY = y + PADDING;
3071
+ for (const row of rows) {
3072
+ if (row.isSeparator) {
3073
+ cursorY += SEPARATOR_HEIGHT;
3074
+ } else {
3075
+ ctx.fillText(row.text, x + PADDING, cursorY);
3076
+ cursorY += LINE_HEIGHT;
3077
+ }
3078
+ }
3079
+ }
3080
+
2931
3081
  // src/viewer/layers.ts
2932
3082
  var LayerManager = class {
2933
3083
  layers = /* @__PURE__ */ new Map();
@@ -3711,11 +3861,26 @@ var CadViewer = class {
3711
3861
  currentTool;
3712
3862
  inputHandler;
3713
3863
  resizeObserver;
3864
+ formatConverters;
3714
3865
  selectedEntityIndex = -1;
3715
3866
  renderPending = false;
3716
3867
  destroyed = false;
3868
+ loadGeneration = 0;
3717
3869
  mouseScreenX = 0;
3718
3870
  mouseScreenY = 0;
3871
+ // Debug mode state
3872
+ debugEnabled = false;
3873
+ debugOptions;
3874
+ lastRenderStats = null;
3875
+ lastDebugStats = null;
3876
+ frameTimestamps = [];
3877
+ lastDoRenderTime = 0;
3878
+ lastFrameTime = 0;
3879
+ parseTime = 0;
3880
+ spatialIndexBuildTime = 0;
3881
+ loadedFileName = null;
3882
+ loadedFileSize = 0;
3883
+ debugRafId = 0;
3719
3884
  constructor(canvas, options) {
3720
3885
  this.canvas = canvas;
3721
3886
  this.options = {
@@ -3727,6 +3892,15 @@ var CadViewer = class {
3727
3892
  zoomSpeed: options?.zoomSpeed ?? 1.1,
3728
3893
  initialTool: options?.initialTool ?? "pan"
3729
3894
  };
3895
+ this.formatConverters = options?.formatConverters ?? [];
3896
+ if (options?.debug) {
3897
+ this.debugEnabled = true;
3898
+ this.debugOptions = resolveDebugOptions(
3899
+ typeof options.debug === "boolean" ? void 0 : options.debug
3900
+ );
3901
+ } else {
3902
+ this.debugOptions = resolveDebugOptions();
3903
+ }
3730
3904
  this.renderer = new CanvasRenderer(canvas);
3731
3905
  this.camera = new Camera(this.options);
3732
3906
  this.layerManager = new LayerManager();
@@ -3739,24 +3913,124 @@ var CadViewer = class {
3739
3913
  this.resizeObserver.observe(canvas);
3740
3914
  canvas.style.cursor = this.getCursorForTool(this.currentTool);
3741
3915
  this.requestRender();
3916
+ if (this.debugEnabled) {
3917
+ this.startDebugLoop();
3918
+ }
3742
3919
  }
3743
3920
  // === Loading ===
3921
+ /**
3922
+ * Throws if the viewer has been destroyed.
3923
+ * Call at the start of any public method that mutates state.
3924
+ */
3925
+ guardDestroyed() {
3926
+ if (this.destroyed) {
3927
+ throw new Error("CadViewer: cannot call methods on a destroyed instance.");
3928
+ }
3929
+ }
3930
+ /**
3931
+ * Run registered format converters on a buffer.
3932
+ * Returns the converted DXF string if a converter matched, or null otherwise.
3933
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
3934
+ */
3935
+ async runConverters(buffer) {
3936
+ for (const converter of this.formatConverters) {
3937
+ let detected = false;
3938
+ try {
3939
+ detected = converter.detect(buffer);
3940
+ } catch {
3941
+ continue;
3942
+ }
3943
+ if (detected) {
3944
+ return converter.convert(buffer);
3945
+ }
3946
+ }
3947
+ return null;
3948
+ }
3949
+ /**
3950
+ * Load a CAD file from a browser File object.
3951
+ * Automatically detects the format using registered converters (e.g. DWG).
3952
+ * Falls back to DXF parsing if no converter matches.
3953
+ */
3744
3954
  async loadFile(file) {
3955
+ this.guardDestroyed();
3956
+ const generation = ++this.loadGeneration;
3957
+ this.loadedFileName = file.name;
3958
+ this.loadedFileSize = file.size;
3745
3959
  const buffer = await file.arrayBuffer();
3746
- this.loadArrayBuffer(buffer);
3960
+ if (this.destroyed || generation !== this.loadGeneration) return;
3961
+ const dxfString = await this.runConverters(buffer);
3962
+ if (this.destroyed || generation !== this.loadGeneration) return;
3963
+ const t0 = performance.now();
3964
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3965
+ this.parseTime = performance.now() - t0;
3966
+ this.onDocumentLoaded();
3747
3967
  }
3968
+ /**
3969
+ * Load a CAD file from an ArrayBuffer with format converter support.
3970
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
3971
+ * checks registered FormatConverters for non-DXF formats.
3972
+ */
3973
+ async loadBuffer(buffer) {
3974
+ this.guardDestroyed();
3975
+ const generation = ++this.loadGeneration;
3976
+ this.loadedFileName = null;
3977
+ this.loadedFileSize = buffer.byteLength;
3978
+ const dxfString = await this.runConverters(buffer);
3979
+ if (this.destroyed || generation !== this.loadGeneration) return;
3980
+ const t0 = performance.now();
3981
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3982
+ this.parseTime = performance.now() - t0;
3983
+ this.onDocumentLoaded();
3984
+ }
3985
+ /**
3986
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
3987
+ * Useful for custom parsers or pre-processed documents.
3988
+ */
3989
+ loadDocument(doc) {
3990
+ this.guardDestroyed();
3991
+ ++this.loadGeneration;
3992
+ this.loadedFileName = null;
3993
+ this.loadedFileSize = 0;
3994
+ this.parseTime = 0;
3995
+ if (!doc || !Array.isArray(doc.entities) || !(doc.layers instanceof Map)) {
3996
+ throw new Error("CadViewer: invalid DxfDocument \u2014 expected entities array and layers Map.");
3997
+ }
3998
+ this.doc = doc;
3999
+ this.onDocumentLoaded();
4000
+ }
4001
+ /**
4002
+ * Load a DXF string directly (synchronous, no format conversion).
4003
+ */
3748
4004
  loadString(dxf) {
4005
+ this.guardDestroyed();
4006
+ ++this.loadGeneration;
4007
+ this.loadedFileName = null;
4008
+ this.loadedFileSize = dxf.length;
4009
+ const t0 = performance.now();
3749
4010
  this.doc = parseDxf(dxf);
4011
+ this.parseTime = performance.now() - t0;
3750
4012
  this.onDocumentLoaded();
3751
4013
  }
4014
+ /**
4015
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
4016
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
4017
+ */
3752
4018
  loadArrayBuffer(buffer) {
4019
+ this.guardDestroyed();
4020
+ ++this.loadGeneration;
4021
+ this.loadedFileName = null;
4022
+ this.loadedFileSize = buffer.byteLength;
4023
+ const t0 = performance.now();
3753
4024
  this.doc = parseDxf(buffer);
4025
+ this.parseTime = performance.now() - t0;
3754
4026
  this.onDocumentLoaded();
3755
4027
  }
3756
4028
  /**
3757
4029
  * Clear the current document and reset all state without destroying the viewer.
3758
4030
  */
3759
4031
  clearDocument() {
4032
+ this.guardDestroyed();
4033
+ ++this.loadGeneration;
3760
4034
  this.doc = null;
3761
4035
  this.selectedEntityIndex = -1;
3762
4036
  this.spatialIndex.clear();
@@ -3767,7 +4041,9 @@ var CadViewer = class {
3767
4041
  onDocumentLoaded() {
3768
4042
  if (!this.doc) return;
3769
4043
  this.layerManager.setLayers(this.doc.layers);
4044
+ const t0 = performance.now();
3770
4045
  this.spatialIndex.build(this.doc.entities);
4046
+ this.spatialIndexBuildTime = performance.now() - t0;
3771
4047
  this.selectedEntityIndex = -1;
3772
4048
  this.measureTool.deactivate();
3773
4049
  if (this.currentTool === "measure") {
@@ -3777,6 +4053,7 @@ var CadViewer = class {
3777
4053
  }
3778
4054
  // === Camera Controls ===
3779
4055
  fitToView() {
4056
+ this.guardDestroyed();
3780
4057
  if (!this.doc) return;
3781
4058
  const bounds = this.computeDocumentBounds();
3782
4059
  if (!bounds) return;
@@ -3788,6 +4065,7 @@ var CadViewer = class {
3788
4065
  this.emitter.emit("viewchange", this.camera.getTransform());
3789
4066
  }
3790
4067
  zoomTo(scale) {
4068
+ this.guardDestroyed();
3791
4069
  const rect = this.canvas.getBoundingClientRect();
3792
4070
  const centerX = rect.width / 2;
3793
4071
  const centerY = rect.height / 2;
@@ -3797,6 +4075,7 @@ var CadViewer = class {
3797
4075
  this.emitter.emit("viewchange", this.camera.getTransform());
3798
4076
  }
3799
4077
  panTo(worldX, worldY) {
4078
+ this.guardDestroyed();
3800
4079
  const rect = this.canvas.getBoundingClientRect();
3801
4080
  const vt = this.camera.getTransform();
3802
4081
  const currentSX = worldX * vt.scale + vt.offsetX;
@@ -3819,15 +4098,18 @@ var CadViewer = class {
3819
4098
  return this.layerManager.getAllLayers();
3820
4099
  }
3821
4100
  setLayerVisible(name, visible) {
4101
+ this.guardDestroyed();
3822
4102
  this.layerManager.setVisible(name, visible);
3823
4103
  this.requestRender();
3824
4104
  }
3825
4105
  setLayerColor(name, color) {
4106
+ this.guardDestroyed();
3826
4107
  this.layerManager.setColorOverride(name, color);
3827
4108
  this.requestRender();
3828
4109
  }
3829
4110
  // === Theme ===
3830
4111
  setTheme(theme) {
4112
+ this.guardDestroyed();
3831
4113
  this.options.theme = theme;
3832
4114
  this.requestRender();
3833
4115
  }
@@ -3835,11 +4117,13 @@ var CadViewer = class {
3835
4117
  return this.options.theme;
3836
4118
  }
3837
4119
  setBackgroundColor(color) {
4120
+ this.guardDestroyed();
3838
4121
  this.options.backgroundColor = color;
3839
4122
  this.requestRender();
3840
4123
  }
3841
4124
  // === Tools ===
3842
4125
  setTool(tool) {
4126
+ this.guardDestroyed();
3843
4127
  if (this.currentTool === "measure" && tool !== "measure") {
3844
4128
  this.measureTool.deactivate();
3845
4129
  }
@@ -3887,6 +4171,7 @@ var CadViewer = class {
3887
4171
  }
3888
4172
  destroy() {
3889
4173
  this.destroyed = true;
4174
+ this.stopDebugLoop();
3890
4175
  this.inputHandler.destroy();
3891
4176
  this.resizeObserver.disconnect();
3892
4177
  this.renderer.destroy();
@@ -3898,7 +4183,12 @@ var CadViewer = class {
3898
4183
  // === Internal (called by InputHandler) ===
3899
4184
  /** @internal */
3900
4185
  requestRender() {
3901
- if (this.renderPending || this.destroyed) return;
4186
+ if (this.destroyed) return;
4187
+ if (this.debugRafId) {
4188
+ this.doRender();
4189
+ return;
4190
+ }
4191
+ if (this.renderPending) return;
3902
4192
  this.renderPending = true;
3903
4193
  requestAnimationFrame(() => {
3904
4194
  this.renderPending = false;
@@ -3911,13 +4201,24 @@ var CadViewer = class {
3911
4201
  this.renderer.renderEmpty(this.options.theme);
3912
4202
  return;
3913
4203
  }
3914
- this.renderer.render(
4204
+ const renderStart = performance.now();
4205
+ const stats = this.renderer.render(
3915
4206
  this.doc,
3916
4207
  this.camera.getTransform(),
3917
4208
  this.options.theme,
3918
4209
  this.layerManager.getVisibleLayerNames(),
3919
4210
  this.selectedEntityIndex
3920
4211
  );
4212
+ this.lastFrameTime = performance.now() - renderStart;
4213
+ this.lastRenderStats = stats;
4214
+ const now = performance.now();
4215
+ if (now - this.lastDoRenderTime >= 3) {
4216
+ this.frameTimestamps.push(now);
4217
+ }
4218
+ this.lastDoRenderTime = now;
4219
+ while (this.frameTimestamps.length > 0 && this.frameTimestamps[0] < now - 1e3) {
4220
+ this.frameTimestamps.shift();
4221
+ }
3921
4222
  if (this.currentTool === "measure" && this.measureTool.state.phase !== "idle") {
3922
4223
  const ctx = this.renderer.getContext();
3923
4224
  renderMeasureOverlay(
@@ -3929,7 +4230,104 @@ var CadViewer = class {
3929
4230
  this.options.theme
3930
4231
  );
3931
4232
  }
4233
+ if (this.debugEnabled) {
4234
+ const ctx = this.renderer.getContext();
4235
+ const debugStats = this.buildDebugStats();
4236
+ this.lastDebugStats = debugStats;
4237
+ renderDebugOverlay(
4238
+ ctx,
4239
+ debugStats,
4240
+ this.options.theme,
4241
+ this.debugOptions,
4242
+ this.renderer.getWidth(),
4243
+ this.renderer.getHeight()
4244
+ );
4245
+ }
3932
4246
  }
4247
+ // === Debug Mode ===
4248
+ /**
4249
+ * Enable or disable the debug overlay.
4250
+ * Pass `true` for defaults, `false` to disable, or an object for granular control.
4251
+ */
4252
+ setDebug(debug) {
4253
+ this.guardDestroyed();
4254
+ if (typeof debug === "boolean") {
4255
+ this.debugEnabled = debug;
4256
+ } else {
4257
+ this.debugEnabled = true;
4258
+ this.debugOptions = resolveDebugOptions(debug);
4259
+ }
4260
+ if (this.debugEnabled) {
4261
+ this.startDebugLoop();
4262
+ } else {
4263
+ this.stopDebugLoop();
4264
+ this.requestRender();
4265
+ }
4266
+ }
4267
+ startDebugLoop() {
4268
+ if (this.debugRafId) return;
4269
+ const loop = () => {
4270
+ if (!this.debugEnabled || this.destroyed) {
4271
+ this.debugRafId = 0;
4272
+ return;
4273
+ }
4274
+ this.doRender();
4275
+ this.debugRafId = requestAnimationFrame(loop);
4276
+ };
4277
+ this.debugRafId = requestAnimationFrame(loop);
4278
+ }
4279
+ stopDebugLoop() {
4280
+ if (this.debugRafId) {
4281
+ cancelAnimationFrame(this.debugRafId);
4282
+ this.debugRafId = 0;
4283
+ }
4284
+ }
4285
+ /**
4286
+ * Get the latest debug stats snapshot, or null if debug mode is off.
4287
+ */
4288
+ getDebugStats() {
4289
+ return this.debugEnabled ? this.lastDebugStats : null;
4290
+ }
4291
+ buildDebugStats() {
4292
+ const vt = this.camera.getTransform();
4293
+ const w = this.renderer.getWidth();
4294
+ const h = this.renderer.getHeight();
4295
+ const bounds = this.computeViewportBounds(vt, w, h);
4296
+ return {
4297
+ fps: this.frameTimestamps.length,
4298
+ frameTime: this.lastFrameTime,
4299
+ renderStats: this.lastRenderStats ?? {
4300
+ entitiesDrawn: 0,
4301
+ entitiesSkipped: 0,
4302
+ drawCalls: 0,
4303
+ byType: {}
4304
+ },
4305
+ entityCount: this.doc?.entities.length ?? 0,
4306
+ layerCount: this.doc?.layers.size ?? 0,
4307
+ visibleLayerCount: this.layerManager.getVisibleLayerNames().size,
4308
+ blockCount: this.doc?.blocks.size ?? 0,
4309
+ parseTime: this.parseTime,
4310
+ spatialIndexBuildTime: this.spatialIndexBuildTime,
4311
+ totalLoadTime: this.parseTime + this.spatialIndexBuildTime,
4312
+ zoom: vt.scale,
4313
+ pixelSize: vt.scale > 0 ? 1 / vt.scale : 0,
4314
+ viewportBounds: bounds,
4315
+ fileName: this.loadedFileName,
4316
+ fileSize: this.loadedFileSize,
4317
+ dxfVersion: this.doc?.header.acadVersion ?? null
4318
+ };
4319
+ }
4320
+ computeViewportBounds(vt, w, h) {
4321
+ const [x1, y1] = screenToWorld(vt, 0, h);
4322
+ const [x2, y2] = screenToWorld(vt, w, 0);
4323
+ return {
4324
+ minX: Math.min(x1, x2),
4325
+ minY: Math.min(y1, y2),
4326
+ maxX: Math.max(x1, x2),
4327
+ maxY: Math.max(y1, y2)
4328
+ };
4329
+ }
4330
+ // === Internal (called by InputHandler) ===
3933
4331
  /** @internal */
3934
4332
  handlePan(dx, dy) {
3935
4333
  this.camera.pan(dx, dy);
@@ -4013,6 +4411,6 @@ var CadViewer = class {
4013
4411
  }
4014
4412
  };
4015
4413
 
4016
- export { CadViewer, Camera, CanvasRenderer, DxfParseError, EventEmitter, LayerManager, MeasureTool, SpatialIndex, THEMES, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
4414
+ export { CadViewer, Camera, CanvasRenderer, DxfParseError, EventEmitter, LayerManager, MeasureTool, SpatialIndex, THEMES, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderDebugOverlay, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
4017
4415
  //# sourceMappingURL=index.js.map
4018
4416
  //# sourceMappingURL=index.js.map