@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/README.md +36 -3
- package/dist/index.cjs +422 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -3
- package/dist/index.d.ts +133 -3
- package/dist/index.js +422 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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)
|
|
2910
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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;
|