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.
@@ -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;