@file-viewer/renderer-cad 2.1.16 → 2.1.18

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.
Files changed (2) hide show
  1. package/dist/cad.js +525 -23
  2. package/package.json +3 -3
package/dist/cad.js CHANGED
@@ -1,7 +1,10 @@
1
- import { CadViewer, } from '@flyfish-dev/cad-viewer';
1
+ import { CadViewer, inferEntityKind, } from '@flyfish-dev/cad-viewer';
2
2
  import { resolveFileViewerCadAssetUrls } from '@file-viewer/core/assets';
3
3
  import { createFileViewerViewStateChange, createFileViewerViewStateChangeEmitter, createFileViewerTranslator, createFileViewerZoomChangeEmitter, registerFileViewerViewStateProvider, registerFileViewerZoomProvider, resolveFileViewerLocale, unregisterFileViewerViewStateProvider, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
4
4
  const CAD_WORKER_TIMEOUT = 120000;
5
+ const CAD_DEFAULT_FIT_PADDING = 0.92;
6
+ const CAD_BOUNDS_EPSILON = 1e-9;
7
+ const CAD_BEST_FIT_OUTLIER_RATIO = 8;
5
8
  const cadStyle = `
6
9
  .cad-shell{display:flex;height:100%;min-height:100%;flex-direction:column;background:#f5f7fb;color:#142335}
7
10
  .cad-shell *{box-sizing:border-box}
@@ -11,17 +14,19 @@ const cadStyle = `
11
14
  .cad-tools button:hover{background:rgba(31,150,110,.14);color:#0f8f62}
12
15
  .cad-zoom,.cad-meta span{color:#64748b;font-size:12px;font-weight:800;letter-spacing:0}
13
16
  .cad-meta span{border-radius:999px;padding:5px 9px;background:rgba(15,23,42,.06)}
14
- .cad-body{display:grid;min-height:0;flex:1;grid-template-columns:minmax(168px,220px) minmax(0,1fr) minmax(150px,190px);background:#eef2f7}
15
- .cad-body.without-layers{grid-template-columns:minmax(0,1fr) minmax(150px,190px)}
17
+ .cad-body{display:grid;min-height:0;flex:1;grid-template-columns:minmax(176px,236px) minmax(0,1fr) minmax(142px,176px);background:#eef2f7}
18
+ .cad-body.without-layers{grid-template-columns:minmax(0,1fr) minmax(142px,176px)}
16
19
  .cad-layers,.cad-inspector{min-height:0;overflow:auto;border-right:1px solid rgba(15,23,42,.08);background:#fff}
17
20
  .cad-layers[hidden]{display:none}
18
21
  .cad-inspector{border-right:0;border-left:1px solid rgba(15,23,42,.08);padding:14px}
19
- .cad-layers-head{position:sticky;top:0;display:flex;align-items:center;justify-content:space-between;padding:12px 12px 8px;background:#fff;color:#1f2a3d;font-size:13px;z-index:1}
20
- .cad-layers-head span{color:#7b8ca5;font-size:12px;font-weight:700}
21
- .cad-layers-list{display:flex;flex-direction:column;padding:0 0 8px}
22
- .cad-layers button{display:flex;width:calc(100% - 16px);min-height:34px;align-items:center;gap:8px;margin:0 8px 6px;border:1px solid rgba(148,163,184,.22);border-radius:8px;background:#f8fafc;color:#25344c;cursor:pointer;font-size:12px;font-weight:700;text-align:left}
23
- .cad-layers button.muted{opacity:.48}
24
- .cad-layer-color{width:10px;height:10px;flex:0 0 auto;border-radius:50%;background:#1f966e;box-shadow:inset 0 0 0 1px rgba(15,23,42,.14)}
22
+ .cad-layers-head{position:sticky;top:0;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 12px 9px;border-bottom:1px solid rgba(148,163,184,.16);background:rgba(255,255,255,.96);color:#1f2a3d;font-size:12px;z-index:1}
23
+ .cad-layers-head span{color:#7b8ca5;font-size:11px;font-weight:800;white-space:nowrap}
24
+ .cad-layers-list{display:flex;flex-direction:column;gap:2px;padding:7px 8px 10px}
25
+ .cad-layers button{display:grid;width:100%;min-height:30px;grid-template-columns:14px minmax(0,1fr);align-items:center;gap:7px;border:0;border-radius:6px;background:transparent;color:#25344c;cursor:pointer;font-size:12px;font-weight:700;text-align:left}
26
+ .cad-layers button:hover{background:#f1f5f9}
27
+ .cad-layers button.muted{color:#94a3b8}
28
+ .cad-layers button span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
29
+ .cad-layer-color{width:8px;height:8px;justify-self:center;border-radius:50%;background:#1f966e;box-shadow:0 0 0 1px rgba(15,23,42,.12),0 0 0 3px rgba(31,150,110,.1)}
25
30
  .cad-canvas-wrap{position:relative;min-width:0;min-height:0;overflow:hidden;background:linear-gradient(90deg,rgba(15,23,42,.04) 1px,transparent 1px),linear-gradient(180deg,rgba(15,23,42,.04) 1px,transparent 1px),#f8fafc;background-size:28px 28px}
26
31
  .cad-stage{position:relative;width:100%;height:100%;min-height:420px;overflow:hidden}
27
32
  .cad-stage canvas{position:absolute;inset:0;display:block;width:100%!important;height:100%!important}
@@ -99,13 +104,79 @@ const createElement = (tagName, className, text) => {
99
104
  const normalizeType = (type) => {
100
105
  return (type || 'dxf').toLowerCase();
101
106
  };
107
+ const normalizeLayerKey = (name) => name.trim().toLocaleLowerCase();
108
+ const getLayerNameScore = (name) => {
109
+ const letters = name.replace(/[^a-z]/gi, '');
110
+ if (!letters) {
111
+ return 0;
112
+ }
113
+ const isUpper = letters === letters.toLocaleUpperCase();
114
+ const isLower = letters === letters.toLocaleLowerCase();
115
+ if (!isUpper && !isLower) {
116
+ return 3;
117
+ }
118
+ return isUpper ? 2 : 1;
119
+ };
120
+ const shouldPreferLayerName = (candidate, current) => {
121
+ const nextScore = getLayerNameScore(candidate);
122
+ const currentScore = getLayerNameScore(current);
123
+ if (nextScore !== currentScore) {
124
+ return nextScore > currentScore;
125
+ }
126
+ if (candidate.length !== current.length) {
127
+ return candidate.length > current.length;
128
+ }
129
+ return candidate.localeCompare(current, undefined, { sensitivity: 'base' }) < 0;
130
+ };
131
+ const addLayerSourceName = (layer, name) => {
132
+ if (typeof name !== 'string' && typeof name !== 'number') {
133
+ return;
134
+ }
135
+ const nextName = String(name);
136
+ if (!nextName || layer.sourceNames.includes(nextName)) {
137
+ return;
138
+ }
139
+ layer.sourceNames.push(nextName);
140
+ };
102
141
  const collectLayers = (result) => {
103
142
  if (!result) {
104
143
  return [];
105
144
  }
106
- return Object.entries(result.document.layers)
107
- .map(([name, layer]) => ({ ...layer, name }))
108
- .sort((left, right) => left.name.localeCompare(right.name));
145
+ const groups = new Map();
146
+ Object.entries(result.document.layers).forEach(([key, layer]) => {
147
+ var _a, _b, _c, _d, _e;
148
+ const sourceName = typeof layer.name === 'string' && layer.name ? layer.name : key;
149
+ const groupKey = normalizeLayerKey(sourceName || key);
150
+ const current = groups.get(groupKey);
151
+ if (!current) {
152
+ groups.set(groupKey, {
153
+ ...layer,
154
+ name: sourceName || key,
155
+ sourceNames: Array.from(new Set([key, sourceName].filter((name) => typeof name === 'string' && name.length > 0))),
156
+ duplicateCount: 1,
157
+ });
158
+ return;
159
+ }
160
+ current.duplicateCount += 1;
161
+ addLayerSourceName(current, key);
162
+ addLayerSourceName(current, sourceName);
163
+ current.isVisible = current.isVisible !== false || layer.isVisible !== false;
164
+ current.isFrozen = !!current.isFrozen && !!layer.isFrozen;
165
+ current.isLocked = !!current.isLocked || !!layer.isLocked;
166
+ current.color = (_a = current.color) !== null && _a !== void 0 ? _a : layer.color;
167
+ current.colorIndex = (_b = current.colorIndex) !== null && _b !== void 0 ? _b : layer.colorIndex;
168
+ current.trueColor = (_c = current.trueColor) !== null && _c !== void 0 ? _c : layer.trueColor;
169
+ current.lineType = (_d = current.lineType) !== null && _d !== void 0 ? _d : layer.lineType;
170
+ current.lineweight = (_e = current.lineweight) !== null && _e !== void 0 ? _e : layer.lineweight;
171
+ if (shouldPreferLayerName(sourceName, current.name)) {
172
+ current.name = sourceName;
173
+ }
174
+ });
175
+ return Array.from(groups.values())
176
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, {
177
+ numeric: true,
178
+ sensitivity: 'base',
179
+ }));
109
180
  };
110
181
  const formatNumber = (value, locale = 'zh-CN') => {
111
182
  if (!Number.isFinite(value)) {
@@ -115,7 +186,350 @@ const formatNumber = (value, locale = 'zh-CN') => {
115
186
  };
116
187
  const getCadDocumentBaseUrl = (target) => {
117
188
  var _a;
118
- return ((_a = target.ownerDocument) === null || _a === void 0 ? void 0 : _a.baseURI) || 'http://localhost/';
189
+ return ((_a = target.ownerDocument) === null || _a === void 0 ? void 0 : _a.baseURI) || 'file:///';
190
+ };
191
+ const createEmptyCadBounds = () => ({
192
+ minX: Number.POSITIVE_INFINITY,
193
+ minY: Number.POSITIVE_INFINITY,
194
+ maxX: Number.NEGATIVE_INFINITY,
195
+ maxY: Number.NEGATIVE_INFINITY,
196
+ });
197
+ const isCadBoundsValid = (bounds) => {
198
+ return !!bounds &&
199
+ Number.isFinite(bounds.minX) &&
200
+ Number.isFinite(bounds.minY) &&
201
+ Number.isFinite(bounds.maxX) &&
202
+ Number.isFinite(bounds.maxY) &&
203
+ bounds.maxX >= bounds.minX &&
204
+ bounds.maxY >= bounds.minY;
205
+ };
206
+ const getCadBoundsWidth = (bounds) => bounds.maxX - bounds.minX;
207
+ const getCadBoundsHeight = (bounds) => bounds.maxY - bounds.minY;
208
+ const isCadBoundsMeaningful = (bounds) => {
209
+ return getCadBoundsWidth(bounds) > CAD_BOUNDS_EPSILON ||
210
+ getCadBoundsHeight(bounds) > CAD_BOUNDS_EPSILON;
211
+ };
212
+ const isCadPoint = (value) => {
213
+ return !!value &&
214
+ typeof value === 'object' &&
215
+ Number.isFinite(value.x) &&
216
+ Number.isFinite(value.y);
217
+ };
218
+ const addCadPointToBounds = (bounds, point) => {
219
+ if (!isCadPoint(point)) {
220
+ return;
221
+ }
222
+ bounds.minX = Math.min(bounds.minX, point.x);
223
+ bounds.minY = Math.min(bounds.minY, point.y);
224
+ bounds.maxX = Math.max(bounds.maxX, point.x);
225
+ bounds.maxY = Math.max(bounds.maxY, point.y);
226
+ };
227
+ const addCadBounds = (target, source) => {
228
+ if (!isCadBoundsValid(source)) {
229
+ return;
230
+ }
231
+ addCadPointToBounds(target, { x: source.minX, y: source.minY });
232
+ addCadPointToBounds(target, { x: source.maxX, y: source.maxY });
233
+ };
234
+ const unionCadBounds = (boundsList) => {
235
+ const union = createEmptyCadBounds();
236
+ boundsList.forEach(bounds => addCadBounds(union, bounds));
237
+ return isCadBoundsValid(union) ? union : null;
238
+ };
239
+ const getCadBoundsArea = (bounds) => {
240
+ return Math.max(getCadBoundsWidth(bounds), CAD_BOUNDS_EPSILON) *
241
+ Math.max(getCadBoundsHeight(bounds), CAD_BOUNDS_EPSILON);
242
+ };
243
+ const addCadCircleBounds = (bounds, center, radius) => {
244
+ const value = Number(radius);
245
+ if (!isCadPoint(center) || !Number.isFinite(value)) {
246
+ return;
247
+ }
248
+ const nextRadius = Math.abs(value);
249
+ addCadPointToBounds(bounds, { x: center.x - nextRadius, y: center.y - nextRadius });
250
+ addCadPointToBounds(bounds, { x: center.x + nextRadius, y: center.y + nextRadius });
251
+ };
252
+ const addCadPointListToBounds = (bounds, points) => {
253
+ if (!Array.isArray(points)) {
254
+ return;
255
+ }
256
+ points.forEach(point => addCadPointToBounds(bounds, point));
257
+ };
258
+ const addCadPathCommandsToBounds = (bounds, commands) => {
259
+ if (!Array.isArray(commands)) {
260
+ return;
261
+ }
262
+ commands.forEach(command => {
263
+ const points = command.points;
264
+ addCadPointListToBounds(bounds, points);
265
+ });
266
+ };
267
+ const addCadTextBounds = (bounds, entity) => {
268
+ var _a, _b, _c, _d;
269
+ const anchor = entity.insertionPoint || entity.startPoint || entity.center;
270
+ if (!isCadPoint(anchor)) {
271
+ return;
272
+ }
273
+ const text = String((_b = (_a = entity.text) !== null && _a !== void 0 ? _a : entity.value) !== null && _b !== void 0 ? _b : '');
274
+ const height = Math.abs(Number((_d = (_c = entity.textHeight) !== null && _c !== void 0 ? _c : entity.height) !== null && _d !== void 0 ? _d : 1));
275
+ const lines = text.split(/\r?\n/g);
276
+ const width = Math.max(1, ...lines.map(line => line.length)) * Math.max(height, 1) * 0.62;
277
+ const blockHeight = Math.max(1, lines.length) * Math.max(height, 1) * 1.22;
278
+ addCadPointToBounds(bounds, anchor);
279
+ addCadPointToBounds(bounds, { x: anchor.x + width, y: anchor.y - blockHeight });
280
+ };
281
+ const getCadEntityAnchor = (entity) => {
282
+ var _a;
283
+ for (const key of ['startPoint', 'insertionPoint', 'center', 'point', 'location']) {
284
+ const point = entity[key];
285
+ if (isCadPoint(point)) {
286
+ return point;
287
+ }
288
+ }
289
+ const vertices = entity.vertices || entity.points;
290
+ if (Array.isArray(vertices) && isCadPoint(vertices[0])) {
291
+ return vertices[0];
292
+ }
293
+ const command = (_a = entity.commands) === null || _a === void 0 ? void 0 : _a.find(item => item.points.length > 0);
294
+ return command === null || command === void 0 ? void 0 : command.points[0];
295
+ };
296
+ const getCadEntityLocalBounds = (entity) => {
297
+ var _a, _b, _c, _d, _e;
298
+ const bounds = createEmptyCadBounds();
299
+ const type = String((_a = entity.type) !== null && _a !== void 0 ? _a : '').toUpperCase();
300
+ const kind = entity.kind || inferEntityKind(type);
301
+ if (kind === 'viewport') {
302
+ return null;
303
+ }
304
+ if (kind === 'line') {
305
+ addCadPointToBounds(bounds, entity.startPoint);
306
+ addCadPointToBounds(bounds, entity.endPoint);
307
+ }
308
+ else if (kind === 'circle' || kind === 'arc') {
309
+ addCadCircleBounds(bounds, entity.center, entity.radius);
310
+ }
311
+ else if (kind === 'polyline' || kind === 'solid' || kind === 'spline') {
312
+ addCadPointListToBounds(bounds, entity.vertices);
313
+ addCadPointListToBounds(bounds, entity.points);
314
+ addCadPointListToBounds(bounds, entity.controlPoints);
315
+ addCadPointListToBounds(bounds, entity.fitPoints);
316
+ }
317
+ else if (kind === 'ellipse') {
318
+ const center = entity.center;
319
+ const major = entity.majorAxisEndPoint;
320
+ if (isCadPoint(center) && isCadPoint(major)) {
321
+ const majorRadius = Math.hypot(major.x, major.y);
322
+ const minorRadius = majorRadius * Math.abs(Number((_b = entity.axisRatio) !== null && _b !== void 0 ? _b : 1));
323
+ addCadCircleBounds(bounds, center, Math.max(majorRadius, minorRadius));
324
+ }
325
+ }
326
+ else if (kind === 'text') {
327
+ addCadTextBounds(bounds, entity);
328
+ }
329
+ else if (kind === 'path') {
330
+ addCadPathCommandsToBounds(bounds, entity.commands);
331
+ }
332
+ else if (kind === 'hatch') {
333
+ (_c = entity.loops) === null || _c === void 0 ? void 0 : _c.forEach(loop => {
334
+ addCadPointListToBounds(bounds, loop.vertices);
335
+ addCadPathCommandsToBounds(bounds, loop.commands);
336
+ });
337
+ }
338
+ else if (kind === 'image') {
339
+ const point = entity.insertionPoint;
340
+ const width = Number((_d = entity.width) !== null && _d !== void 0 ? _d : 32);
341
+ const height = Number((_e = entity.height) !== null && _e !== void 0 ? _e : 32);
342
+ if (isCadPoint(point) && Number.isFinite(width) && Number.isFinite(height)) {
343
+ addCadPointToBounds(bounds, point);
344
+ addCadPointToBounds(bounds, { x: point.x + width, y: point.y - height });
345
+ }
346
+ }
347
+ else {
348
+ addCadPointToBounds(bounds, getCadEntityAnchor(entity));
349
+ }
350
+ return isCadBoundsValid(bounds) ? bounds : null;
351
+ };
352
+ const identityCadMatrix = () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
353
+ const translateCadMatrix = (x, y) => ({ a: 1, b: 0, c: 0, d: 1, e: x, f: y });
354
+ const scaleCadMatrix = (x, y) => ({ a: x, b: 0, c: 0, d: y, e: 0, f: 0 });
355
+ const rotateCadMatrix = (rotation) => {
356
+ const cos = Math.cos(rotation);
357
+ const sin = Math.sin(rotation);
358
+ return { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };
359
+ };
360
+ const multiplyCadMatrix = (left, right) => ({
361
+ a: left.a * right.a + left.c * right.b,
362
+ b: left.b * right.a + left.d * right.b,
363
+ c: left.a * right.c + left.c * right.d,
364
+ d: left.b * right.c + left.d * right.d,
365
+ e: left.a * right.e + left.c * right.f + left.e,
366
+ f: left.b * right.e + left.d * right.f + left.f,
367
+ });
368
+ const transformCadPoint = (point, matrix) => ({
369
+ x: point.x * matrix.a + point.y * matrix.c + matrix.e,
370
+ y: point.x * matrix.b + point.y * matrix.d + matrix.f,
371
+ });
372
+ const transformCadBounds = (bounds, matrix) => {
373
+ const next = createEmptyCadBounds();
374
+ [
375
+ { x: bounds.minX, y: bounds.minY },
376
+ { x: bounds.minX, y: bounds.maxY },
377
+ { x: bounds.maxX, y: bounds.minY },
378
+ { x: bounds.maxX, y: bounds.maxY },
379
+ ].forEach(point => addCadPointToBounds(next, transformCadPoint(point, matrix)));
380
+ return isCadBoundsValid(next) ? next : null;
381
+ };
382
+ const createCadInsertMatrix = (entity, blockBasePoint = { x: 0, y: 0 }) => {
383
+ var _a, _b, _c, _d, _e;
384
+ const insertion = entity.insertionPoint || { x: 0, y: 0 };
385
+ const scale = entity.scale;
386
+ const scaleX = Number(typeof scale === 'object' && scale ? (_a = scale.x) !== null && _a !== void 0 ? _a : 1 : (_b = entity.scaleX) !== null && _b !== void 0 ? _b : 1);
387
+ const scaleY = Number(typeof scale === 'object' && scale ? (_c = scale.y) !== null && _c !== void 0 ? _c : scaleX : (_d = entity.scaleY) !== null && _d !== void 0 ? _d : scaleX);
388
+ const rotation = Number((_e = entity.rotation) !== null && _e !== void 0 ? _e : 0);
389
+ let matrix = translateCadMatrix(isCadPoint(insertion) ? insertion.x : 0, isCadPoint(insertion) ? insertion.y : 0);
390
+ matrix = multiplyCadMatrix(matrix, rotateCadMatrix(Number.isFinite(rotation) ? rotation : 0));
391
+ matrix = multiplyCadMatrix(matrix, scaleCadMatrix(Number.isFinite(scaleX) ? scaleX : 1, Number.isFinite(scaleY) ? scaleY : 1));
392
+ matrix = multiplyCadMatrix(matrix, translateCadMatrix(-blockBasePoint.x, -blockBasePoint.y));
393
+ return matrix;
394
+ };
395
+ const findCadLayer = (document, name) => {
396
+ if (typeof name !== 'string' && typeof name !== 'number') {
397
+ return undefined;
398
+ }
399
+ const key = String(name);
400
+ return document.layers[key] ||
401
+ document.layers[key.toLocaleLowerCase()] ||
402
+ Object.values(document.layers).find(layer => normalizeLayerKey(layer.name) === normalizeLayerKey(key));
403
+ };
404
+ const findCadBlock = (document, name) => {
405
+ if (typeof name !== 'string' && typeof name !== 'number') {
406
+ return undefined;
407
+ }
408
+ const key = String(name);
409
+ return document.blocks[key] ||
410
+ document.blocks[key.toLocaleLowerCase()] ||
411
+ Object.values(document.blocks).find(block => normalizeLayerKey(block.name) === normalizeLayerKey(key));
412
+ };
413
+ const isCadEntityVisible = (document, entity) => {
414
+ if (entity.isVisible === false) {
415
+ return false;
416
+ }
417
+ const layer = findCadLayer(document, entity.layer);
418
+ return layer ? layer.isVisible !== false && !layer.isFrozen : true;
419
+ };
420
+ const collectCadVisibleEntityBounds = (document, maxInsertDepth) => {
421
+ const boundsList = [];
422
+ const visit = (entity, depth, matrix) => {
423
+ var _a, _b;
424
+ if (!isCadEntityVisible(document, entity)) {
425
+ return;
426
+ }
427
+ const type = String((_a = entity.type) !== null && _a !== void 0 ? _a : '').toUpperCase();
428
+ const kind = entity.kind || inferEntityKind(type);
429
+ if (kind === 'insert') {
430
+ const block = findCadBlock(document, (_b = entity.blockName) !== null && _b !== void 0 ? _b : entity.name);
431
+ if (block && depth < maxInsertDepth) {
432
+ const nextMatrix = multiplyCadMatrix(matrix, createCadInsertMatrix(entity, block.basePoint || { x: 0, y: 0 }));
433
+ block.entities.forEach(child => visit(child, depth + 1, nextMatrix));
434
+ return;
435
+ }
436
+ }
437
+ const localBounds = getCadEntityLocalBounds(entity);
438
+ if (!localBounds) {
439
+ return;
440
+ }
441
+ const nextBounds = transformCadBounds(localBounds, matrix);
442
+ if (nextBounds) {
443
+ boundsList.push(nextBounds);
444
+ }
445
+ };
446
+ document.entities.forEach(entity => visit(entity, 0, identityCadMatrix()));
447
+ return boundsList;
448
+ };
449
+ const inflateCadDegenerateBounds = (bounds) => {
450
+ const width = getCadBoundsWidth(bounds);
451
+ const height = getCadBoundsHeight(bounds);
452
+ if (width > CAD_BOUNDS_EPSILON && height > CAD_BOUNDS_EPSILON) {
453
+ return bounds;
454
+ }
455
+ const centerX = (bounds.minX + bounds.maxX) / 2;
456
+ const centerY = (bounds.minY + bounds.maxY) / 2;
457
+ const pad = Math.max(width, height, 1) * 0.5;
458
+ return {
459
+ minX: centerX - (width > CAD_BOUNDS_EPSILON ? width / 2 : pad),
460
+ maxX: centerX + (width > CAD_BOUNDS_EPSILON ? width / 2 : pad),
461
+ minY: centerY - (height > CAD_BOUNDS_EPSILON ? height / 2 : pad),
462
+ maxY: centerY + (height > CAD_BOUNDS_EPSILON ? height / 2 : pad),
463
+ };
464
+ };
465
+ const selectCadBestFitBounds = (boundsList) => {
466
+ let candidates = boundsList.filter(isCadBoundsValid);
467
+ const meaningful = candidates.filter(isCadBoundsMeaningful);
468
+ if (meaningful.length > 0) {
469
+ candidates = meaningful;
470
+ }
471
+ const globalBounds = unionCadBounds(candidates);
472
+ if (!globalBounds || candidates.length < 8) {
473
+ return globalBounds ? inflateCadDegenerateBounds(globalBounds) : null;
474
+ }
475
+ const centers = candidates.map(bounds => ({
476
+ bounds,
477
+ x: (bounds.minX + bounds.maxX) / 2,
478
+ y: (bounds.minY + bounds.maxY) / 2,
479
+ }));
480
+ const trim = Math.max(1, Math.floor(centers.length * 0.05));
481
+ const sortedX = [...centers].sort((left, right) => left.x - right.x);
482
+ const sortedY = [...centers].sort((left, right) => left.y - right.y);
483
+ const minX = sortedX[Math.min(trim, sortedX.length - 1)].x;
484
+ const maxX = sortedX[Math.max(sortedX.length - 1 - trim, 0)].x;
485
+ const minY = sortedY[Math.min(trim, sortedY.length - 1)].y;
486
+ const maxY = sortedY[Math.max(sortedY.length - 1 - trim, 0)].y;
487
+ const trimmed = centers
488
+ .filter(item => item.x >= minX && item.x <= maxX && item.y >= minY && item.y <= maxY)
489
+ .map(item => item.bounds);
490
+ const minKeep = Math.max(4, Math.floor(candidates.length * 0.55));
491
+ const trimmedBounds = trimmed.length >= minKeep ? unionCadBounds(trimmed) : null;
492
+ if (!trimmedBounds) {
493
+ return inflateCadDegenerateBounds(globalBounds);
494
+ }
495
+ const globalSpan = Math.max(getCadBoundsWidth(globalBounds), getCadBoundsHeight(globalBounds), CAD_BOUNDS_EPSILON);
496
+ const trimmedSpan = Math.max(getCadBoundsWidth(trimmedBounds), getCadBoundsHeight(trimmedBounds), CAD_BOUNDS_EPSILON);
497
+ const areaRatio = getCadBoundsArea(globalBounds) / getCadBoundsArea(trimmedBounds);
498
+ if (globalSpan / trimmedSpan >= CAD_BEST_FIT_OUTLIER_RATIO || areaRatio >= CAD_BEST_FIT_OUTLIER_RATIO ** 2) {
499
+ return inflateCadDegenerateBounds(trimmedBounds);
500
+ }
501
+ return inflateCadDegenerateBounds(globalBounds);
502
+ };
503
+ const resolveCadFitPadding = (options) => {
504
+ const padding = Number(options.fitPadding);
505
+ if (!Number.isFinite(padding) || padding <= 0) {
506
+ return CAD_DEFAULT_FIT_PADDING;
507
+ }
508
+ return Math.min(1, Math.max(0.2, padding));
509
+ };
510
+ const resolveCadFitBounds = (document, rendererBounds, options) => {
511
+ var _a;
512
+ if (!document || options.fitMode === 'native') {
513
+ return rendererBounds && isCadBoundsValid(rendererBounds)
514
+ ? inflateCadDegenerateBounds(rendererBounds)
515
+ : null;
516
+ }
517
+ const configuredDepth = Number((_a = options.maxInsertDepth) !== null && _a !== void 0 ? _a : 16);
518
+ const maxInsertDepth = Number.isFinite(configuredDepth) ? Math.max(0, configuredDepth) : 16;
519
+ const entityBounds = collectCadVisibleEntityBounds(document, maxInsertDepth);
520
+ return selectCadBestFitBounds(entityBounds) ||
521
+ (rendererBounds && isCadBoundsValid(rendererBounds) ? inflateCadDegenerateBounds(rendererBounds) : null);
522
+ };
523
+ const resolveLayerSourceNames = (document, layer) => {
524
+ const normalized = normalizeLayerKey(layer.name);
525
+ const names = new Set(layer.sourceNames);
526
+ Object.entries(document.layers).forEach(([key, source]) => {
527
+ if (normalizeLayerKey(key) === normalized || normalizeLayerKey(source.name) === normalized) {
528
+ names.add(key);
529
+ names.add(source.name);
530
+ }
531
+ });
532
+ return Array.from(names);
119
533
  };
120
534
  export default async function renderCad(buffer, target, type = 'dxf', context) {
121
535
  var _a;
@@ -133,6 +547,7 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
133
547
  let viewer = null;
134
548
  let resizeObserver = null;
135
549
  let abortController = null;
550
+ let fitViewActive = true;
136
551
  let disposed = false;
137
552
  const style = createStyle();
138
553
  const shell = createElement('div', 'cad-shell');
@@ -247,7 +662,12 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
247
662
  layersList.replaceChildren(...layers.map(layer => {
248
663
  const button = createElement('button');
249
664
  button.type = 'button';
250
- button.classList.toggle('muted', layer.isVisible === false || !!layer.isFrozen);
665
+ const layerVisible = layer.isVisible !== false && !layer.isFrozen;
666
+ button.classList.toggle('muted', !layerVisible);
667
+ button.title = layer.duplicateCount > 1
668
+ ? `${layer.name} (${layer.duplicateCount} merged)`
669
+ : layer.name;
670
+ button.setAttribute('aria-pressed', String(layerVisible));
251
671
  const color = createElement('span', 'cad-layer-color');
252
672
  if (typeof layer.color === 'string') {
253
673
  color.style.background = layer.color;
@@ -255,16 +675,22 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
255
675
  button.append(color, createElement('span', undefined, layer.name));
256
676
  button.addEventListener('click', () => {
257
677
  const document = viewer === null || viewer === void 0 ? void 0 : viewer.getDocument();
258
- if (!(document === null || document === void 0 ? void 0 : document.layers[layer.name])) {
678
+ if (!document) {
259
679
  return;
260
680
  }
261
- const current = document.layers[layer.name];
262
- current.isVisible = current.isVisible === false;
681
+ const nextVisible = !layerVisible;
682
+ resolveLayerSourceNames(document, layer).forEach(sourceName => {
683
+ const current = document.layers[sourceName] || findCadLayer(document, sourceName);
684
+ if (current) {
685
+ current.isVisible = nextVisible;
686
+ }
687
+ });
263
688
  loadResult = (viewer === null || viewer === void 0 ? void 0 : viewer.setDocument(document, buildFileName())) || null;
264
689
  layers = collectLayers(loadResult);
265
690
  syncUi();
266
691
  queueMicrotask(() => {
267
- viewer === null || viewer === void 0 ? void 0 : viewer.fit();
692
+ fitViewActive = true;
693
+ applyCadFit();
268
694
  cadZoomEmitter.emit();
269
695
  emitCadViewStateChange('layer-toggle', 'user');
270
696
  });
@@ -295,19 +721,83 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
295
721
  progressMessage = `${prefix}${progress.message}${percent}`;
296
722
  syncState();
297
723
  };
298
- const fitToView = () => {
299
- viewer === null || viewer === void 0 ? void 0 : viewer.fit();
724
+ const applyCadFit = () => {
725
+ var _a, _b, _c, _d;
726
+ if (!viewer) {
727
+ return false;
728
+ }
729
+ if ((_a = viewer.isNativeRendererActive) === null || _a === void 0 ? void 0 : _a.call(viewer)) {
730
+ viewer.fit();
731
+ return true;
732
+ }
733
+ const renderer = viewer.renderer;
734
+ const rendererBounds = (_b = renderer.getBounds) === null || _b === void 0 ? void 0 : _b.call(renderer);
735
+ const fitBounds = resolveCadFitBounds(viewer.getDocument(), rendererBounds, options);
736
+ if (!fitBounds || !renderer.setViewState) {
737
+ viewer.fit();
738
+ return true;
739
+ }
740
+ const width = Math.max(1, stage.clientWidth || viewer.canvas.clientWidth || target.clientWidth);
741
+ const height = Math.max(1, stage.clientHeight || viewer.canvas.clientHeight || target.clientHeight);
742
+ const boundsWidth = Math.max(getCadBoundsWidth(fitBounds), CAD_BOUNDS_EPSILON);
743
+ const boundsHeight = Math.max(getCadBoundsHeight(fitBounds), CAD_BOUNDS_EPSILON);
744
+ const canvasOptions = options.canvasOptions || {};
745
+ const minScale = Number((_c = canvasOptions.minScale) !== null && _c !== void 0 ? _c : CAD_BOUNDS_EPSILON);
746
+ const maxScale = Number((_d = canvasOptions.maxScale) !== null && _d !== void 0 ? _d : 1e9);
747
+ const unclampedScale = Math.min(width / boundsWidth, height / boundsHeight) * resolveCadFitPadding(options);
748
+ const nextScale = Math.min(Number.isFinite(maxScale) ? maxScale : 1e9, Math.max(Number.isFinite(minScale) ? minScale : CAD_BOUNDS_EPSILON, unclampedScale));
749
+ const nextView = {
750
+ centerX: (fitBounds.minX + fitBounds.maxX) / 2,
751
+ centerY: (fitBounds.minY + fitBounds.maxY) / 2,
752
+ scale: nextScale,
753
+ };
754
+ // Keep cad-viewer's zoom label at 100% for File Viewer's best-fit bounds.
755
+ renderer.fitScale = nextScale;
756
+ renderer.setViewState(nextView);
757
+ return true;
758
+ };
759
+ const fitToView = (source = 'user', action = 'zoom-reset') => {
760
+ fitViewActive = true;
761
+ applyCadFit();
762
+ cadZoomEmitter.emit();
763
+ syncState();
764
+ emitCadViewStateChange(action, source);
765
+ };
766
+ const applyCadFitRequest = (request) => {
767
+ fitViewActive = true;
768
+ const applied = applyCadFit();
769
+ if (!applied) {
770
+ return {
771
+ applied: false,
772
+ mode: request.mode,
773
+ resize: request.resize,
774
+ source: request.source,
775
+ reason: 'not-ready',
776
+ provider: 'view-state',
777
+ };
778
+ }
300
779
  cadZoomEmitter.emit();
301
780
  syncState();
302
- emitCadViewStateChange('zoom-reset', 'user');
781
+ const state = emitCadViewStateChange('fit', request.source);
782
+ return {
783
+ applied: true,
784
+ mode: request.mode,
785
+ resize: request.resize,
786
+ scale: state.scale,
787
+ source: request.source,
788
+ provider: 'view-state',
789
+ state,
790
+ };
303
791
  };
304
792
  const zoomIn = () => {
793
+ fitViewActive = false;
305
794
  viewer === null || viewer === void 0 ? void 0 : viewer.zoomIn();
306
795
  cadZoomEmitter.emit();
307
796
  syncState();
308
797
  emitCadViewStateChange('zoom-in', 'user');
309
798
  };
310
799
  const zoomOut = () => {
800
+ fitViewActive = false;
311
801
  viewer === null || viewer === void 0 ? void 0 : viewer.zoomOut();
312
802
  cadZoomEmitter.emit();
313
803
  syncState();
@@ -326,6 +816,7 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
326
816
  fitToView();
327
817
  return getCadZoomState();
328
818
  },
819
+ fit: applyCadFitRequest,
329
820
  getState: getCadZoomState,
330
821
  subscribe: cadZoomEmitter.subscribe,
331
822
  });
@@ -340,9 +831,10 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
340
831
  }
341
832
  return getCadViewState();
342
833
  },
834
+ fit: applyCadFitRequest,
343
835
  subscribe: cadViewStateEmitter.subscribe,
344
836
  });
345
- fitButton.addEventListener('click', fitToView);
837
+ fitButton.addEventListener('click', () => fitToView());
346
838
  zoomInButton.addEventListener('click', zoomIn);
347
839
  zoomOutButton.addEventListener('click', zoomOut);
348
840
  const createViewer = () => {
@@ -424,6 +916,7 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
424
916
  renderStats = null;
425
917
  viewState = null;
426
918
  layers = [];
919
+ fitViewActive = true;
427
920
  syncUi();
428
921
  abortController === null || abortController === void 0 ? void 0 : abortController.abort();
429
922
  const controller = new AbortController();
@@ -448,7 +941,7 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
448
941
  status = 'ready';
449
942
  syncUi();
450
943
  queueMicrotask(() => {
451
- viewer === null || viewer === void 0 ? void 0 : viewer.fit();
944
+ applyCadFit();
452
945
  cadZoomEmitter.emit();
453
946
  emitCadViewStateChange('init', 'viewer');
454
947
  syncState();
@@ -467,6 +960,15 @@ export default async function renderCad(buffer, target, type = 'dxf', context) {
467
960
  if (typeof ResizeObserver !== 'undefined') {
468
961
  resizeObserver = new ResizeObserver(() => {
469
962
  viewer === null || viewer === void 0 ? void 0 : viewer.resize();
963
+ if (fitViewActive) {
964
+ requestAnimationFrame(() => {
965
+ if (!disposed && fitViewActive) {
966
+ applyCadFit();
967
+ cadZoomEmitter.emit();
968
+ syncState();
969
+ }
970
+ });
971
+ }
470
972
  });
471
973
  resizeObserver.observe(stage);
472
974
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-cad",
3
- "version": "2.1.16",
3
+ "version": "2.1.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone CAD renderer plugin for Flyfish File Viewer powered by @flyfish-dev/cad-viewer.",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "funding": {
38
38
  "type": "individual",
39
- "url": "https://dev.flyfish.group/shop"
39
+ "url": "https://dev.flyfish.group/donate?source=npm"
40
40
  },
41
41
  "main": "./dist/index.js",
42
42
  "module": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "LICENSE"
57
57
  ],
58
58
  "dependencies": {
59
- "@file-viewer/core": "^2.1.16",
59
+ "@file-viewer/core": "^2.1.18",
60
60
  "@flyfish-dev/cad-viewer": "^0.6.4"
61
61
  },
62
62
  "devDependencies": {