@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.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
|
|
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
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
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)
|
|
2904
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|