@teachinglab/omd 0.7.6 → 0.7.8

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.
@@ -127,6 +127,11 @@ export class SelectTool extends Tool {
127
127
  this.draggedOMDElement = omdElement; // Primary drag target
128
128
  this.startPoint = { x: event.x, y: event.y };
129
129
 
130
+ // Show resize handles if this is the only selected element
131
+ if (this.selectedOMDElements.size === 1) {
132
+ this.resizeHandleManager.selectElement(omdElement);
133
+ }
134
+
130
135
  if (this.canvas.eventManager) {
131
136
  this.canvas.eventManager.isDrawing = true;
132
137
  }
@@ -903,11 +908,7 @@ export class SelectTool extends Tool {
903
908
  }
904
909
 
905
910
  // Update resize handles
906
- if (this.selectedOMDElements.size === 1 && this.selectedSegments.size === 0) {
907
- this.resizeHandleManager.selectElement(this.selectedOMDElements.values().next().value);
908
- } else {
909
- this.resizeHandleManager.clearSelection();
910
- }
911
+ this.resizeHandleManager.clearSelection();
911
912
 
912
913
  this._updateSegmentSelectionVisuals();
913
914
  }
@@ -1064,16 +1064,29 @@ _propagateBackgroundStyle(style, visited = new Set()) {
1064
1064
  const rightExpr = this._normalizeExpressionString(this.getRight().toString());
1065
1065
 
1066
1066
  let graphEquations = [];
1067
+
1068
+ // Helper to check if expression is just 'y' (case-insensitive)
1069
+ const isY = (expr) => expr.trim().toLowerCase() === 'y';
1070
+
1067
1071
  if (options.side === 'left') {
1068
- graphEquations = [{ equation: `y = ${leftExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'blue', strokeWidth: 2 }];
1072
+ if (!isY(leftExpr)) {
1073
+ graphEquations.push({ equation: `y = ${leftExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'blue', strokeWidth: 2 });
1074
+ }
1069
1075
  } else if (options.side === 'right') {
1070
- graphEquations = [{ equation: `y = ${rightExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'red', strokeWidth: 2 }];
1076
+ if (!isY(rightExpr)) {
1077
+ graphEquations.push({ equation: `y = ${rightExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'red', strokeWidth: 2 });
1078
+ }
1071
1079
  } else {
1072
1080
  // both: plot left and right as two functions; intersection corresponds to equality
1073
- graphEquations = [
1074
- { equation: `y = ${leftExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'blue', strokeWidth: 2 },
1075
- { equation: `y = ${rightExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'red', strokeWidth: 2 }
1076
- ];
1081
+ // Skip 'y' sides to avoid plotting "y=y"
1082
+ if (!isY(leftExpr)) {
1083
+ graphEquations.push({ equation: `y = ${leftExpr}`, domain: { min: options.xMin, max: options.xMax }, color: 'blue', strokeWidth: 2 });
1084
+ }
1085
+ if (!isY(rightExpr)) {
1086
+ // If we skipped left because it was y, use blue for right side (primary function color)
1087
+ const color = isY(leftExpr) ? 'blue' : 'red';
1088
+ graphEquations.push({ equation: `y = ${rightExpr}`, domain: { min: options.xMin, max: options.xMax }, color: color, strokeWidth: 2 });
1089
+ }
1077
1090
  }
1078
1091
 
1079
1092
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -363,25 +363,34 @@ export class omdCoordinatePlane extends jsvgGroup {
363
363
 
364
364
  graphMultipleFunctions(holder) {
365
365
  for (const functionConfig of this.graphEquations) {
366
- const path = new jsvgPath();
367
- path.setStrokeColor(this.getAllowedColor(functionConfig.color));
368
- path.setStrokeWidth(functionConfig.strokeWidth);
369
- this.graphFunctionWithDomain(path, functionConfig.equation, functionConfig.domain);
370
- holder.addChild(path);
371
-
372
- if (functionConfig.label) {
373
- this.addFunctionLabel(holder, functionConfig);
366
+ try {
367
+ const path = new jsvgPath();
368
+ path.setStrokeColor(this.getAllowedColor(functionConfig.color));
369
+ path.setStrokeWidth(functionConfig.strokeWidth);
370
+ this.graphFunctionWithDomain(path, functionConfig.equation, functionConfig.domain);
371
+ holder.addChild(path);
372
+
373
+ if (functionConfig.label) {
374
+ this.addFunctionLabel(holder, functionConfig);
375
+ }
376
+ } catch (e) {
377
+ console.warn(`Failed to graph equation: ${functionConfig.equation}`, e);
374
378
  }
375
379
  }
376
380
  }
377
381
 
382
+ _extractExpression(equationString) {
383
+ let expression = String(equationString).trim();
384
+ // Remove "y=" from start or "=y" from end, case insensitive, handling spaces
385
+ // This converts "y = 3x+2" or "3x+2 = y" into "3x+2"
386
+ expression = expression.replace(/^\s*y\s*=\s*/i, '').replace(/\s*=\s*y\s*$/i, '');
387
+ return expression;
388
+ }
389
+
378
390
  graphFunctionWithDomain(pathObject, functionString, domain) {
379
391
  pathObject.clearPoints();
380
392
 
381
- let expression = functionString;
382
- if (expression.toLowerCase().startsWith("y=")) {
383
- expression = expression.substring(2).trim();
384
- }
393
+ const expression = this._extractExpression(functionString);
385
394
 
386
395
  const compiledExpression = math.compile(expression);
387
396
 
@@ -417,10 +426,8 @@ export class omdCoordinatePlane extends jsvgGroup {
417
426
  : this.xMin + (this.xMax - this.xMin) * 0.1;
418
427
 
419
428
  let anchorY = this.yMin + (this.yMax - this.yMin) * 0.1;
420
- let equationBody = functionConfig.equation;
421
- if (equationBody.toLowerCase().startsWith("y=")) {
422
- equationBody = equationBody.substring(2).trim();
423
- }
429
+ const equationBody = this._extractExpression(functionConfig.equation);
430
+
424
431
  const compiled = math.compile(equationBody);
425
432
  const yVal = compiled.evaluate({ x: anchorX });
426
433
  anchorY = yVal;
package/src/omdUtils.js CHANGED
@@ -22,12 +22,32 @@ export function parseExpressionString(str) {
22
22
  // term or number/variable
23
23
  const m = tok.match(/^([+-]?)(\d*\.?\d*)([a-zA-Z]?)(?:\^(\d+))?$/);
24
24
  if (m) {
25
- const sign = m[1] === '-' ? -1 : 1;
25
+ const signStr = m[1];
26
+ let sign = signStr === '-' ? -1 : 1;
26
27
  const coefRaw = m[2];
27
28
  const varChar = m[3] || '';
28
29
  const exp = m[4] ? Number(m[4]) : 1;
29
30
  let coef = 1;
30
31
  if (coefRaw && coefRaw.length > 0) coef = Number(coefRaw);
32
+
33
+ // Logic to split sign into operator if needed
34
+ // If we have a sign (+ or -), and we are NOT at the start and NOT following an operator,
35
+ // we should split it into an operator and a positive term.
36
+ let splitSign = false;
37
+ if (signStr) {
38
+ const isStart = out.length === 0;
39
+ const prevIsOperator = !isStart && out[out.length-1].omdType === 'operator';
40
+
41
+ if (!isStart && !prevIsOperator) {
42
+ splitSign = true;
43
+ }
44
+ }
45
+
46
+ if (splitSign) {
47
+ out.push({ omdType: 'operator', operator: signStr });
48
+ sign = 1; // Term becomes positive
49
+ }
50
+
31
51
  if (!varChar && coefRaw) {
32
52
  // pure number
33
53
  out.push({ omdType: 'number', value: sign * coef });
@@ -41,21 +61,27 @@ export function parseExpressionString(str) {
41
61
  }
42
62
 
43
63
  // If first token is an operator like '+' or '-', and followed by a term/number, fold it
64
+ // ONLY if it is a unary operator (at start or after another operator)
44
65
  const folded = [];
45
66
  for (let i = 0; i < out.length; i++) {
46
67
  const t = out[i];
47
68
  if (t.omdType === 'operator' && (t.operator === '+' || t.operator === '-')) {
48
- // If it's a leading sign attached to next term, merge
49
- const next = out[i+1];
50
- if (next && (next.omdType === 'term' || next.omdType === 'number')) {
51
- if (next.omdType === 'number') {
52
- next.value = (t.operator === '-') ? -next.value : next.value;
53
- } else if (next.omdType === 'term') {
54
- next.coefficient = (t.operator === '-') ? -next.coefficient : next.coefficient;
69
+ const isStart = folded.length === 0;
70
+ const prevIsOperator = !isStart && folded[folded.length-1].omdType === 'operator';
71
+
72
+ // Only fold if it's unary
73
+ if (isStart || prevIsOperator) {
74
+ const next = out[i+1];
75
+ if (next && (next.omdType === 'term' || next.omdType === 'number')) {
76
+ if (next.omdType === 'number') {
77
+ next.value = (t.operator === '-') ? -next.value : next.value;
78
+ } else if (next.omdType === 'term') {
79
+ next.coefficient = (t.operator === '-') ? -next.coefficient : next.coefficient;
80
+ }
81
+ i++; // skip next because we've merged
82
+ folded.push(next);
83
+ continue;
55
84
  }
56
- i++; // skip next because we've merged
57
- folded.push(next);
58
- continue;
59
85
  }
60
86
  }
61
87
  folded.push(t);