cyclecad 1.3.2 → 2.0.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/DRAWING_MODULE_INTEGRATION.md +633 -0
- package/README.md +124 -296
- package/app/index.html +2 -0
- package/app/js/brep-kernel.js +853 -0
- package/app/js/kernel.js +684 -0
- package/app/js/modules/assembly-module.js +582 -0
- package/app/js/modules/brep-module.js +583 -0
- package/app/js/modules/drawing-module.js +883 -0
- package/app/js/modules/operations-module.js +660 -0
- package/app/js/modules/simulation-module.js +834 -0
- package/app/js/modules/sketch-module.js +720 -0
- package/app/js/modules/step-module.js +510 -0
- package/app/js/modules/viewport-module.js +530 -0
- package/fusion360-gap-analysis.html +636 -0
- package/package.json +1 -1
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrawingModule — 2D Engineering Drawing / Documentation Workspace
|
|
3
|
+
* LEGO block for cycleCAD microkernel architecture
|
|
4
|
+
* Fusion 360 Drawing parity — the #1 most-requested missing feature
|
|
5
|
+
*
|
|
6
|
+
* Creates orthographic, section, detail, isometric, and auxiliary views
|
|
7
|
+
* with associative dimensions, GD&T annotations, and manufacturing documentation
|
|
8
|
+
*
|
|
9
|
+
* Version: 1.0.0
|
|
10
|
+
* Author: cycleCAD Team
|
|
11
|
+
* License: MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DrawingModule = {
|
|
15
|
+
id: 'drawing',
|
|
16
|
+
name: '2D Drawing',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
category: 'tool',
|
|
19
|
+
description: 'Engineering drawings with orthographic/section/detail views, dimensions, GD&T, and export',
|
|
20
|
+
dependencies: ['viewport', 'operations'],
|
|
21
|
+
memoryEstimate: 25,
|
|
22
|
+
|
|
23
|
+
// ===== STATE =====
|
|
24
|
+
state: {
|
|
25
|
+
isActive: false,
|
|
26
|
+
sheets: [], // array of { id, paperSize, scale, views[], dimensions[], annotations[], titleBlock, bom }
|
|
27
|
+
currentSheetId: null,
|
|
28
|
+
currentSheet: null,
|
|
29
|
+
svgContainer: null,
|
|
30
|
+
svgDoc: null,
|
|
31
|
+
scale: 1, // drawing scale (1:1, 1:2, 1:5, etc.)
|
|
32
|
+
paperSize: 'A3', // A0-A4, ANSI A-E
|
|
33
|
+
paperDimensions: {
|
|
34
|
+
A0: { w: 1189, h: 1682 },
|
|
35
|
+
A1: { w: 841, h: 1189 },
|
|
36
|
+
A2: { w: 594, h: 841 },
|
|
37
|
+
A3: { w: 420, h: 594 },
|
|
38
|
+
A4: { w: 297, h: 420 },
|
|
39
|
+
'ANSI A': { w: 216, h: 279 },
|
|
40
|
+
'ANSI B': { w: 279, h: 432 },
|
|
41
|
+
'ANSI C': { w: 432, h: 559 },
|
|
42
|
+
'ANSI D': { w: 559, h: 864 },
|
|
43
|
+
'ANSI E': { w: 864, h: 1118 }
|
|
44
|
+
},
|
|
45
|
+
views: new Map(), // { id -> { type, direction, projection, position, scale, svgGroup } }
|
|
46
|
+
dimensions: new Map(), // { id -> { type, entities[], value, position, tolerance, associated } }
|
|
47
|
+
annotations: new Map(), // { id -> { type, position, data, svgElement } }
|
|
48
|
+
balloons: new Map(), // { id -> { partId, position, number, svgElement } }
|
|
49
|
+
nextBalloonNumber: 1,
|
|
50
|
+
centerMarks: new Map(),
|
|
51
|
+
centerlines: new Map(),
|
|
52
|
+
leaders: new Map(),
|
|
53
|
+
selectedElement: null,
|
|
54
|
+
titleBlockTemplate: 'default', // default, iso, ansi
|
|
55
|
+
titleBlockFields: {},
|
|
56
|
+
bomData: null,
|
|
57
|
+
mode: 'view', // view, dimension, annotation, balloon, leader, centerMark
|
|
58
|
+
tempLines: [], // for line-based tools (leader, centerline)
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// ===== LEGO INTERFACE =====
|
|
62
|
+
init() {
|
|
63
|
+
window.addEventListener('drawing:create', (e) => this.create(e.detail.paperSize, e.detail.scale));
|
|
64
|
+
window.addEventListener('drawing:start', () => this.start());
|
|
65
|
+
window.addEventListener('drawing:finish', () => this.finish());
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
getUI() {
|
|
69
|
+
return `
|
|
70
|
+
<div id="drawing-workspace" style="display: none; background: #f5f5f5; overflow: auto; position: relative; flex: 1;">
|
|
71
|
+
<div id="drawing-canvas" style="background: white; margin: 20px auto; box-shadow: 0 2px 8px rgba(0,0,0,0.15); position: relative; display: inline-block;"></div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div id="drawing-toolbar" style="display: none; background: #2a2a2a; padding: 8px; border-radius: 4px; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid #444;">
|
|
75
|
+
<button data-drawing-tool="view" title="View Mode">👁</button>
|
|
76
|
+
<button data-drawing-tool="orthographic" title="Add Orthographic View">⬜</button>
|
|
77
|
+
<button data-drawing-tool="section" title="Add Section View">✂️</button>
|
|
78
|
+
<button data-drawing-tool="detail" title="Add Detail View">🔍</button>
|
|
79
|
+
<button data-drawing-tool="isometric" title="Add Isometric View">📐</button>
|
|
80
|
+
<button data-drawing-tool="auxiliary" title="Add Auxiliary View">⬀</button>
|
|
81
|
+
<div style="border-left: 1px solid #444; margin: 0 4px;"></div>
|
|
82
|
+
<button data-drawing-tool="linearDim" title="Linear Dimension (L)">—</button>
|
|
83
|
+
<button data-drawing-tool="angularDim" title="Angular Dimension (A)">∠</button>
|
|
84
|
+
<button data-drawing-tool="radialDim" title="Radial Dimension (R)">◯</button>
|
|
85
|
+
<button data-drawing-tool="diameterDim" title="Diameter Dimension (⌀)">◯</button>
|
|
86
|
+
<button data-drawing-tool="ordinateDim" title="Ordinate Dimension">📏</button>
|
|
87
|
+
<div style="border-left: 1px solid #444; margin: 0 4px;"></div>
|
|
88
|
+
<button data-drawing-tool="gdtSymbol" title="GD&T Symbol">🔤</button>
|
|
89
|
+
<button data-drawing-tool="surfaceFinish" title="Surface Finish">≈</button>
|
|
90
|
+
<button data-drawing-tool="weldSymbol" title="Weld Symbol">⧂</button>
|
|
91
|
+
<button data-drawing-tool="centerMark" title="Center Mark">⊕</button>
|
|
92
|
+
<button data-drawing-tool="centerline" title="Centerline">⋮</button>
|
|
93
|
+
<button data-drawing-tool="leader" title="Leader with Note">→</button>
|
|
94
|
+
<button data-drawing-tool="balloon" title="Balloon (Assembly)">①</button>
|
|
95
|
+
<div style="border-left: 1px solid #444; margin: 0 4px;"></div>
|
|
96
|
+
<button id="drawing-export-btn" title="Export Drawing">💾</button>
|
|
97
|
+
<button id="drawing-addbom-btn" title="Add BOM Table">📋</button>
|
|
98
|
+
<button id="drawing-finish-btn" style="margin-left: 16px; background: #00aa00; color: white;" title="Exit Drawing">✕</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div id="drawing-properties" style="display: none; position: fixed; right: 0; top: 0; width: 280px; height: 100%; background: #2a2a2a; border-left: 1px solid #444; overflow-y: auto; padding: 12px; z-index: 500; color: #aaa; font-size: 12px;">
|
|
102
|
+
<h3 style="color: #fff; margin-top: 0; font-size: 14px;">Drawing Properties</h3>
|
|
103
|
+
<div style="margin-bottom: 12px;">
|
|
104
|
+
<label style="display: block; margin-bottom: 4px;">Paper Size</label>
|
|
105
|
+
<select id="drawing-paper-size" style="width: 100%; padding: 4px; background: #1a1a1a; border: 1px solid #444; color: #fff; border-radius: 2px;">
|
|
106
|
+
<option>A0</option><option>A1</option><option>A2</option><option>A3</option><option>A4</option>
|
|
107
|
+
<option>ANSI A</option><option>ANSI B</option><option>ANSI C</option><option>ANSI D</option><option>ANSI E</option>
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
<div style="margin-bottom: 12px;">
|
|
111
|
+
<label style="display: block; margin-bottom: 4px;">Scale (1:X)</label>
|
|
112
|
+
<select id="drawing-scale" style="width: 100%; padding: 4px; background: #1a1a1a; border: 1px solid #444; color: #fff; border-radius: 2px;">
|
|
113
|
+
<option value="1">1:1</option><option value="2">1:2</option><option value="5">1:5</option>
|
|
114
|
+
<option value="10">1:10</option><option value="20">1:20</option><option value="50">1:50</option>
|
|
115
|
+
</select>
|
|
116
|
+
</div>
|
|
117
|
+
<div style="margin-bottom: 12px;">
|
|
118
|
+
<label style="display: block; margin-bottom: 4px;">Title Block</label>
|
|
119
|
+
<select id="drawing-titleblock" style="width: 100%; padding: 4px; background: #1a1a1a; border: 1px solid #444; color: #fff; border-radius: 2px;">
|
|
120
|
+
<option value="default">Default</option><option value="iso">ISO 7200</option><option value="ansi">ANSI Y14.1</option>
|
|
121
|
+
</select>
|
|
122
|
+
</div>
|
|
123
|
+
<div id="drawing-selected-info" style="margin-top: 20px; padding-top: 12px; border-top: 1px solid #444;">
|
|
124
|
+
<p style="margin: 4px 0; color: #aaa;">No selection</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div id="drawing-dimension-dialog" style="display: none; position: fixed; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 12px; z-index: 10000; min-width: 240px;">
|
|
129
|
+
<label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 4px;">Dimension Value (mm)</label>
|
|
130
|
+
<input id="drawing-dim-value" type="number" step="0.01" style="width: 100%; padding: 6px; background: #1a1a1a; border: 1px solid #666; color: #fff; border-radius: 2px; margin-bottom: 8px;">
|
|
131
|
+
<label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 4px;">Tolerance (optional)</label>
|
|
132
|
+
<input id="drawing-dim-tolerance" type="text" placeholder="+0.5/-0.5" style="width: 100%; padding: 6px; background: #1a1a1a; border: 1px solid #666; color: #fff; border-radius: 2px; margin-bottom: 8px; font-size: 11px;">
|
|
133
|
+
<button id="drawing-dim-ok" style="width: 100%; padding: 6px; background: #00aa00; color: white; border: none; border-radius: 2px; cursor: pointer; margin-bottom: 4px;">OK</button>
|
|
134
|
+
<button id="drawing-dim-cancel" style="width: 100%; padding: 6px; background: #666; color: white; border: none; border-radius: 2px; cursor: pointer;">Cancel</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div id="drawing-gdt-selector" style="display: none; position: fixed; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 12px; z-index: 10000; min-width: 200px;">
|
|
138
|
+
<label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 8px;">GD&T Symbol Type</label>
|
|
139
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
|
|
140
|
+
<button data-gdt="flatness" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">⬥ Flatness</button>
|
|
141
|
+
<button data-gdt="straightness" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">— Straightness</button>
|
|
142
|
+
<button data-gdt="circularity" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">◯ Circularity</button>
|
|
143
|
+
<button data-gdt="cylindricity" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">◯ Cylindricity</button>
|
|
144
|
+
<button data-gdt="perpendicular" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">⊥ Perpendicular</button>
|
|
145
|
+
<button data-gdt="parallel" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">∥ Parallel</button>
|
|
146
|
+
<button data-gdt="position" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">⊕ Position</button>
|
|
147
|
+
<button data-gdt="concentricity" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">⊕ Concentricity</button>
|
|
148
|
+
<button data-gdt="runout" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">⟳ Runout</button>
|
|
149
|
+
<button data-gdt="profile" style="padding: 6px; background: #444; color: #aaa; border: 1px solid #666; border-radius: 2px; cursor: pointer; font-size: 11px;">✓ Profile</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div id="drawing-export-dialog" style="display: none; position: fixed; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 16px; z-index: 10000; min-width: 280px; box-shadow: 0 4px 16px rgba(0,0,0,0.4);">
|
|
154
|
+
<h3 style="margin-top: 0; color: #fff;">Export Drawing</h3>
|
|
155
|
+
<label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 4px;">Format</label>
|
|
156
|
+
<select id="drawing-export-format" style="width: 100%; padding: 6px; background: #1a1a1a; border: 1px solid #666; color: #fff; border-radius: 2px; margin-bottom: 12px;">
|
|
157
|
+
<option value="pdf">PDF (Vector)</option>
|
|
158
|
+
<option value="dxf">DXF (CAD)</option>
|
|
159
|
+
<option value="svg">SVG (Web)</option>
|
|
160
|
+
<option value="png">PNG (Raster at 300 DPI)</option>
|
|
161
|
+
</select>
|
|
162
|
+
<label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 4px;">Filename</label>
|
|
163
|
+
<input id="drawing-export-name" type="text" placeholder="drawing" style="width: 100%; padding: 6px; background: #1a1a1a; border: 1px solid #666; color: #fff; border-radius: 2px; margin-bottom: 12px; box-sizing: border-box;">
|
|
164
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
165
|
+
<button id="drawing-export-ok" style="padding: 8px; background: #00aa00; color: white; border: none; border-radius: 2px; cursor: pointer;">Export</button>
|
|
166
|
+
<button id="drawing-export-cancel" style="padding: 8px; background: #666; color: white; border: none; border-radius: 2px; cursor: pointer;">Cancel</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
execute(command, params = {}) {
|
|
173
|
+
switch (command) {
|
|
174
|
+
case 'create': return this.create(params.paperSize, params.scale);
|
|
175
|
+
case 'start': return this.start();
|
|
176
|
+
case 'finish': return this.finish();
|
|
177
|
+
case 'addView': return this.addView(params.type, params.direction, params.position, params.scale);
|
|
178
|
+
case 'addDimension': return this.addDimension(params.type, params.entities, params.value, params.tolerance);
|
|
179
|
+
case 'addAnnotation': return this.addAnnotation(params.type, params.position, params.data);
|
|
180
|
+
case 'addBalloon': return this.addBalloon(params.partId, params.position);
|
|
181
|
+
case 'addCenterMark': return this.addCenterMark(params.circleEntity, params.viewId);
|
|
182
|
+
case 'addCenterline': return this.addCenterline(params.entity1, params.entity2, params.viewId);
|
|
183
|
+
case 'addLeader': return this.addLeader(params.position, params.text);
|
|
184
|
+
case 'setTitleBlock': return this.setTitleBlock(params.template, params.fields);
|
|
185
|
+
case 'addBOM': return this.addBOM(params.assemblyId);
|
|
186
|
+
case 'export': return this.export(params.format, params.filename);
|
|
187
|
+
case 'addSheet': return this.addSheet();
|
|
188
|
+
case 'setScale': return this.setScale(params.viewId, params.scale);
|
|
189
|
+
case 'setMode': return this.setMode(params.mode);
|
|
190
|
+
default: throw new Error(`Unknown drawing command: ${command}`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// ===== CORE METHODS =====
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a new drawing sheet
|
|
198
|
+
*/
|
|
199
|
+
create(paperSize = 'A3', scale = 1) {
|
|
200
|
+
this.state.paperSize = paperSize;
|
|
201
|
+
this.state.scale = scale;
|
|
202
|
+
|
|
203
|
+
const dims = this.state.paperDimensions[paperSize];
|
|
204
|
+
const sheetId = `sheet_${Date.now()}`;
|
|
205
|
+
|
|
206
|
+
const sheet = {
|
|
207
|
+
id: sheetId,
|
|
208
|
+
paperSize,
|
|
209
|
+
scale,
|
|
210
|
+
views: new Map(),
|
|
211
|
+
dimensions: new Map(),
|
|
212
|
+
annotations: new Map(),
|
|
213
|
+
balloons: new Map(),
|
|
214
|
+
centerMarks: new Map(),
|
|
215
|
+
centerlines: new Map(),
|
|
216
|
+
titleBlock: { template: 'default', fields: {} },
|
|
217
|
+
bom: null
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
this.state.sheets.push(sheet);
|
|
221
|
+
this.state.currentSheetId = sheetId;
|
|
222
|
+
this.state.currentSheet = sheet;
|
|
223
|
+
|
|
224
|
+
this._createSVGCanvas(dims.w, dims.h);
|
|
225
|
+
this._drawPageBorder(dims.w, dims.h);
|
|
226
|
+
this._drawTitleBlock();
|
|
227
|
+
|
|
228
|
+
window.dispatchEvent(new CustomEvent('drawing:created', { detail: { sheetId } }));
|
|
229
|
+
return { sheetId, paperSize, scale };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Start drawing workspace
|
|
234
|
+
*/
|
|
235
|
+
start() {
|
|
236
|
+
if (!this.state.currentSheet) {
|
|
237
|
+
this.create('A3', 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
document.getElementById('drawing-workspace').style.display = 'flex';
|
|
241
|
+
document.getElementById('drawing-toolbar').style.display = 'flex';
|
|
242
|
+
document.getElementById('drawing-properties').style.display = 'block';
|
|
243
|
+
|
|
244
|
+
this.state.isActive = true;
|
|
245
|
+
this.setMode('view');
|
|
246
|
+
|
|
247
|
+
// Hide 3D viewport and show drawing canvas
|
|
248
|
+
if (document.getElementById('viewport')) {
|
|
249
|
+
document.getElementById('viewport').style.display = 'none';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
window.dispatchEvent(new CustomEvent('drawing:started'));
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Exit drawing workspace, return to 3D
|
|
257
|
+
*/
|
|
258
|
+
finish() {
|
|
259
|
+
this.state.isActive = false;
|
|
260
|
+
document.getElementById('drawing-workspace').style.display = 'none';
|
|
261
|
+
document.getElementById('drawing-toolbar').style.display = 'none';
|
|
262
|
+
document.getElementById('drawing-properties').style.display = 'none';
|
|
263
|
+
|
|
264
|
+
if (document.getElementById('viewport')) {
|
|
265
|
+
document.getElementById('viewport').style.display = 'flex';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
window.dispatchEvent(new CustomEvent('drawing:finished'));
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Add a view to the drawing (orthographic, section, detail, isometric, auxiliary)
|
|
273
|
+
*/
|
|
274
|
+
addView(type, direction = [0, 0, 1], position = [100, 100], viewScale = 1) {
|
|
275
|
+
if (!this.state.currentSheet) return null;
|
|
276
|
+
|
|
277
|
+
const viewId = `view_${Date.now()}`;
|
|
278
|
+
const view = {
|
|
279
|
+
id: viewId,
|
|
280
|
+
type, // 'orthographic', 'section', 'detail', 'isometric', 'auxiliary'
|
|
281
|
+
direction: Array.isArray(direction) ? new THREE.Vector3(...direction) : direction,
|
|
282
|
+
position,
|
|
283
|
+
scale: viewScale,
|
|
284
|
+
svgGroup: null,
|
|
285
|
+
projection: [], // array of 2D line segments
|
|
286
|
+
edges: [], // visible edges
|
|
287
|
+
hiddenEdges: [], // dashed hidden edges
|
|
288
|
+
centerlines: [] // thin chain lines
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Generate orthographic projection
|
|
292
|
+
this._projectView(view);
|
|
293
|
+
|
|
294
|
+
// Add to SVG
|
|
295
|
+
const svgGroup = this._createSVGGroup(viewId);
|
|
296
|
+
this._renderViewToSVG(view, svgGroup);
|
|
297
|
+
|
|
298
|
+
view.svgGroup = svgGroup;
|
|
299
|
+
this.state.currentSheet.views.set(viewId, view);
|
|
300
|
+
this.state.views.set(viewId, view);
|
|
301
|
+
|
|
302
|
+
window.dispatchEvent(new CustomEvent('drawing:viewAdded', { detail: { viewId, type } }));
|
|
303
|
+
return { viewId, type, position };
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Add dimension to drawing (linear, angular, radial, diameter, ordinate)
|
|
308
|
+
*/
|
|
309
|
+
addDimension(type, entities = [], value = null, tolerance = '') {
|
|
310
|
+
if (!this.state.currentSheet) return null;
|
|
311
|
+
|
|
312
|
+
const dimId = `dim_${Date.now()}`;
|
|
313
|
+
const dimension = {
|
|
314
|
+
id: dimId,
|
|
315
|
+
type, // 'linear', 'angular', 'radial', 'diameter', 'ordinate'
|
|
316
|
+
entities, // array of entity IDs or geometry references
|
|
317
|
+
value,
|
|
318
|
+
tolerance,
|
|
319
|
+
position: [200, 200], // will be set by user placement
|
|
320
|
+
associated: true, // updates when model changes
|
|
321
|
+
svgElement: null
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
this.state.currentSheet.dimensions.set(dimId, dimension);
|
|
325
|
+
this.state.dimensions.set(dimId, dimension);
|
|
326
|
+
|
|
327
|
+
window.dispatchEvent(new CustomEvent('drawing:dimensionAdded', { detail: { dimId, type } }));
|
|
328
|
+
return dimId;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Add annotation (GD&T, surface finish, weld symbols)
|
|
333
|
+
*/
|
|
334
|
+
addAnnotation(type, position, data = {}) {
|
|
335
|
+
if (!this.state.currentSheet) return null;
|
|
336
|
+
|
|
337
|
+
const annId = `ann_${Date.now()}`;
|
|
338
|
+
const annotation = {
|
|
339
|
+
id: annId,
|
|
340
|
+
type, // 'gdt', 'surfaceFinish', 'weld', 'general'
|
|
341
|
+
position,
|
|
342
|
+
data, // { gdtType, value, datum, etc. }
|
|
343
|
+
svgElement: null
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const svgElement = this._renderAnnotation(annotation);
|
|
347
|
+
annotation.svgElement = svgElement;
|
|
348
|
+
|
|
349
|
+
this.state.currentSheet.annotations.set(annId, annotation);
|
|
350
|
+
this.state.annotations.set(annId, annotation);
|
|
351
|
+
|
|
352
|
+
window.dispatchEvent(new CustomEvent('drawing:annotationAdded', { detail: { annId, type } }));
|
|
353
|
+
return annId;
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Add balloon (for assembly drawings)
|
|
358
|
+
*/
|
|
359
|
+
addBalloon(partId, position) {
|
|
360
|
+
if (!this.state.currentSheet) return null;
|
|
361
|
+
|
|
362
|
+
const balloonId = `balloon_${Date.now()}`;
|
|
363
|
+
const number = this.state.nextBalloonNumber++;
|
|
364
|
+
|
|
365
|
+
const balloon = {
|
|
366
|
+
id: balloonId,
|
|
367
|
+
partId,
|
|
368
|
+
position,
|
|
369
|
+
number,
|
|
370
|
+
svgElement: null
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const svgElement = this._renderBalloon(balloon);
|
|
374
|
+
balloon.svgElement = svgElement;
|
|
375
|
+
|
|
376
|
+
this.state.currentSheet.balloons.set(balloonId, balloon);
|
|
377
|
+
this.state.balloons.set(balloonId, balloon);
|
|
378
|
+
|
|
379
|
+
window.dispatchEvent(new CustomEvent('drawing:balloonAdded', { detail: { balloonId, number } }));
|
|
380
|
+
return balloonId;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Add center mark on circle/arc
|
|
385
|
+
*/
|
|
386
|
+
addCenterMark(circleEntity, viewId = null) {
|
|
387
|
+
if (!this.state.currentSheet) return null;
|
|
388
|
+
|
|
389
|
+
const cmId = `cm_${Date.now()}`;
|
|
390
|
+
const centerMark = {
|
|
391
|
+
id: cmId,
|
|
392
|
+
entity: circleEntity,
|
|
393
|
+
viewId,
|
|
394
|
+
svgElement: null
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const svgElement = this._renderCenterMark(centerMark);
|
|
398
|
+
centerMark.svgElement = svgElement;
|
|
399
|
+
|
|
400
|
+
this.state.currentSheet.centerMarks.set(cmId, centerMark);
|
|
401
|
+
return cmId;
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Add centerline between two features
|
|
406
|
+
*/
|
|
407
|
+
addCenterline(entity1, entity2, viewId = null) {
|
|
408
|
+
if (!this.state.currentSheet) return null;
|
|
409
|
+
|
|
410
|
+
const clId = `cl_${Date.now()}`;
|
|
411
|
+
const centerline = {
|
|
412
|
+
id: clId,
|
|
413
|
+
entity1,
|
|
414
|
+
entity2,
|
|
415
|
+
viewId,
|
|
416
|
+
svgElement: null
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const svgElement = this._renderCenterline(centerline);
|
|
420
|
+
centerline.svgElement = svgElement;
|
|
421
|
+
|
|
422
|
+
this.state.currentSheet.centerlines.set(clId, centerline);
|
|
423
|
+
return clId;
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Add leader with text note
|
|
428
|
+
*/
|
|
429
|
+
addLeader(position, text) {
|
|
430
|
+
if (!this.state.currentSheet) return null;
|
|
431
|
+
|
|
432
|
+
const leaderId = `leader_${Date.now()}`;
|
|
433
|
+
const leader = {
|
|
434
|
+
id: leaderId,
|
|
435
|
+
position,
|
|
436
|
+
text,
|
|
437
|
+
svgElement: null
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const svgElement = this._renderLeader(leader);
|
|
441
|
+
leader.svgElement = svgElement;
|
|
442
|
+
|
|
443
|
+
return leaderId;
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Set title block template and fields
|
|
448
|
+
*/
|
|
449
|
+
setTitleBlock(template = 'default', fields = {}) {
|
|
450
|
+
if (!this.state.currentSheet) return;
|
|
451
|
+
|
|
452
|
+
this.state.currentSheet.titleBlock = { template, fields };
|
|
453
|
+
this._drawTitleBlock();
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Add Bill of Materials table
|
|
458
|
+
*/
|
|
459
|
+
addBOM(assemblyId = null) {
|
|
460
|
+
if (!this.state.currentSheet) return null;
|
|
461
|
+
|
|
462
|
+
// Generate BOM from current assembly or specified assembly
|
|
463
|
+
const bomData = {
|
|
464
|
+
items: [
|
|
465
|
+
// { item: 1, partNumber: 'ASM-001', description: 'Main Assembly', material: 'Steel', qty: 1 },
|
|
466
|
+
// Auto-populated from model
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
this._renderBOMTable(bomData);
|
|
471
|
+
this.state.currentSheet.bom = bomData;
|
|
472
|
+
|
|
473
|
+
return { itemCount: bomData.items.length };
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Export drawing to PDF, DXF, SVG, or PNG
|
|
478
|
+
*/
|
|
479
|
+
export(format = 'pdf', filename = 'drawing') {
|
|
480
|
+
if (!this.state.svgDoc) return { error: 'No active drawing' };
|
|
481
|
+
|
|
482
|
+
const svgString = new XMLSerializer().serializeToString(this.state.svgDoc);
|
|
483
|
+
|
|
484
|
+
switch (format) {
|
|
485
|
+
case 'pdf':
|
|
486
|
+
return this._exportPDF(svgString, filename);
|
|
487
|
+
case 'dxf':
|
|
488
|
+
return this._exportDXF(svgString, filename);
|
|
489
|
+
case 'svg':
|
|
490
|
+
return this._exportSVG(svgString, filename);
|
|
491
|
+
case 'png':
|
|
492
|
+
return this._exportPNG(svgString, filename);
|
|
493
|
+
default:
|
|
494
|
+
return { error: `Unknown format: ${format}` };
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Add another sheet to the drawing
|
|
500
|
+
*/
|
|
501
|
+
addSheet() {
|
|
502
|
+
const dims = this.state.paperDimensions[this.state.paperSize];
|
|
503
|
+
this.create(this.state.paperSize, this.state.scale);
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Change scale of a specific view
|
|
508
|
+
*/
|
|
509
|
+
setScale(viewId, scale) {
|
|
510
|
+
const view = this.state.views.get(viewId);
|
|
511
|
+
if (!view) return null;
|
|
512
|
+
|
|
513
|
+
view.scale = scale;
|
|
514
|
+
this._projectView(view);
|
|
515
|
+
// Refresh SVG rendering
|
|
516
|
+
return { viewId, newScale: scale };
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Set drawing mode (view, dimension, annotation, balloon, etc.)
|
|
521
|
+
*/
|
|
522
|
+
setMode(mode) {
|
|
523
|
+
this.state.mode = mode;
|
|
524
|
+
document.querySelectorAll('[data-drawing-tool]').forEach(btn => {
|
|
525
|
+
btn.style.background = btn.dataset.drawingTool === mode ? '#0066cc' : '';
|
|
526
|
+
});
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// ===== INTERNAL RENDERING =====
|
|
530
|
+
|
|
531
|
+
_createSVGCanvas(width, height) {
|
|
532
|
+
const container = document.getElementById('drawing-canvas');
|
|
533
|
+
container.innerHTML = '';
|
|
534
|
+
|
|
535
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
536
|
+
svg.setAttribute('width', width);
|
|
537
|
+
svg.setAttribute('height', height);
|
|
538
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
539
|
+
svg.style.background = 'white';
|
|
540
|
+
svg.style.display = 'block';
|
|
541
|
+
|
|
542
|
+
container.appendChild(svg);
|
|
543
|
+
this.state.svgDoc = svg;
|
|
544
|
+
this.state.svgContainer = container;
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
_drawPageBorder(w, h) {
|
|
548
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
549
|
+
rect.setAttribute('x', '10');
|
|
550
|
+
rect.setAttribute('y', '10');
|
|
551
|
+
rect.setAttribute('width', w - 20);
|
|
552
|
+
rect.setAttribute('height', h - 20);
|
|
553
|
+
rect.setAttribute('fill', 'none');
|
|
554
|
+
rect.setAttribute('stroke', '#000');
|
|
555
|
+
rect.setAttribute('stroke-width', '1.5');
|
|
556
|
+
this.state.svgDoc.appendChild(rect);
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
_drawTitleBlock() {
|
|
560
|
+
const template = this.state.currentSheet?.titleBlock.template || 'default';
|
|
561
|
+
const fields = this.state.currentSheet?.titleBlock.fields || {};
|
|
562
|
+
const w = this.state.paperDimensions[this.state.paperSize].w;
|
|
563
|
+
const h = this.state.paperDimensions[this.state.paperSize].h;
|
|
564
|
+
|
|
565
|
+
// Standard title block at bottom right
|
|
566
|
+
const tbW = 80, tbH = 50;
|
|
567
|
+
const tbX = w - tbW - 10, tbY = h - tbH - 10;
|
|
568
|
+
|
|
569
|
+
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
570
|
+
|
|
571
|
+
// Border
|
|
572
|
+
const border = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
573
|
+
border.setAttribute('x', tbX);
|
|
574
|
+
border.setAttribute('y', tbY);
|
|
575
|
+
border.setAttribute('width', tbW);
|
|
576
|
+
border.setAttribute('height', tbH);
|
|
577
|
+
border.setAttribute('fill', 'none');
|
|
578
|
+
border.setAttribute('stroke', '#000');
|
|
579
|
+
border.setAttribute('stroke-width', '0.5');
|
|
580
|
+
group.appendChild(border);
|
|
581
|
+
|
|
582
|
+
// Labels
|
|
583
|
+
const labels = [
|
|
584
|
+
{ text: 'Scale: 1:' + this.state.scale, x: tbX + 4, y: tbY + 12 },
|
|
585
|
+
{ text: 'Date: ' + new Date().toLocaleDateString(), x: tbX + 4, y: tbY + 24 },
|
|
586
|
+
{ text: 'Drawn: cycleCAD', x: tbX + 4, y: tbY + 36 }
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
labels.forEach(({ text, x, y }) => {
|
|
590
|
+
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
591
|
+
txt.setAttribute('x', x);
|
|
592
|
+
txt.setAttribute('y', y);
|
|
593
|
+
txt.setAttribute('font-size', '8');
|
|
594
|
+
txt.setAttribute('font-family', 'Arial, sans-serif');
|
|
595
|
+
txt.setAttribute('fill', '#000');
|
|
596
|
+
txt.textContent = text;
|
|
597
|
+
group.appendChild(txt);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
this.state.svgDoc.appendChild(group);
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
_createSVGGroup(id) {
|
|
604
|
+
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
605
|
+
group.setAttribute('id', id);
|
|
606
|
+
group.setAttribute('data-view-id', id);
|
|
607
|
+
this.state.svgDoc.appendChild(group);
|
|
608
|
+
return group;
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
_projectView(view) {
|
|
612
|
+
// Simplified projection — in real implementation, would:
|
|
613
|
+
// 1. Project 3D model onto 2D plane based on view direction
|
|
614
|
+
// 2. Perform hidden line removal
|
|
615
|
+
// 3. Detect sharp edges vs smooth surfaces
|
|
616
|
+
// 4. Generate 2D line segments
|
|
617
|
+
|
|
618
|
+
// For now, store projection data
|
|
619
|
+
view.projection = [
|
|
620
|
+
// [x1, y1, x2, y2, lineType] // lineType: 'visible', 'hidden', 'centerline'
|
|
621
|
+
];
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
_renderViewToSVG(view, svgGroup) {
|
|
625
|
+
// Render view projection as SVG lines
|
|
626
|
+
// Line weights: visible=0.5mm, hidden=0.25mm, centerline=0.25mm chain
|
|
627
|
+
view.projection.forEach(([x1, y1, x2, y2, lineType]) => {
|
|
628
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
629
|
+
line.setAttribute('x1', view.position[0] + x1 * view.scale);
|
|
630
|
+
line.setAttribute('y1', view.position[1] + y1 * view.scale);
|
|
631
|
+
line.setAttribute('x2', view.position[0] + x2 * view.scale);
|
|
632
|
+
line.setAttribute('y2', view.position[1] + y2 * view.scale);
|
|
633
|
+
line.setAttribute('stroke', '#000');
|
|
634
|
+
|
|
635
|
+
if (lineType === 'visible') {
|
|
636
|
+
line.setAttribute('stroke-width', '0.5');
|
|
637
|
+
} else if (lineType === 'hidden') {
|
|
638
|
+
line.setAttribute('stroke-width', '0.25');
|
|
639
|
+
line.setAttribute('stroke-dasharray', '2,1');
|
|
640
|
+
} else if (lineType === 'centerline') {
|
|
641
|
+
line.setAttribute('stroke-width', '0.25');
|
|
642
|
+
line.setAttribute('stroke-dasharray', '4,2,1,2');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
svgGroup.appendChild(line);
|
|
646
|
+
});
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
_renderAnnotation(annotation) {
|
|
650
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
651
|
+
g.setAttribute('transform', `translate(${annotation.position[0]}, ${annotation.position[1]})`);
|
|
652
|
+
|
|
653
|
+
// Draw GD&T symbol or surface finish
|
|
654
|
+
if (annotation.type === 'gdt') {
|
|
655
|
+
const box = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
656
|
+
box.setAttribute('width', '12');
|
|
657
|
+
box.setAttribute('height', '12');
|
|
658
|
+
box.setAttribute('fill', 'none');
|
|
659
|
+
box.setAttribute('stroke', '#000');
|
|
660
|
+
box.setAttribute('stroke-width', '0.5');
|
|
661
|
+
g.appendChild(box);
|
|
662
|
+
|
|
663
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
664
|
+
text.setAttribute('x', '2');
|
|
665
|
+
text.setAttribute('y', '10');
|
|
666
|
+
text.setAttribute('font-size', '8');
|
|
667
|
+
text.textContent = annotation.data.gdtType?.[0] || '◯';
|
|
668
|
+
g.appendChild(text);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
this.state.svgDoc.appendChild(g);
|
|
672
|
+
return g;
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
_renderBalloon(balloon) {
|
|
676
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
677
|
+
circle.setAttribute('cx', balloon.position[0]);
|
|
678
|
+
circle.setAttribute('cy', balloon.position[1]);
|
|
679
|
+
circle.setAttribute('r', '8');
|
|
680
|
+
circle.setAttribute('fill', 'none');
|
|
681
|
+
circle.setAttribute('stroke', '#000');
|
|
682
|
+
circle.setAttribute('stroke-width', '0.5');
|
|
683
|
+
|
|
684
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
685
|
+
text.setAttribute('x', balloon.position[0]);
|
|
686
|
+
text.setAttribute('y', balloon.position[1]);
|
|
687
|
+
text.setAttribute('text-anchor', 'middle');
|
|
688
|
+
text.setAttribute('dominant-baseline', 'middle');
|
|
689
|
+
text.setAttribute('font-size', '8');
|
|
690
|
+
text.setAttribute('font-weight', 'bold');
|
|
691
|
+
text.textContent = String(balloon.number);
|
|
692
|
+
|
|
693
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
694
|
+
g.appendChild(circle);
|
|
695
|
+
g.appendChild(text);
|
|
696
|
+
this.state.svgDoc.appendChild(g);
|
|
697
|
+
return g;
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
_renderCenterMark(centerMark) {
|
|
701
|
+
// ⊕ symbol
|
|
702
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
703
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
704
|
+
circle.setAttribute('r', '1.5');
|
|
705
|
+
circle.setAttribute('fill', 'none');
|
|
706
|
+
circle.setAttribute('stroke', '#000');
|
|
707
|
+
circle.setAttribute('stroke-width', '0.25');
|
|
708
|
+
g.appendChild(circle);
|
|
709
|
+
|
|
710
|
+
const h = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
711
|
+
h.setAttribute('x1', '-3');
|
|
712
|
+
h.setAttribute('y1', '0');
|
|
713
|
+
h.setAttribute('x2', '3');
|
|
714
|
+
h.setAttribute('y2', '0');
|
|
715
|
+
h.setAttribute('stroke', '#000');
|
|
716
|
+
h.setAttribute('stroke-width', '0.25');
|
|
717
|
+
g.appendChild(h);
|
|
718
|
+
|
|
719
|
+
const v = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
720
|
+
v.setAttribute('x1', '0');
|
|
721
|
+
v.setAttribute('y1', '-3');
|
|
722
|
+
v.setAttribute('x2', '0');
|
|
723
|
+
v.setAttribute('y2', '3');
|
|
724
|
+
v.setAttribute('stroke', '#000');
|
|
725
|
+
v.setAttribute('stroke-width', '0.25');
|
|
726
|
+
g.appendChild(v);
|
|
727
|
+
|
|
728
|
+
this.state.svgDoc.appendChild(g);
|
|
729
|
+
return g;
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
_renderCenterline(centerline) {
|
|
733
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
734
|
+
line.setAttribute('stroke', '#000');
|
|
735
|
+
line.setAttribute('stroke-width', '0.25');
|
|
736
|
+
line.setAttribute('stroke-dasharray', '4,2,1,2');
|
|
737
|
+
this.state.svgDoc.appendChild(line);
|
|
738
|
+
return line;
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
_renderLeader(leader) {
|
|
742
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
743
|
+
|
|
744
|
+
// Arrow line
|
|
745
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
746
|
+
line.setAttribute('x1', leader.position[0]);
|
|
747
|
+
line.setAttribute('y1', leader.position[1]);
|
|
748
|
+
line.setAttribute('x2', leader.position[0] + 30);
|
|
749
|
+
line.setAttribute('y2', leader.position[1]);
|
|
750
|
+
line.setAttribute('stroke', '#000');
|
|
751
|
+
line.setAttribute('stroke-width', '0.5');
|
|
752
|
+
g.appendChild(line);
|
|
753
|
+
|
|
754
|
+
// Text
|
|
755
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
756
|
+
text.setAttribute('x', leader.position[0] + 35);
|
|
757
|
+
text.setAttribute('y', leader.position[1]);
|
|
758
|
+
text.setAttribute('font-size', '10');
|
|
759
|
+
text.setAttribute('font-family', 'Arial, sans-serif');
|
|
760
|
+
text.textContent = leader.text;
|
|
761
|
+
g.appendChild(text);
|
|
762
|
+
|
|
763
|
+
this.state.svgDoc.appendChild(g);
|
|
764
|
+
return g;
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
_renderBOMTable(bomData) {
|
|
768
|
+
// Render table with columns: Item, Part #, Desc, Material, Qty
|
|
769
|
+
const tableX = 20, tableY = 100;
|
|
770
|
+
const colW = 60, rowH = 14;
|
|
771
|
+
|
|
772
|
+
const headers = ['Item', 'Part #', 'Description', 'Material', 'Qty'];
|
|
773
|
+
headers.forEach((header, i) => {
|
|
774
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
775
|
+
rect.setAttribute('x', tableX + i * colW);
|
|
776
|
+
rect.setAttribute('y', tableY);
|
|
777
|
+
rect.setAttribute('width', colW);
|
|
778
|
+
rect.setAttribute('height', rowH);
|
|
779
|
+
rect.setAttribute('fill', '#e0e0e0');
|
|
780
|
+
rect.setAttribute('stroke', '#000');
|
|
781
|
+
rect.setAttribute('stroke-width', '0.5');
|
|
782
|
+
this.state.svgDoc.appendChild(rect);
|
|
783
|
+
|
|
784
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
785
|
+
text.setAttribute('x', tableX + i * colW + 3);
|
|
786
|
+
text.setAttribute('y', tableY + 11);
|
|
787
|
+
text.setAttribute('font-size', '9');
|
|
788
|
+
text.setAttribute('font-weight', 'bold');
|
|
789
|
+
text.textContent = header;
|
|
790
|
+
this.state.svgDoc.appendChild(text);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Rows
|
|
794
|
+
bomData.items.forEach((item, idx) => {
|
|
795
|
+
const values = [item.item, item.partNumber, item.description, item.material, item.qty];
|
|
796
|
+
values.forEach((val, i) => {
|
|
797
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
798
|
+
rect.setAttribute('x', tableX + i * colW);
|
|
799
|
+
rect.setAttribute('y', tableY + rowH + (idx + 1) * rowH);
|
|
800
|
+
rect.setAttribute('width', colW);
|
|
801
|
+
rect.setAttribute('height', rowH);
|
|
802
|
+
rect.setAttribute('fill', 'none');
|
|
803
|
+
rect.setAttribute('stroke', '#000');
|
|
804
|
+
rect.setAttribute('stroke-width', '0.5');
|
|
805
|
+
this.state.svgDoc.appendChild(rect);
|
|
806
|
+
|
|
807
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
808
|
+
text.setAttribute('x', tableX + i * colW + 3);
|
|
809
|
+
text.setAttribute('y', tableY + rowH + (idx + 1.75) * rowH);
|
|
810
|
+
text.setAttribute('font-size', '8');
|
|
811
|
+
text.textContent = String(val || '');
|
|
812
|
+
this.state.svgDoc.appendChild(text);
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
// ===== EXPORT FUNCTIONS =====
|
|
818
|
+
|
|
819
|
+
_exportPDF(svgString, filename) {
|
|
820
|
+
// Use jsPDF + svg2pdf for PDF export
|
|
821
|
+
// Placeholder — real implementation would integrate library
|
|
822
|
+
console.log(`Exporting to PDF: ${filename}.pdf`);
|
|
823
|
+
window.dispatchEvent(new CustomEvent('drawing:exported', { detail: { format: 'pdf', filename } }));
|
|
824
|
+
return { format: 'pdf', filename: filename + '.pdf' };
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
_exportDXF(svgString, filename) {
|
|
828
|
+
// Convert SVG lines to DXF format
|
|
829
|
+
console.log(`Exporting to DXF: ${filename}.dxf`);
|
|
830
|
+
window.dispatchEvent(new CustomEvent('drawing:exported', { detail: { format: 'dxf', filename } }));
|
|
831
|
+
return { format: 'dxf', filename: filename + '.dxf' };
|
|
832
|
+
},
|
|
833
|
+
|
|
834
|
+
_exportSVG(svgString, filename) {
|
|
835
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
|
836
|
+
const url = URL.createObjectURL(blob);
|
|
837
|
+
const a = document.createElement('a');
|
|
838
|
+
a.href = url;
|
|
839
|
+
a.download = filename + '.svg';
|
|
840
|
+
a.click();
|
|
841
|
+
URL.revokeObjectURL(url);
|
|
842
|
+
|
|
843
|
+
window.dispatchEvent(new CustomEvent('drawing:exported', { detail: { format: 'svg', filename } }));
|
|
844
|
+
return { format: 'svg', filename: filename + '.svg' };
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
_exportPNG(svgString, filename) {
|
|
848
|
+
// Convert SVG to PNG via canvas (300 DPI)
|
|
849
|
+
const canvas = document.createElement('canvas');
|
|
850
|
+
const svg = new Blob([svgString], { type: 'image/svg+xml' });
|
|
851
|
+
const url = URL.createObjectURL(svg);
|
|
852
|
+
const img = new Image();
|
|
853
|
+
|
|
854
|
+
img.onload = () => {
|
|
855
|
+
canvas.width = img.width * 4; // 4x for 300 DPI
|
|
856
|
+
canvas.height = img.height * 4;
|
|
857
|
+
const ctx = canvas.getContext('2d');
|
|
858
|
+
ctx.scale(4, 4);
|
|
859
|
+
ctx.drawImage(img, 0, 0);
|
|
860
|
+
|
|
861
|
+
canvas.toBlob(blob => {
|
|
862
|
+
const a = document.createElement('a');
|
|
863
|
+
a.href = URL.createObjectURL(blob);
|
|
864
|
+
a.download = filename + '.png';
|
|
865
|
+
a.click();
|
|
866
|
+
|
|
867
|
+
window.dispatchEvent(new CustomEvent('drawing:exported', { detail: { format: 'png', filename } }));
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
URL.revokeObjectURL(url);
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
img.src = url;
|
|
874
|
+
return { format: 'png', filename: filename + '.png' };
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// ===== MICROKERNEL REGISTRATION =====
|
|
879
|
+
if (typeof window !== 'undefined') {
|
|
880
|
+
window.DrawingModule = DrawingModule;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export default DrawingModule;
|