@teachinglab/omd 0.3.8 → 0.3.9

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.
@@ -1,324 +1,321 @@
1
- import { Tool } from './tool.js';
2
- import { Stroke } from '../drawing/stroke.js';
3
-
4
- /**
5
- * Pencil tool for freehand drawing
6
- */
7
- export class PencilTool extends Tool {
8
- constructor(canvas, options = {}) {
9
- super(canvas, {
10
- strokeWidth: 5,
11
- strokeColor: '#000000',
12
- strokeOpacity: 1,
13
- smoothing: 0.5,
14
- pressureSensitive: true,
15
- ...options
16
- });
17
-
18
- this.displayName = 'Pencil';
19
- this.description = 'Draw freehand strokes';
20
- this.icon = 'pencil';
21
- this.shortcut = 'P';
22
- this.category = 'drawing';
23
-
24
- // Drawing state
25
- this.points = [];
26
- this.lastPoint = null;
27
- this.minDistance = 2.0; // Minimum distance between points (increased for better performance)
28
- }
29
-
30
- /**
31
- * Start drawing a new stroke
32
- */
33
- onPointerDown(event) {
34
- if (!this.canUse()) return;
35
-
36
- console.log('[Pencil Debug] Starting new stroke at:', event.x, event.y);
37
-
38
- this.isDrawing = true;
39
- this.points = [];
40
- this.lastPoint = { x: event.x, y: event.y };
41
-
42
- // Create new stroke
43
- this.currentStroke = new Stroke({
44
- x: event.x,
45
- y: event.y,
46
- strokeWidth: this.calculateStrokeWidth(event.pressure),
47
- strokeColor: this.config.strokeColor,
48
- strokeOpacity: this.config.strokeOpacity,
49
- tool: this.name
50
- });
51
-
52
- // Add first point
53
- this.addPoint(event.x, event.y, event.pressure);
54
-
55
- // Add stroke to canvas
56
- const strokeId = this.canvas.addStroke(this.currentStroke);
57
- console.log('[Pencil Debug] Added stroke to canvas with ID:', strokeId, 'Total strokes:', this.canvas.strokes.size);
58
-
59
- this.canvas.emit('strokeStarted', {
60
- stroke: this.currentStroke,
61
- tool: this.name,
62
- point: { x: event.x, y: event.y, pressure: event.pressure }
63
- });
64
- }
65
-
66
- /**
67
- * Continue drawing the stroke
68
- */
69
- onPointerMove(event) {
70
- if (!this.isDrawing || !this.currentStroke) return;
71
-
72
- // Process coalesced events for higher precision if available
73
- if (event.coalescedEvents && event.coalescedEvents.length > 0) {
74
- // Limit the number of coalesced events processed to prevent performance issues
75
- const maxCoalescedEvents = 5;
76
- const eventsToProcess = event.coalescedEvents.slice(0, maxCoalescedEvents);
77
-
78
- for (const coalescedEvent of eventsToProcess) {
79
- this._addPointIfNeeded(coalescedEvent.x, coalescedEvent.y, coalescedEvent.pressure);
80
- }
81
- } else {
82
- // Fallback to regular event
83
- this._addPointIfNeeded(event.x, event.y, event.pressure);
84
- }
85
-
86
- this.canvas.emit('strokeContinued', {
87
- stroke: this.currentStroke,
88
- tool: this.name,
89
- point: { x: event.x, y: event.y, pressure: event.pressure }
90
- });
91
- }
92
-
93
- /**
94
- * Finish drawing the stroke
95
- */
96
- onPointerUp(event) {
97
- if (!this.isDrawing || !this.currentStroke) return;
98
-
99
- // Add final point
100
- this.addPoint(event.x, event.y, event.pressure);
101
-
102
- // Finish the stroke
103
- this.currentStroke.finish();
104
-
105
- console.log('[Pencil Debug] Finished stroke with', this.points.length, 'points. Canvas now has', this.canvas.strokes.size, 'strokes');
106
-
107
- this.canvas.emit('strokeCompleted', {
108
- stroke: this.currentStroke,
109
- tool: this.name,
110
- totalPoints: this.points.length
111
- });
112
-
113
- // Reset drawing state
114
- this.isDrawing = false;
115
- this.currentStroke = null;
116
- this.points = [];
117
- this.lastPoint = null;
118
- }
119
-
120
- /**
121
- * Cancel current stroke
122
- */
123
- onCancel() {
124
- if (this.isDrawing && this.currentStroke) {
125
- // Remove incomplete stroke
126
- this.canvas.removeStroke(this.currentStroke.id);
127
-
128
- this.canvas.emit('strokeCancelled', {
129
- stroke: this.currentStroke,
130
- tool: this.name
131
- });
132
- }
133
-
134
- super.onCancel();
135
-
136
- // Reset state
137
- this.points = [];
138
- this.lastPoint = null;
139
- }
140
-
141
- /**
142
- * Add a point to the current stroke
143
- * @private
144
- */
145
- addPoint(x, y, pressure = 0.5) {
146
- const point = {
147
- x,
148
- y,
149
- pressure,
150
- width: this.calculateStrokeWidth(pressure),
151
- timestamp: Date.now()
152
- };
153
-
154
- this.points.push(point);
155
-
156
- if (this.currentStroke) {
157
- this.currentStroke.addPoint(point);
158
- }
159
- }
160
-
161
- /**
162
- * Add a point only if it meets distance requirements
163
- * @private
164
- */
165
- _addPointIfNeeded(x, y, pressure = 0.5) {
166
- if (!this.lastPoint) {
167
- this.addPoint(x, y, pressure);
168
- this.lastPoint = { x, y };
169
- return;
170
- }
171
-
172
- const distance = this.getDistance(this.lastPoint, { x, y });
173
-
174
- // Add point if moved enough distance
175
- if (distance >= this.minDistance) {
176
- // Only interpolate for very large gaps to prevent performance issues
177
- if (distance > 8) {
178
- this._interpolatePoints(this.lastPoint, { x, y, pressure });
179
- } else {
180
- this.addPoint(x, y, pressure);
181
- }
182
- this.lastPoint = { x, y };
183
- }
184
- }
185
-
186
- /**
187
- * Add interpolated points between two points for smoother strokes
188
- * @private
189
- */
190
- _interpolatePoints(fromPoint, toPoint) {
191
- const distance = this.getDistance(fromPoint, toPoint);
192
- // Increase spacing to reduce point density - point every 3 pixels instead of 1.5
193
- const steps = Math.ceil(distance / 3);
194
-
195
- // Limit maximum interpolation steps to prevent performance issues
196
- const maxSteps = 10;
197
- const actualSteps = Math.min(steps, maxSteps);
198
-
199
- for (let i = 1; i <= actualSteps; i++) {
200
- const t = i / actualSteps;
201
- const x = fromPoint.x + (toPoint.x - fromPoint.x) * t;
202
- const y = fromPoint.y + (toPoint.y - fromPoint.y) * t;
203
- const pressure = fromPoint.pressure ?
204
- fromPoint.pressure + (toPoint.pressure - fromPoint.pressure) * t :
205
- toPoint.pressure;
206
-
207
- this.addPoint(x, y, pressure);
208
- }
209
- }
210
-
211
- /**
212
- * Calculate distance between two points
213
- * @private
214
- */
215
- getDistance(p1, p2) {
216
- const dx = p2.x - p1.x;
217
- const dy = p2.y - p1.y;
218
- return Math.sqrt(dx * dx + dy * dy);
219
- }
220
-
221
- /**
222
- * Update configuration and apply smoothing
223
- */
224
- onConfigUpdate() {
225
- // Update minimum distance based on stroke width
226
- this.minDistance = Math.max(1, this.config.strokeWidth * 0.2);
227
-
228
- // Update current stroke if drawing
229
- if (this.isDrawing && this.currentStroke) {
230
- this.currentStroke.updateConfig({
231
- strokeColor: this.config.strokeColor,
232
- strokeOpacity: this.config.strokeOpacity
233
- });
234
- }
235
-
236
- // Update cursor size
237
- if (this.canvas.cursor) {
238
- this.canvas.cursor.updateFromToolConfig(this.config);
239
- }
240
- }
241
-
242
- /**
243
- * Calculate stroke width with pressure sensitivity
244
- */
245
- calculateStrokeWidth(pressure = 0.5) {
246
- if (!this.config.pressureSensitive) {
247
- return this.config.strokeWidth;
248
- }
249
-
250
- return super.calculateStrokeWidth(pressure);
251
- }
252
-
253
- /**
254
- * Get smoothed points using interpolation
255
- * @param {Array} points - Array of points to smooth
256
- * @returns {Array} Smoothed points
257
- */
258
- getSmoothPath(points) {
259
- if (points.length < 2) return points;
260
-
261
- const smoothed = [];
262
- const smoothing = this.config.smoothing;
263
-
264
- // First point
265
- smoothed.push(points[0]);
266
-
267
- // Smooth intermediate points
268
- for (let i = 1; i < points.length - 1; i++) {
269
- const prev = points[i - 1];
270
- const curr = points[i];
271
- const next = points[i + 1];
272
-
273
- const smoothedPoint = {
274
- x: curr.x + smoothing * ((prev.x + next.x) / 2 - curr.x),
275
- y: curr.y + smoothing * ((prev.y + next.y) / 2 - curr.y),
276
- pressure: curr.pressure,
277
- width: curr.width,
278
- timestamp: curr.timestamp
279
- };
280
-
281
- smoothed.push(smoothedPoint);
282
- }
283
-
284
- // Last point
285
- if (points.length > 1) {
286
- smoothed.push(points[points.length - 1]);
287
- }
288
-
289
- return smoothed;
290
- }
291
-
292
- /**
293
- * Handle keyboard shortcuts specific to pencil tool
294
- */
295
- onKeyboardShortcut(key, event) {
296
- switch (key) {
297
- case '[':
298
- // Decrease brush size
299
- this.updateConfig({
300
- strokeWidth: Math.max(1, this.config.strokeWidth - 1)
301
- });
302
- // Notify tool manager of config change
303
- this.canvas.toolManager.updateToolConfig(this.name, this.config);
304
- return true;
305
- case ']':
306
- // Increase brush size
307
- this.updateConfig({
308
- strokeWidth: Math.min(50, this.config.strokeWidth + 1)
309
- });
310
- // Notify tool manager of config change
311
- this.canvas.toolManager.updateToolConfig(this.name, this.config);
312
- return true;
313
- default:
314
- return super.onKeyboardShortcut(key, event);
315
- }
316
- }
317
-
318
- /**
319
- * Get help text for pencil tool
320
- */
321
- getHelpText() {
322
- return `${super.getHelpText()}\nShortcuts: [ ] to adjust brush size`;
323
- }
324
- }
1
+ import { Tool } from './tool.js';
2
+ import { Stroke } from '../drawing/stroke.js';
3
+
4
+ /**
5
+ * Pencil tool for freehand drawing
6
+ */
7
+ export class PencilTool extends Tool {
8
+ constructor(canvas, options = {}) {
9
+ super(canvas, {
10
+ strokeWidth: 5,
11
+ strokeColor: '#000000',
12
+ strokeOpacity: 1,
13
+ smoothing: 0.5,
14
+ pressureSensitive: true,
15
+ ...options
16
+ });
17
+
18
+ this.displayName = 'Pencil';
19
+ this.description = 'Draw freehand strokes';
20
+ this.icon = 'pencil';
21
+ this.shortcut = 'P';
22
+ this.category = 'drawing';
23
+
24
+ // Drawing state
25
+ this.points = [];
26
+ this.lastPoint = null;
27
+ this.minDistance = 2.0; // Minimum distance between points (increased for better performance)
28
+ }
29
+
30
+ /**
31
+ * Start drawing a new stroke
32
+ */
33
+ onPointerDown(event) {
34
+ if (!this.canUse()) return;
35
+
36
+
37
+ this.isDrawing = true;
38
+ this.points = [];
39
+ this.lastPoint = { x: event.x, y: event.y };
40
+
41
+ // Create new stroke
42
+ this.currentStroke = new Stroke({
43
+ x: event.x,
44
+ y: event.y,
45
+ strokeWidth: this.calculateStrokeWidth(event.pressure),
46
+ strokeColor: this.config.strokeColor,
47
+ strokeOpacity: this.config.strokeOpacity,
48
+ tool: this.name
49
+ });
50
+
51
+ // Add first point
52
+ this.addPoint(event.x, event.y, event.pressure);
53
+
54
+ // Add stroke to canvas
55
+ const strokeId = this.canvas.addStroke(this.currentStroke);
56
+
57
+ this.canvas.emit('strokeStarted', {
58
+ stroke: this.currentStroke,
59
+ tool: this.name,
60
+ point: { x: event.x, y: event.y, pressure: event.pressure }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Continue drawing the stroke
66
+ */
67
+ onPointerMove(event) {
68
+ if (!this.isDrawing || !this.currentStroke) return;
69
+
70
+ // Process coalesced events for higher precision if available
71
+ if (event.coalescedEvents && event.coalescedEvents.length > 0) {
72
+ // Limit the number of coalesced events processed to prevent performance issues
73
+ const maxCoalescedEvents = 5;
74
+ const eventsToProcess = event.coalescedEvents.slice(0, maxCoalescedEvents);
75
+
76
+ for (const coalescedEvent of eventsToProcess) {
77
+ this._addPointIfNeeded(coalescedEvent.x, coalescedEvent.y, coalescedEvent.pressure);
78
+ }
79
+ } else {
80
+ // Fallback to regular event
81
+ this._addPointIfNeeded(event.x, event.y, event.pressure);
82
+ }
83
+
84
+ this.canvas.emit('strokeContinued', {
85
+ stroke: this.currentStroke,
86
+ tool: this.name,
87
+ point: { x: event.x, y: event.y, pressure: event.pressure }
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Finish drawing the stroke
93
+ */
94
+ onPointerUp(event) {
95
+ if (!this.isDrawing || !this.currentStroke) return;
96
+
97
+ // Add final point
98
+ this.addPoint(event.x, event.y, event.pressure);
99
+
100
+ // Finish the stroke
101
+ this.currentStroke.finish();
102
+
103
+
104
+ this.canvas.emit('strokeCompleted', {
105
+ stroke: this.currentStroke,
106
+ tool: this.name,
107
+ totalPoints: this.points.length
108
+ });
109
+
110
+ // Reset drawing state
111
+ this.isDrawing = false;
112
+ this.currentStroke = null;
113
+ this.points = [];
114
+ this.lastPoint = null;
115
+ }
116
+
117
+ /**
118
+ * Cancel current stroke
119
+ */
120
+ onCancel() {
121
+ if (this.isDrawing && this.currentStroke) {
122
+ // Remove incomplete stroke
123
+ this.canvas.removeStroke(this.currentStroke.id);
124
+
125
+ this.canvas.emit('strokeCancelled', {
126
+ stroke: this.currentStroke,
127
+ tool: this.name
128
+ });
129
+ }
130
+
131
+ super.onCancel();
132
+
133
+ // Reset state
134
+ this.points = [];
135
+ this.lastPoint = null;
136
+ }
137
+
138
+ /**
139
+ * Add a point to the current stroke
140
+ * @private
141
+ */
142
+ addPoint(x, y, pressure = 0.5) {
143
+ const point = {
144
+ x,
145
+ y,
146
+ pressure,
147
+ width: this.calculateStrokeWidth(pressure),
148
+ timestamp: Date.now()
149
+ };
150
+
151
+ this.points.push(point);
152
+
153
+ if (this.currentStroke) {
154
+ this.currentStroke.addPoint(point);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Add a point only if it meets distance requirements
160
+ * @private
161
+ */
162
+ _addPointIfNeeded(x, y, pressure = 0.5) {
163
+ if (!this.lastPoint) {
164
+ this.addPoint(x, y, pressure);
165
+ this.lastPoint = { x, y };
166
+ return;
167
+ }
168
+
169
+ const distance = this.getDistance(this.lastPoint, { x, y });
170
+
171
+ // Add point if moved enough distance
172
+ if (distance >= this.minDistance) {
173
+ // Only interpolate for very large gaps to prevent performance issues
174
+ if (distance > 8) {
175
+ this._interpolatePoints(this.lastPoint, { x, y, pressure });
176
+ } else {
177
+ this.addPoint(x, y, pressure);
178
+ }
179
+ this.lastPoint = { x, y };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Add interpolated points between two points for smoother strokes
185
+ * @private
186
+ */
187
+ _interpolatePoints(fromPoint, toPoint) {
188
+ const distance = this.getDistance(fromPoint, toPoint);
189
+ // Increase spacing to reduce point density - point every 3 pixels instead of 1.5
190
+ const steps = Math.ceil(distance / 3);
191
+
192
+ // Limit maximum interpolation steps to prevent performance issues
193
+ const maxSteps = 10;
194
+ const actualSteps = Math.min(steps, maxSteps);
195
+
196
+ for (let i = 1; i <= actualSteps; i++) {
197
+ const t = i / actualSteps;
198
+ const x = fromPoint.x + (toPoint.x - fromPoint.x) * t;
199
+ const y = fromPoint.y + (toPoint.y - fromPoint.y) * t;
200
+ const pressure = fromPoint.pressure ?
201
+ fromPoint.pressure + (toPoint.pressure - fromPoint.pressure) * t :
202
+ toPoint.pressure;
203
+
204
+ this.addPoint(x, y, pressure);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Calculate distance between two points
210
+ * @private
211
+ */
212
+ getDistance(p1, p2) {
213
+ const dx = p2.x - p1.x;
214
+ const dy = p2.y - p1.y;
215
+ return Math.sqrt(dx * dx + dy * dy);
216
+ }
217
+
218
+ /**
219
+ * Update configuration and apply smoothing
220
+ */
221
+ onConfigUpdate() {
222
+ // Update minimum distance based on stroke width
223
+ this.minDistance = Math.max(1, this.config.strokeWidth * 0.2);
224
+
225
+ // Update current stroke if drawing
226
+ if (this.isDrawing && this.currentStroke) {
227
+ this.currentStroke.updateConfig({
228
+ strokeColor: this.config.strokeColor,
229
+ strokeOpacity: this.config.strokeOpacity
230
+ });
231
+ }
232
+
233
+ // Update cursor size
234
+ if (this.canvas.cursor) {
235
+ this.canvas.cursor.updateFromToolConfig(this.config);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Calculate stroke width with pressure sensitivity
241
+ */
242
+ calculateStrokeWidth(pressure = 0.5) {
243
+ if (!this.config.pressureSensitive) {
244
+ return this.config.strokeWidth;
245
+ }
246
+
247
+ return super.calculateStrokeWidth(pressure);
248
+ }
249
+
250
+ /**
251
+ * Get smoothed points using interpolation
252
+ * @param {Array} points - Array of points to smooth
253
+ * @returns {Array} Smoothed points
254
+ */
255
+ getSmoothPath(points) {
256
+ if (points.length < 2) return points;
257
+
258
+ const smoothed = [];
259
+ const smoothing = this.config.smoothing;
260
+
261
+ // First point
262
+ smoothed.push(points[0]);
263
+
264
+ // Smooth intermediate points
265
+ for (let i = 1; i < points.length - 1; i++) {
266
+ const prev = points[i - 1];
267
+ const curr = points[i];
268
+ const next = points[i + 1];
269
+
270
+ const smoothedPoint = {
271
+ x: curr.x + smoothing * ((prev.x + next.x) / 2 - curr.x),
272
+ y: curr.y + smoothing * ((prev.y + next.y) / 2 - curr.y),
273
+ pressure: curr.pressure,
274
+ width: curr.width,
275
+ timestamp: curr.timestamp
276
+ };
277
+
278
+ smoothed.push(smoothedPoint);
279
+ }
280
+
281
+ // Last point
282
+ if (points.length > 1) {
283
+ smoothed.push(points[points.length - 1]);
284
+ }
285
+
286
+ return smoothed;
287
+ }
288
+
289
+ /**
290
+ * Handle keyboard shortcuts specific to pencil tool
291
+ */
292
+ onKeyboardShortcut(key, event) {
293
+ switch (key) {
294
+ case '[':
295
+ // Decrease brush size
296
+ this.updateConfig({
297
+ strokeWidth: Math.max(1, this.config.strokeWidth - 1)
298
+ });
299
+ // Notify tool manager of config change
300
+ this.canvas.toolManager.updateToolConfig(this.name, this.config);
301
+ return true;
302
+ case ']':
303
+ // Increase brush size
304
+ this.updateConfig({
305
+ strokeWidth: Math.min(50, this.config.strokeWidth + 1)
306
+ });
307
+ // Notify tool manager of config change
308
+ this.canvas.toolManager.updateToolConfig(this.name, this.config);
309
+ return true;
310
+ default:
311
+ return super.onKeyboardShortcut(key, event);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Get help text for pencil tool
317
+ */
318
+ getHelpText() {
319
+ return `${super.getHelpText()}\nShortcuts: [ ] to adjust brush size`;
320
+ }
321
+ }