@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.cjs CHANGED
@@ -1551,7 +1551,7 @@ function decodeInput(input) {
1551
1551
  const bytes = new Uint8Array(input);
1552
1552
  const sentinelBytes = new TextDecoder("ascii").decode(bytes.slice(0, BINARY_DXF_SENTINEL.length));
1553
1553
  if (sentinelBytes === BINARY_DXF_SENTINEL) {
1554
- throw new Error("Binary DXF format is not supported. Please export as ASCII DXF.");
1554
+ throw new DxfParseError("Binary DXF format is not supported. Please export as ASCII DXF.");
1555
1555
  }
1556
1556
  let text = new TextDecoder("utf-8").decode(input);
1557
1557
  const versionMatch = text.match(/\$ACADVER[\s\S]*?\n\s*1\s*\n\s*(\S+)/);
@@ -1622,7 +1622,7 @@ function parseDxf(input) {
1622
1622
  try {
1623
1623
  text = decodeInput(input);
1624
1624
  } catch (err) {
1625
- if (err instanceof Error && err.message.includes("Binary DXF")) {
1625
+ if (err instanceof DxfParseError) {
1626
1626
  throw err;
1627
1627
  }
1628
1628
  throw new DxfParseError("Failed to decode DXF input.", err);
@@ -2485,23 +2485,25 @@ function deBoor(degree, controlPoints, knots, t, weights) {
2485
2485
  const denom = knots[i + degree - r + 1] - knots[i];
2486
2486
  if (Math.abs(denom) < 1e-10) continue;
2487
2487
  const alpha = (t - knots[i]) / denom;
2488
+ const dj = d[j];
2489
+ const djPrev = d[j - 1];
2488
2490
  if (weights) {
2489
2491
  const w0 = w[j - 1] * (1 - alpha);
2490
2492
  const w1 = w[j] * alpha;
2491
2493
  const wSum = w0 + w1;
2492
2494
  if (Math.abs(wSum) < 1e-10) continue;
2493
- d[j].x = (d[j - 1].x * w0 + d[j].x * w1) / wSum;
2494
- d[j].y = (d[j - 1].y * w0 + d[j].y * w1) / wSum;
2495
- d[j].z = (d[j - 1].z * w0 + d[j].z * w1) / wSum;
2495
+ dj.x = (djPrev.x * w0 + dj.x * w1) / wSum;
2496
+ dj.y = (djPrev.y * w0 + dj.y * w1) / wSum;
2497
+ dj.z = (djPrev.z * w0 + dj.z * w1) / wSum;
2496
2498
  w[j] = wSum;
2497
2499
  } else {
2498
- d[j].x = (1 - alpha) * d[j - 1].x + alpha * d[j].x;
2499
- d[j].y = (1 - alpha) * d[j - 1].y + alpha * d[j].y;
2500
- d[j].z = (1 - alpha) * d[j - 1].z + alpha * d[j].z;
2500
+ dj.x = (1 - alpha) * djPrev.x + alpha * dj.x;
2501
+ dj.y = (1 - alpha) * djPrev.y + alpha * dj.y;
2502
+ dj.z = (1 - alpha) * djPrev.z + alpha * dj.z;
2501
2503
  }
2502
2504
  }
2503
2505
  }
2504
- return d[degree];
2506
+ return d[degree] ?? { x: 0, y: 0, z: 0 };
2505
2507
  }
2506
2508
  function fitPointsToPolyline(fitPoints) {
2507
2509
  if (fitPoints.length < 2) return fitPoints.map((p) => ({ x: p.x, y: p.y }));
@@ -2695,7 +2697,7 @@ function drawMText(ctx, entity, pixelSize) {
2695
2697
 
2696
2698
  // src/renderer/entities/draw-insert.ts
2697
2699
  var MAX_INSERT_DEPTH = 100;
2698
- function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2700
+ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0, stats) {
2699
2701
  if (depth > MAX_INSERT_DEPTH) return;
2700
2702
  const block = doc.blocks.get(entity.blockName);
2701
2703
  if (!block) return;
@@ -2720,9 +2722,9 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2720
2722
  ctx.fillStyle = color;
2721
2723
  ctx.lineWidth = adjustedPixelSize;
2722
2724
  if (blockEntity.type === "INSERT") {
2723
- drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1);
2725
+ drawInsert(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, depth + 1, stats);
2724
2726
  } else {
2725
- drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize);
2727
+ drawEntity(ctx, blockEntity, doc, vt, theme, adjustedPixelSize, stats);
2726
2728
  }
2727
2729
  }
2728
2730
  ctx.restore();
@@ -2731,7 +2733,7 @@ function drawInsert(ctx, entity, doc, vt, theme, pixelSize, depth = 0) {
2731
2733
  }
2732
2734
 
2733
2735
  // src/renderer/entities/draw-dimension.ts
2734
- function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2736
+ function drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats) {
2735
2737
  if (entity.blockName) {
2736
2738
  const block = doc.blocks.get(entity.blockName);
2737
2739
  if (block) {
@@ -2740,7 +2742,7 @@ function drawDimension(ctx, entity, doc, vt, theme, pixelSize) {
2740
2742
  ctx.strokeStyle = color;
2741
2743
  ctx.fillStyle = color;
2742
2744
  ctx.lineWidth = pixelSize;
2743
- drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize);
2745
+ drawEntity(ctx, blockEntity, doc, vt, theme, pixelSize, stats);
2744
2746
  }
2745
2747
  return;
2746
2748
  }
@@ -2817,7 +2819,11 @@ function drawPoint(ctx, entity, pixelSize) {
2817
2819
  }
2818
2820
 
2819
2821
  // src/renderer/entities/draw-entity.ts
2820
- 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
+ }
2821
2827
  switch (entity.type) {
2822
2828
  case "LINE":
2823
2829
  drawLine(ctx, entity);
@@ -2847,10 +2853,10 @@ function drawEntity(ctx, entity, doc, vt, theme, pixelSize) {
2847
2853
  drawMText(ctx, entity, pixelSize);
2848
2854
  break;
2849
2855
  case "INSERT":
2850
- drawInsert(ctx, entity, doc, vt, theme, pixelSize);
2856
+ drawInsert(ctx, entity, doc, vt, theme, pixelSize, 0, stats);
2851
2857
  break;
2852
2858
  case "DIMENSION":
2853
- drawDimension(ctx, entity, doc, vt, theme, pixelSize);
2859
+ drawDimension(ctx, entity, doc, vt, theme, pixelSize, stats);
2854
2860
  break;
2855
2861
  case "HATCH":
2856
2862
  drawHatch(ctx, entity);
@@ -2897,6 +2903,12 @@ var CanvasRenderer = class {
2897
2903
  render(doc, vt, theme, visibleLayers, selectedEntityIndex) {
2898
2904
  const ctx = this.ctx;
2899
2905
  const dpr = window.devicePixelRatio || 1;
2906
+ const stats = {
2907
+ entitiesDrawn: 0,
2908
+ entitiesSkipped: 0,
2909
+ drawCalls: 0,
2910
+ byType: {}
2911
+ };
2900
2912
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2901
2913
  ctx.fillStyle = THEMES[theme].backgroundColor;
2902
2914
  ctx.fillRect(0, 0, this.width, this.height);
@@ -2906,13 +2918,20 @@ var CanvasRenderer = class {
2906
2918
  ctx.lineJoin = "round";
2907
2919
  for (let i = 0; i < doc.entities.length; i++) {
2908
2920
  const entity = doc.entities[i];
2909
- if (!entity.visible) continue;
2910
- 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
+ }
2911
2929
  const color = resolveEntityColor(entity, doc.layers, theme);
2912
2930
  ctx.strokeStyle = color;
2913
2931
  ctx.fillStyle = color;
2914
2932
  ctx.lineWidth = pixelSize;
2915
- drawEntity(ctx, entity, doc, vt, theme, pixelSize);
2933
+ stats.entitiesDrawn++;
2934
+ drawEntity(ctx, entity, doc, vt, theme, pixelSize, stats);
2916
2935
  }
2917
2936
  if (selectedEntityIndex >= 0 && selectedEntityIndex < doc.entities.length) {
2918
2937
  const selEntity = doc.entities[selectedEntityIndex];
@@ -2922,6 +2941,7 @@ var CanvasRenderer = class {
2922
2941
  ctx.lineWidth = pixelSize * 3;
2923
2942
  drawEntity(ctx, selEntity, doc, vt, theme, pixelSize);
2924
2943
  }
2944
+ return stats;
2925
2945
  }
2926
2946
  renderEmpty(theme) {
2927
2947
  const ctx = this.ctx;
@@ -2934,6 +2954,136 @@ var CanvasRenderer = class {
2934
2954
  }
2935
2955
  };
2936
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
+
2937
3087
  // src/viewer/layers.ts
2938
3088
  var LayerManager = class {
2939
3089
  layers = /* @__PURE__ */ new Map();
@@ -3717,11 +3867,26 @@ var CadViewer = class {
3717
3867
  currentTool;
3718
3868
  inputHandler;
3719
3869
  resizeObserver;
3870
+ formatConverters;
3720
3871
  selectedEntityIndex = -1;
3721
3872
  renderPending = false;
3722
3873
  destroyed = false;
3874
+ loadGeneration = 0;
3723
3875
  mouseScreenX = 0;
3724
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;
3725
3890
  constructor(canvas, options) {
3726
3891
  this.canvas = canvas;
3727
3892
  this.options = {
@@ -3733,6 +3898,15 @@ var CadViewer = class {
3733
3898
  zoomSpeed: options?.zoomSpeed ?? 1.1,
3734
3899
  initialTool: options?.initialTool ?? "pan"
3735
3900
  };
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
+ }
3736
3910
  this.renderer = new CanvasRenderer(canvas);
3737
3911
  this.camera = new Camera(this.options);
3738
3912
  this.layerManager = new LayerManager();
@@ -3745,24 +3919,124 @@ var CadViewer = class {
3745
3919
  this.resizeObserver.observe(canvas);
3746
3920
  canvas.style.cursor = this.getCursorForTool(this.currentTool);
3747
3921
  this.requestRender();
3922
+ if (this.debugEnabled) {
3923
+ this.startDebugLoop();
3924
+ }
3748
3925
  }
3749
3926
  // === Loading ===
3927
+ /**
3928
+ * Throws if the viewer has been destroyed.
3929
+ * Call at the start of any public method that mutates state.
3930
+ */
3931
+ guardDestroyed() {
3932
+ if (this.destroyed) {
3933
+ throw new Error("CadViewer: cannot call methods on a destroyed instance.");
3934
+ }
3935
+ }
3936
+ /**
3937
+ * Run registered format converters on a buffer.
3938
+ * Returns the converted DXF string if a converter matched, or null otherwise.
3939
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
3940
+ */
3941
+ async runConverters(buffer) {
3942
+ for (const converter of this.formatConverters) {
3943
+ let detected = false;
3944
+ try {
3945
+ detected = converter.detect(buffer);
3946
+ } catch {
3947
+ continue;
3948
+ }
3949
+ if (detected) {
3950
+ return converter.convert(buffer);
3951
+ }
3952
+ }
3953
+ return null;
3954
+ }
3955
+ /**
3956
+ * Load a CAD file from a browser File object.
3957
+ * Automatically detects the format using registered converters (e.g. DWG).
3958
+ * Falls back to DXF parsing if no converter matches.
3959
+ */
3750
3960
  async loadFile(file) {
3961
+ this.guardDestroyed();
3962
+ const generation = ++this.loadGeneration;
3963
+ this.loadedFileName = file.name;
3964
+ this.loadedFileSize = file.size;
3751
3965
  const buffer = await file.arrayBuffer();
3752
- this.loadArrayBuffer(buffer);
3966
+ if (this.destroyed || generation !== this.loadGeneration) return;
3967
+ const dxfString = await this.runConverters(buffer);
3968
+ if (this.destroyed || generation !== this.loadGeneration) return;
3969
+ const t0 = performance.now();
3970
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3971
+ this.parseTime = performance.now() - t0;
3972
+ this.onDocumentLoaded();
3753
3973
  }
3974
+ /**
3975
+ * Load a CAD file from an ArrayBuffer with format converter support.
3976
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
3977
+ * checks registered FormatConverters for non-DXF formats.
3978
+ */
3979
+ async loadBuffer(buffer) {
3980
+ this.guardDestroyed();
3981
+ const generation = ++this.loadGeneration;
3982
+ this.loadedFileName = null;
3983
+ this.loadedFileSize = buffer.byteLength;
3984
+ const dxfString = await this.runConverters(buffer);
3985
+ if (this.destroyed || generation !== this.loadGeneration) return;
3986
+ const t0 = performance.now();
3987
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3988
+ this.parseTime = performance.now() - t0;
3989
+ this.onDocumentLoaded();
3990
+ }
3991
+ /**
3992
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
3993
+ * Useful for custom parsers or pre-processed documents.
3994
+ */
3995
+ loadDocument(doc) {
3996
+ this.guardDestroyed();
3997
+ ++this.loadGeneration;
3998
+ this.loadedFileName = null;
3999
+ this.loadedFileSize = 0;
4000
+ this.parseTime = 0;
4001
+ if (!doc || !Array.isArray(doc.entities) || !(doc.layers instanceof Map)) {
4002
+ throw new Error("CadViewer: invalid DxfDocument \u2014 expected entities array and layers Map.");
4003
+ }
4004
+ this.doc = doc;
4005
+ this.onDocumentLoaded();
4006
+ }
4007
+ /**
4008
+ * Load a DXF string directly (synchronous, no format conversion).
4009
+ */
3754
4010
  loadString(dxf) {
4011
+ this.guardDestroyed();
4012
+ ++this.loadGeneration;
4013
+ this.loadedFileName = null;
4014
+ this.loadedFileSize = dxf.length;
4015
+ const t0 = performance.now();
3755
4016
  this.doc = parseDxf(dxf);
4017
+ this.parseTime = performance.now() - t0;
3756
4018
  this.onDocumentLoaded();
3757
4019
  }
4020
+ /**
4021
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
4022
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
4023
+ */
3758
4024
  loadArrayBuffer(buffer) {
4025
+ this.guardDestroyed();
4026
+ ++this.loadGeneration;
4027
+ this.loadedFileName = null;
4028
+ this.loadedFileSize = buffer.byteLength;
4029
+ const t0 = performance.now();
3759
4030
  this.doc = parseDxf(buffer);
4031
+ this.parseTime = performance.now() - t0;
3760
4032
  this.onDocumentLoaded();
3761
4033
  }
3762
4034
  /**
3763
4035
  * Clear the current document and reset all state without destroying the viewer.
3764
4036
  */
3765
4037
  clearDocument() {
4038
+ this.guardDestroyed();
4039
+ ++this.loadGeneration;
3766
4040
  this.doc = null;
3767
4041
  this.selectedEntityIndex = -1;
3768
4042
  this.spatialIndex.clear();
@@ -3773,7 +4047,9 @@ var CadViewer = class {
3773
4047
  onDocumentLoaded() {
3774
4048
  if (!this.doc) return;
3775
4049
  this.layerManager.setLayers(this.doc.layers);
4050
+ const t0 = performance.now();
3776
4051
  this.spatialIndex.build(this.doc.entities);
4052
+ this.spatialIndexBuildTime = performance.now() - t0;
3777
4053
  this.selectedEntityIndex = -1;
3778
4054
  this.measureTool.deactivate();
3779
4055
  if (this.currentTool === "measure") {
@@ -3783,6 +4059,7 @@ var CadViewer = class {
3783
4059
  }
3784
4060
  // === Camera Controls ===
3785
4061
  fitToView() {
4062
+ this.guardDestroyed();
3786
4063
  if (!this.doc) return;
3787
4064
  const bounds = this.computeDocumentBounds();
3788
4065
  if (!bounds) return;
@@ -3794,6 +4071,7 @@ var CadViewer = class {
3794
4071
  this.emitter.emit("viewchange", this.camera.getTransform());
3795
4072
  }
3796
4073
  zoomTo(scale) {
4074
+ this.guardDestroyed();
3797
4075
  const rect = this.canvas.getBoundingClientRect();
3798
4076
  const centerX = rect.width / 2;
3799
4077
  const centerY = rect.height / 2;
@@ -3803,6 +4081,7 @@ var CadViewer = class {
3803
4081
  this.emitter.emit("viewchange", this.camera.getTransform());
3804
4082
  }
3805
4083
  panTo(worldX, worldY) {
4084
+ this.guardDestroyed();
3806
4085
  const rect = this.canvas.getBoundingClientRect();
3807
4086
  const vt = this.camera.getTransform();
3808
4087
  const currentSX = worldX * vt.scale + vt.offsetX;
@@ -3825,15 +4104,18 @@ var CadViewer = class {
3825
4104
  return this.layerManager.getAllLayers();
3826
4105
  }
3827
4106
  setLayerVisible(name, visible) {
4107
+ this.guardDestroyed();
3828
4108
  this.layerManager.setVisible(name, visible);
3829
4109
  this.requestRender();
3830
4110
  }
3831
4111
  setLayerColor(name, color) {
4112
+ this.guardDestroyed();
3832
4113
  this.layerManager.setColorOverride(name, color);
3833
4114
  this.requestRender();
3834
4115
  }
3835
4116
  // === Theme ===
3836
4117
  setTheme(theme) {
4118
+ this.guardDestroyed();
3837
4119
  this.options.theme = theme;
3838
4120
  this.requestRender();
3839
4121
  }
@@ -3841,11 +4123,13 @@ var CadViewer = class {
3841
4123
  return this.options.theme;
3842
4124
  }
3843
4125
  setBackgroundColor(color) {
4126
+ this.guardDestroyed();
3844
4127
  this.options.backgroundColor = color;
3845
4128
  this.requestRender();
3846
4129
  }
3847
4130
  // === Tools ===
3848
4131
  setTool(tool) {
4132
+ this.guardDestroyed();
3849
4133
  if (this.currentTool === "measure" && tool !== "measure") {
3850
4134
  this.measureTool.deactivate();
3851
4135
  }
@@ -3893,6 +4177,7 @@ var CadViewer = class {
3893
4177
  }
3894
4178
  destroy() {
3895
4179
  this.destroyed = true;
4180
+ this.stopDebugLoop();
3896
4181
  this.inputHandler.destroy();
3897
4182
  this.resizeObserver.disconnect();
3898
4183
  this.renderer.destroy();
@@ -3904,7 +4189,12 @@ var CadViewer = class {
3904
4189
  // === Internal (called by InputHandler) ===
3905
4190
  /** @internal */
3906
4191
  requestRender() {
3907
- 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;
3908
4198
  this.renderPending = true;
3909
4199
  requestAnimationFrame(() => {
3910
4200
  this.renderPending = false;
@@ -3917,13 +4207,24 @@ var CadViewer = class {
3917
4207
  this.renderer.renderEmpty(this.options.theme);
3918
4208
  return;
3919
4209
  }
3920
- this.renderer.render(
4210
+ const renderStart = performance.now();
4211
+ const stats = this.renderer.render(
3921
4212
  this.doc,
3922
4213
  this.camera.getTransform(),
3923
4214
  this.options.theme,
3924
4215
  this.layerManager.getVisibleLayerNames(),
3925
4216
  this.selectedEntityIndex
3926
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
+ }
3927
4228
  if (this.currentTool === "measure" && this.measureTool.state.phase !== "idle") {
3928
4229
  const ctx = this.renderer.getContext();
3929
4230
  renderMeasureOverlay(
@@ -3935,7 +4236,104 @@ var CadViewer = class {
3935
4236
  this.options.theme
3936
4237
  );
3937
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
+ }
3938
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
+ };
4335
+ }
4336
+ // === Internal (called by InputHandler) ===
3939
4337
  /** @internal */
3940
4338
  handlePan(dx, dy) {
3941
4339
  this.camera.pan(dx, dy);
@@ -4038,6 +4436,7 @@ exports.findSnaps = findSnaps;
4038
4436
  exports.fitToView = fitToView;
4039
4437
  exports.hitTest = hitTest;
4040
4438
  exports.parseDxf = parseDxf;
4439
+ exports.renderDebugOverlay = renderDebugOverlay;
4041
4440
  exports.renderMeasureOverlay = renderMeasureOverlay;
4042
4441
  exports.resolveEntityColor = resolveEntityColor;
4043
4442
  exports.screenToWorld = screenToWorld;