@teachinglab/omd 0.6.6 → 0.7.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/canvas/features/resizeHandleManager.js +4 -0
- package/canvas/tools/SelectTool.js +8 -2
- package/canvas/ui/toolbar.js +40 -23
- package/index.js +2 -1
- package/jsvg/jsvgComponents.js +2 -1
- package/npm-docs/json-schemas.md +12 -27
- package/omd/display/omdDisplay.js +4 -13
- package/omd/nodes/omdFunctionNode.js +5 -1
- package/package.json +1 -1
- package/src/index.js +17 -3
- package/src/json-schemas.md +154 -27
- package/src/omd.js +8 -0
- package/src/omdDoubleNumberLine.js +72 -0
- package/src/omdDoubleTapeDiagram.js +115 -0
- package/src/omdEquation.js +64 -1
- package/src/omdFactory.js +4 -0
- package/src/omdNumberLine.js +173 -54
- package/src/omdTapeDiagram.js +161 -87
package/src/omdTapeDiagram.js
CHANGED
|
@@ -111,30 +111,26 @@ export class omdTapeDiagram extends jsvgGroup
|
|
|
111
111
|
|
|
112
112
|
this.type = "omdTapeDiagram";
|
|
113
113
|
|
|
114
|
+
this.title = "";
|
|
114
115
|
this.values = [];
|
|
115
|
-
this.showValues = true;
|
|
116
|
-
this.colors = [];
|
|
117
116
|
this.labelSet = [];
|
|
118
|
-
this.
|
|
117
|
+
this.totalWidth = 300;
|
|
119
118
|
this.updateLayout();
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
loadFromJSON( data )
|
|
123
122
|
{
|
|
124
|
-
if ( typeof data.
|
|
125
|
-
this.
|
|
123
|
+
if ( typeof data.title !== "undefined" )
|
|
124
|
+
this.title = data.title;
|
|
126
125
|
|
|
127
|
-
if ( typeof data.
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
if ( typeof data.colors != "undefined" )
|
|
131
|
-
this.colors = data.colors;
|
|
126
|
+
if ( typeof data.values !== "undefined" )
|
|
127
|
+
this.values = data.values;
|
|
132
128
|
|
|
133
|
-
if ( typeof data.labelSet
|
|
129
|
+
if ( typeof data.labelSet !== "undefined" )
|
|
134
130
|
this.labelSet = data.labelSet;
|
|
135
131
|
|
|
136
|
-
if ( typeof data.
|
|
137
|
-
this.
|
|
132
|
+
if ( typeof data.totalWidth !== "undefined" )
|
|
133
|
+
this.totalWidth = data.totalWidth;
|
|
138
134
|
|
|
139
135
|
this.updateLayout();
|
|
140
136
|
}
|
|
@@ -142,104 +138,182 @@ export class omdTapeDiagram extends jsvgGroup
|
|
|
142
138
|
setValues( newValues )
|
|
143
139
|
{
|
|
144
140
|
this.values = newValues;
|
|
141
|
+
this.updateLayout();
|
|
145
142
|
}
|
|
146
143
|
|
|
147
144
|
updateLayout()
|
|
148
145
|
{
|
|
149
146
|
this.removeAllChildren();
|
|
150
147
|
|
|
151
|
-
|
|
148
|
+
const leftPadding = this.title ? 80 : 20;
|
|
149
|
+
const rightPadding = 20;
|
|
150
|
+
const titleWidth = 70;
|
|
151
|
+
|
|
152
|
+
// Add title if present
|
|
153
|
+
if (this.title) {
|
|
154
|
+
const titleText = new jsvgTextBox();
|
|
155
|
+
titleText.setWidthAndHeight(titleWidth, 30);
|
|
156
|
+
titleText.setFontFamily("Albert Sans");
|
|
157
|
+
titleText.setFontColor("black");
|
|
158
|
+
titleText.setFontSize(12);
|
|
159
|
+
titleText.setAlignment("left");
|
|
160
|
+
titleText.setText(this.title);
|
|
161
|
+
titleText.setPosition(5, 5);
|
|
162
|
+
this.addChild(titleText);
|
|
163
|
+
}
|
|
152
164
|
|
|
153
|
-
//
|
|
154
|
-
|
|
165
|
+
// Parse values and calculate proportional widths
|
|
166
|
+
const parsedValues = [];
|
|
167
|
+
let totalNumericValue = 0;
|
|
168
|
+
|
|
169
|
+
for (const valueData of this.values) {
|
|
170
|
+
let value = "";
|
|
171
|
+
let showLabel = true;
|
|
172
|
+
let color = omdColor.lightGray;
|
|
173
|
+
let numericValue = 1; // default for non-numeric
|
|
174
|
+
|
|
175
|
+
// Handle both old format (simple values) and new format (objects)
|
|
176
|
+
if (typeof valueData === "object" && valueData !== null) {
|
|
177
|
+
value = valueData.value || "";
|
|
178
|
+
showLabel = valueData.showLabel !== undefined ? valueData.showLabel : true;
|
|
179
|
+
color = valueData.color || omdColor.lightGray;
|
|
180
|
+
} else {
|
|
181
|
+
value = valueData.toString();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Parse numeric value from string (e.g., "3", "2x", "5y")
|
|
185
|
+
// Extract coefficient from expressions like "2x", "3", "0.5y"
|
|
186
|
+
const match = value.match(/^([0-9.]+)?([a-zA-Z]*)$/);
|
|
187
|
+
if (match) {
|
|
188
|
+
const coefficient = match[1] ? parseFloat(match[1]) : (match[2] ? 1 : 1);
|
|
189
|
+
const variable = match[2] || "";
|
|
190
|
+
numericValue = coefficient;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
parsedValues.push({ value, showLabel, color, numericValue });
|
|
194
|
+
totalNumericValue += numericValue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Calculate width for each segment based on proportion
|
|
198
|
+
var pX = leftPadding;
|
|
155
199
|
var indexPositions = [];
|
|
156
|
-
|
|
157
|
-
{
|
|
200
|
+
|
|
201
|
+
for (const parsed of parsedValues) {
|
|
158
202
|
indexPositions.push(pX);
|
|
159
203
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
W = 20 + value.length*10;
|
|
165
|
-
}
|
|
166
|
-
else
|
|
167
|
-
{
|
|
168
|
-
W = value * this.unitWidth;
|
|
169
|
-
}
|
|
170
|
-
|
|
204
|
+
// Calculate proportional width
|
|
205
|
+
const proportion = totalNumericValue > 0 ? parsed.numericValue / totalNumericValue : 1 / parsedValues.length;
|
|
206
|
+
const segmentWidth = this.totalWidth * proportion;
|
|
171
207
|
|
|
172
|
-
//
|
|
208
|
+
// Make box
|
|
173
209
|
var box = new jsvgRect();
|
|
174
|
-
box.setWidthAndHeight(
|
|
210
|
+
box.setWidthAndHeight(segmentWidth, 30);
|
|
175
211
|
box.setCornerRadius(5);
|
|
176
|
-
box.setStrokeColor(
|
|
177
|
-
box.setStrokeWidth(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
box.setStrokeColor("white");
|
|
213
|
+
box.setStrokeWidth(1);
|
|
214
|
+
box.setFillColor(parsed.color);
|
|
215
|
+
box.setPosition(pX, 0);
|
|
216
|
+
this.addChild(box);
|
|
217
|
+
|
|
218
|
+
// Make box text (if showLabel is true)
|
|
219
|
+
if (parsed.showLabel) {
|
|
220
|
+
var boxText = new jsvgTextBox();
|
|
221
|
+
boxText.setWidthAndHeight(segmentWidth, 30);
|
|
222
|
+
boxText.setFontFamily("Albert Sans");
|
|
223
|
+
boxText.setFontColor("black");
|
|
224
|
+
boxText.setFontSize(18);
|
|
225
|
+
boxText.setAlignment("center");
|
|
226
|
+
boxText.setVerticalCentering();
|
|
227
|
+
boxText.setText(parsed.value);
|
|
228
|
+
boxText.setPosition(pX, 0);
|
|
229
|
+
this.addChild(boxText);
|
|
184
230
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
box.setPosition( pX, 0 );
|
|
188
|
-
this.addChild( box );
|
|
189
|
-
|
|
190
|
-
// make box text
|
|
191
|
-
var boxText = new jsvgTextBox();
|
|
192
|
-
boxText.setWidthAndHeight( W,30 );
|
|
193
|
-
boxText.setText ( this.name );
|
|
194
|
-
boxText.setFontFamily( "Albert Sans" );
|
|
195
|
-
boxText.setFontColor( "black" );
|
|
196
|
-
boxText.setFontSize( 18 );
|
|
197
|
-
boxText.setAlignment("center");
|
|
198
|
-
boxText.setVerticalCentering();
|
|
199
|
-
boxText.setText( value.toString() );
|
|
200
|
-
boxText.setPosition( pX, 0 );
|
|
201
|
-
this.addChild( boxText );
|
|
202
|
-
|
|
203
|
-
pX += W;
|
|
231
|
+
|
|
232
|
+
pX += segmentWidth;
|
|
204
233
|
}
|
|
205
234
|
|
|
206
235
|
indexPositions.push(pX);
|
|
207
236
|
|
|
208
|
-
// Calculate
|
|
209
|
-
var contentWidth = pX
|
|
210
|
-
var contentHeight = 30;
|
|
211
|
-
var
|
|
237
|
+
// Calculate dimensions
|
|
238
|
+
var contentWidth = pX - leftPadding;
|
|
239
|
+
var contentHeight = 30;
|
|
240
|
+
var topLabelSpace = 0;
|
|
241
|
+
var bottomLabelSpace = 0;
|
|
212
242
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
else
|
|
219
|
-
labelHeight = Math.max(labelHeight, 30); // 20 offset above + 10 buffer
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if ( labelHeight > 0 )
|
|
223
|
-
contentHeight += labelHeight;
|
|
243
|
+
// Sort labels by span length (number of segments they cover)
|
|
244
|
+
const sortedLabels = this.labelSet.slice().map((labelData, index) => ({
|
|
245
|
+
data: labelData,
|
|
246
|
+
span: (labelData.endIndex || 0) - (labelData.startIndex || 0)
|
|
247
|
+
})).sort((a, b) => a.span - b.span); // Shortest first
|
|
224
248
|
|
|
225
|
-
//
|
|
226
|
-
|
|
249
|
+
// Track occupied label layers to prevent overlap
|
|
250
|
+
const topLayers = [];
|
|
251
|
+
const bottomLayers = [];
|
|
252
|
+
|
|
253
|
+
// Make label text
|
|
254
|
+
for ( const item of sortedLabels )
|
|
227
255
|
{
|
|
228
|
-
|
|
229
|
-
T
|
|
256
|
+
const labelData = item.data;
|
|
257
|
+
const T = new omdTapeLabel();
|
|
230
258
|
T.setIndexPositions( indexPositions );
|
|
231
259
|
T.loadFromJSON( labelData );
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
|
|
261
|
+
if ( T.showBelow ) {
|
|
262
|
+
// Find the lowest available layer for this label
|
|
263
|
+
let layer = 0;
|
|
264
|
+
const start = labelData.startIndex || 0;
|
|
265
|
+
const end = labelData.endIndex || 0;
|
|
266
|
+
|
|
267
|
+
while (layer < bottomLayers.length) {
|
|
268
|
+
const conflicts = bottomLayers[layer].some(occupied =>
|
|
269
|
+
!(end <= occupied.start || start >= occupied.end)
|
|
270
|
+
);
|
|
271
|
+
if (!conflicts) break;
|
|
272
|
+
layer++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (layer === bottomLayers.length) {
|
|
276
|
+
bottomLayers.push([]);
|
|
277
|
+
}
|
|
278
|
+
bottomLayers[layer].push({ start, end });
|
|
279
|
+
|
|
280
|
+
const yPos = 40 + (layer * 35); // Stack labels with 35px spacing
|
|
281
|
+
T.setPosition(0, yPos);
|
|
282
|
+
bottomLabelSpace = Math.max(bottomLabelSpace, yPos + 30);
|
|
283
|
+
} else {
|
|
284
|
+
// Find the highest available layer for this label
|
|
285
|
+
let layer = 0;
|
|
286
|
+
const start = labelData.startIndex || 0;
|
|
287
|
+
const end = labelData.endIndex || 0;
|
|
288
|
+
|
|
289
|
+
while (layer < topLayers.length) {
|
|
290
|
+
const conflicts = topLayers[layer].some(occupied =>
|
|
291
|
+
!(end <= occupied.start || start >= occupied.end)
|
|
292
|
+
);
|
|
293
|
+
if (!conflicts) break;
|
|
294
|
+
layer++;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (layer === topLayers.length) {
|
|
298
|
+
topLayers.push([]);
|
|
299
|
+
}
|
|
300
|
+
topLayers[layer].push({ start, end });
|
|
301
|
+
|
|
302
|
+
const yPos = -10 - (layer * 35); // Stack labels upward with 35px spacing
|
|
303
|
+
T.setPosition(0, yPos);
|
|
304
|
+
topLabelSpace = Math.max(topLabelSpace, (layer + 1) * 35);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.addChild( T );
|
|
237
308
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.
|
|
241
|
-
//
|
|
242
|
-
|
|
309
|
+
// Set proper dimensions including space for labels above and below
|
|
310
|
+
this.width = leftPadding + contentWidth + rightPadding;
|
|
311
|
+
this.height = topLabelSpace + contentHeight + bottomLabelSpace;
|
|
312
|
+
// Adjust viewBox to show everything including title and labels
|
|
313
|
+
const viewBoxY = -topLabelSpace;
|
|
314
|
+
const viewBoxHeight = this.height;
|
|
315
|
+
this.svgObject.setAttribute("viewBox", `0 ${viewBoxY} ${this.width} ${viewBoxHeight}`);
|
|
316
|
+
this.svgObject.setAttribute("viewBox", `0 ${-topLabelSpace} ${this.width} ${this.height}`);
|
|
243
317
|
}
|
|
244
318
|
|
|
245
319
|
}
|