@xrn07/figure-renderer 0.2.0 → 0.2.2
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/README.md +149 -16
- package/dist/parse.d.ts +2 -2
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +26 -4
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +24 -2
- package/dist/renderers/forceDiagram.d.ts.map +1 -1
- package/dist/renderers/forceDiagram.js +57 -6
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/compass.d.ts +3 -2
- package/dist/utils/compass.d.ts.map +1 -1
- package/dist/utils/compass.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Zero-dependency pure TypeScript figure renderer - pure SVG string generation.
|
|
4
4
|
|
|
5
|
+
> **✨ Version 0.2.2 - Truly Generic Figure Renderer**
|
|
6
|
+
> Now supports ANY figure type without errors - physics, chemistry, math, biology, and more. No more "unknown figure type" errors!
|
|
7
|
+
>
|
|
8
|
+
> **✨ Version 0.2.1 - Enhanced API Format Support**
|
|
9
|
+
> Real-world API responses with flexible parsing, numeric magnitudes, and automatic field normalization.
|
|
10
|
+
>
|
|
5
11
|
> **⚠️ Version 0.2.0 Breaking Change**
|
|
6
|
-
>
|
|
12
|
+
> New flexible DSL format. See [MIGRATION_V0.2.0.md](MIGRATION_V0.2.0.md) for details.
|
|
7
13
|
|
|
8
14
|
## Features
|
|
9
15
|
|
|
10
16
|
- ✅ **Zero runtime dependencies** - Only ~8KB minified
|
|
11
17
|
- ✅ **Universal** - Works in Node.js, browser, Next.js SSR, PDF export
|
|
12
18
|
- ✅ **Type-safe** - Full TypeScript support
|
|
13
|
-
- ✅ **
|
|
19
|
+
- ✅ **Truly Generic** - Works with ANY figure type (physics, chemistry, math, biology, etc.)
|
|
20
|
+
- ✅ **No Errors** - Renders placeholder SVG for unknown types instead of crashing
|
|
14
21
|
- ✅ **Testable** - SVG strings are easily snapshot-able
|
|
15
22
|
- ✅ **Accessible** - React component with proper ARIA labels
|
|
16
23
|
- ✅ **International** - Localization support via `alt` field
|
|
24
|
+
- ✅ **API-friendly** - Handles both JSON strings and objects, numeric or string magnitudes
|
|
17
25
|
|
|
18
26
|
## Installation
|
|
19
27
|
|
|
@@ -39,10 +47,35 @@ console.log(svg); // <svg>...</svg>
|
|
|
39
47
|
import { FigureView } from "@xrn07/figure-renderer/react";
|
|
40
48
|
import { parseFigureDSL } from "@xrn07/figure-renderer";
|
|
41
49
|
|
|
50
|
+
// Parse DSL from API response (can be JSON string or object)
|
|
42
51
|
const dsl = parseFigureDSL(question.figure_dsl);
|
|
43
52
|
return <FigureView dsl={dsl} alt={question.figure_alt_bn} className='w-full' />;
|
|
44
53
|
```
|
|
45
54
|
|
|
55
|
+
### Real-World API Format (v0.2.1+)
|
|
56
|
+
|
|
57
|
+
The parser now handles real API responses directly:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// API returns object with numeric magnitude
|
|
61
|
+
const apiResponse = {
|
|
62
|
+
type: "force_diagram",
|
|
63
|
+
alt_bn: "বস্তুর উপর ক্রিয়াশীল বল",
|
|
64
|
+
arrows: [
|
|
65
|
+
{
|
|
66
|
+
direction: "right",
|
|
67
|
+
label: "10 N",
|
|
68
|
+
magnitude: 10, // Number instead of string
|
|
69
|
+
unit: "N",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Parse from object (not just JSON string)
|
|
75
|
+
const dsl = parseFigureDSL(apiResponse);
|
|
76
|
+
const svg = renderFigure(dsl);
|
|
77
|
+
```
|
|
78
|
+
|
|
46
79
|
## API
|
|
47
80
|
|
|
48
81
|
### `renderFigure(dsl: FigureDSL): string`
|
|
@@ -58,14 +91,26 @@ const svg = renderFigure({
|
|
|
58
91
|
});
|
|
59
92
|
```
|
|
60
93
|
|
|
61
|
-
### `parseFigureDSL(str: string): FigureDSL | null`
|
|
94
|
+
### `parseFigureDSL(str: string | object): FigureDSL | null`
|
|
62
95
|
|
|
63
|
-
Safe JSON parser with Zod validation. Returns `null` for invalid input.
|
|
96
|
+
Safe JSON parser with Zod validation. Accepts both JSON strings and objects. Returns `null` for invalid input.
|
|
97
|
+
|
|
98
|
+
**New in v0.2.1:**
|
|
99
|
+
|
|
100
|
+
- ✅ Accepts objects directly (not just JSON strings)
|
|
101
|
+
- ✅ Handles `alt_bn` field (automatically maps to `alt`)
|
|
102
|
+
- ✅ Supports numeric or string magnitudes
|
|
103
|
+
- ✅ Automatic field normalization for API responses
|
|
64
104
|
|
|
65
105
|
```typescript
|
|
106
|
+
// Parse from JSON string
|
|
66
107
|
const dsl = parseFigureDSL(jsonString);
|
|
108
|
+
|
|
109
|
+
// Parse from object (NEW!)
|
|
110
|
+
const dsl = parseFigureDSL(apiResponseObject);
|
|
111
|
+
|
|
67
112
|
if (dsl) {
|
|
68
|
-
// Valid DSL
|
|
113
|
+
// Valid DSL with normalized fields
|
|
69
114
|
}
|
|
70
115
|
```
|
|
71
116
|
|
|
@@ -79,8 +124,25 @@ React wrapper component for rendering figures.
|
|
|
79
124
|
|
|
80
125
|
## Supported Figure Types
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
The package is **truly generic** and supports ANY figure type:
|
|
128
|
+
|
|
129
|
+
- ✅ **`force_diagram`** - Force vector diagrams with compass directions (full renderer)
|
|
130
|
+
- ✅ **ANY custom type** - Renders placeholder SVG for unknown types (physics, chemistry, math, biology diagrams)
|
|
131
|
+
|
|
132
|
+
**No more errors!** If you try to render an unsupported type like `circuit_diagram`, `graph`, `geometry`, etc., the package will render a nice placeholder instead of crashing.
|
|
133
|
+
|
|
134
|
+
### Registering Custom Renderers
|
|
135
|
+
|
|
136
|
+
For specialized rendering of specific types, you can register custom renderers:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { registerRenderer } from "@xrn07/figure-renderer";
|
|
140
|
+
|
|
141
|
+
registerRenderer("circuit_diagram", (dsl) => {
|
|
142
|
+
// Your custom rendering logic
|
|
143
|
+
return `<svg>...</svg>`;
|
|
144
|
+
});
|
|
145
|
+
```
|
|
84
146
|
|
|
85
147
|
## DSL Schema (v0.2.0)
|
|
86
148
|
|
|
@@ -116,16 +178,9 @@ interface FigureDSL {
|
|
|
116
178
|
{
|
|
117
179
|
"direction": "up",
|
|
118
180
|
"label": "সমতলের প্রতিক্রিয়া বল",
|
|
119
|
-
"magnitude":
|
|
181
|
+
"magnitude": 100,
|
|
120
182
|
"unit": "N",
|
|
121
183
|
"color": "#2563eb"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"direction": "right",
|
|
125
|
-
"label": "ঘর্ষণ বল",
|
|
126
|
-
"magnitude": "50 N",
|
|
127
|
-
"unit": "N",
|
|
128
|
-
"color": "#16a34a"
|
|
129
184
|
}
|
|
130
185
|
]
|
|
131
186
|
}
|
|
@@ -133,6 +188,8 @@ interface FigureDSL {
|
|
|
133
188
|
|
|
134
189
|
**Compass Directions:** `up`, `down`, `left`, `right`, `up-left`, `up-right`, `down-left`, `down-right`
|
|
135
190
|
|
|
191
|
+
**New in v0.2.1:** Magnitude can be string (`"100 N"`) or number (`100`). The parser handles both formats automatically.
|
|
192
|
+
|
|
136
193
|
**Optional Object:**
|
|
137
194
|
|
|
138
195
|
```json
|
|
@@ -169,7 +226,34 @@ const circuitDSL = {
|
|
|
169
226
|
};
|
|
170
227
|
```
|
|
171
228
|
|
|
172
|
-
### Force Diagram
|
|
229
|
+
### Force Diagram (v0.2.0+ Format)
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const forceDSL = {
|
|
233
|
+
type: "force_diagram", // NEW: snake_case
|
|
234
|
+
width: 400, // Optional
|
|
235
|
+
height: 300, // Optional
|
|
236
|
+
arrows: [
|
|
237
|
+
// NEW: compass-based arrows
|
|
238
|
+
{
|
|
239
|
+
direction: "down", // Instead of angle: -90
|
|
240
|
+
label: "মাধ্যাকর্ষণ",
|
|
241
|
+
magnitude: "100 N", // Can be number or string
|
|
242
|
+
unit: "N",
|
|
243
|
+
color: "#dc2626",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
direction: "up",
|
|
247
|
+
label: "Normal Force",
|
|
248
|
+
magnitude: 100, // Number works too
|
|
249
|
+
unit: "N",
|
|
250
|
+
color: "#2563eb",
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Force Diagram (Old v0.1.x Format - Deprecated)
|
|
173
257
|
|
|
174
258
|
```typescript
|
|
175
259
|
const forceDSL = {
|
|
@@ -209,6 +293,55 @@ test('renders circuit diagram', () => {
|
|
|
209
293
|
- Gzipped: ~3KB
|
|
210
294
|
- Tree-shakeable: Unused renderers can be excluded
|
|
211
295
|
|
|
296
|
+
## Changelog
|
|
297
|
+
|
|
298
|
+
### v0.2.2 (2026-06-XX)
|
|
299
|
+
|
|
300
|
+
**Major Enhancement:**
|
|
301
|
+
|
|
302
|
+
- 🎉 **Truly Generic Package** - Now supports ANY figure type without errors
|
|
303
|
+
- ✨ No more "unknown figure type" crashes
|
|
304
|
+
- ✨ Renders placeholder SVG for unregistered types (physics, chemistry, math, biology, etc.)
|
|
305
|
+
- ✨ Graceful degradation instead of errors
|
|
306
|
+
- 🐛 Fixed arrow overlap for multiple arrows in same direction
|
|
307
|
+
- ✨ Automatic arrow stacking with proper spacing
|
|
308
|
+
|
|
309
|
+
**Improvements:**
|
|
310
|
+
|
|
311
|
+
- 📦 Package now works out-of-the-box for any educational content
|
|
312
|
+
- 🎯 Better developer experience - no need to pre-register every type
|
|
313
|
+
|
|
314
|
+
### v0.2.1 (2026-06-XX)
|
|
315
|
+
|
|
316
|
+
**Enhancements:**
|
|
317
|
+
|
|
318
|
+
- ✨ Accept objects directly in `parseFigureDSL()` (not just JSON strings)
|
|
319
|
+
- ✨ Support numeric magnitudes (e.g., `magnitude: 100` instead of `magnitude: "100 N"`)
|
|
320
|
+
- ✨ Automatic field normalization (e.g., `alt_bn` → `alt`)
|
|
321
|
+
- ✨ Flexible magnitude parsing with automatic unit handling
|
|
322
|
+
- 📝 Updated tests to verify real API format compatibility
|
|
323
|
+
|
|
324
|
+
**Bug Fixes:**
|
|
325
|
+
|
|
326
|
+
- 🐛 Fixed parsing of real-world API responses with numeric values
|
|
327
|
+
|
|
328
|
+
### v0.2.0 (2025-01-XX)
|
|
329
|
+
|
|
330
|
+
**Breaking Changes:**
|
|
331
|
+
|
|
332
|
+
- 🔴 Migrated from rigid enum types to generic string-based type system
|
|
333
|
+
- 🔴 Changed force diagram format from angle-based to compass directions
|
|
334
|
+
- 🔴 Renamed `forceDiagram` → `force_diagram`
|
|
335
|
+
- 🔴 Replaced `forces` array with `arrows` array
|
|
336
|
+
|
|
337
|
+
**New Features:**
|
|
338
|
+
|
|
339
|
+
- ✨ Compass direction system (up, down, left, right, etc.)
|
|
340
|
+
- ✨ Optional dimensions with sensible defaults
|
|
341
|
+
- ✨ Bengali and multilingual support via `alt` field
|
|
342
|
+
- ✨ Renderer registry for custom figure types
|
|
343
|
+
- ✨ Props customization system
|
|
344
|
+
|
|
212
345
|
## License
|
|
213
346
|
|
|
214
347
|
MIT
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { FigureDSL } from './types';
|
|
2
2
|
/**
|
|
3
|
-
* Safely parse FigureDSL from JSON string
|
|
3
|
+
* Safely parse FigureDSL from JSON string or object
|
|
4
4
|
* Returns null if parsing or validation fails
|
|
5
5
|
*/
|
|
6
|
-
export declare function parseFigureDSL(
|
|
6
|
+
export declare function parseFigureDSL(input: string | object): FigureDSL | null;
|
|
7
7
|
/**
|
|
8
8
|
* Validate a FigureDSL object
|
|
9
9
|
*/
|
package/dist/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAS,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAS,MAAM,SAAS,CAAC;AAuEhD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,CAwBvE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,SAAS,CAahE"}
|
package/dist/parse.js
CHANGED
|
@@ -10,7 +10,7 @@ const compassDirectionSchema = z.enum(['up', 'down', 'left', 'right', 'up-left',
|
|
|
10
10
|
const arrowSchema = z.object({
|
|
11
11
|
direction: compassDirectionSchema,
|
|
12
12
|
label: z.string(),
|
|
13
|
-
magnitude: z.string(),
|
|
13
|
+
magnitude: z.union([z.string(), z.number()]), // Accept both string and number
|
|
14
14
|
unit: z.string().optional(),
|
|
15
15
|
color: z.string().optional()
|
|
16
16
|
});
|
|
@@ -46,12 +46,34 @@ const forceDiagramSchema = z.object({
|
|
|
46
46
|
}).optional()
|
|
47
47
|
}).passthrough();
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* Normalize API response fields to DSL format
|
|
50
|
+
*/
|
|
51
|
+
function normalizeAPIResponse(obj) {
|
|
52
|
+
if (!obj || typeof obj !== 'object') {
|
|
53
|
+
return obj;
|
|
54
|
+
}
|
|
55
|
+
const normalized = { ...obj };
|
|
56
|
+
// Map alt_bn to alt
|
|
57
|
+
if (normalized.alt_bn && !normalized.alt) {
|
|
58
|
+
normalized.alt = normalized.alt_bn;
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Safely parse FigureDSL from JSON string or object
|
|
50
64
|
* Returns null if parsing or validation fails
|
|
51
65
|
*/
|
|
52
|
-
export function parseFigureDSL(
|
|
66
|
+
export function parseFigureDSL(input) {
|
|
53
67
|
try {
|
|
54
|
-
|
|
68
|
+
let parsed;
|
|
69
|
+
if (typeof input === 'string') {
|
|
70
|
+
parsed = JSON.parse(input);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
parsed = input;
|
|
74
|
+
}
|
|
75
|
+
// Normalize API response fields
|
|
76
|
+
parsed = normalizeAPIResponse(parsed);
|
|
55
77
|
// Detect type and use appropriate schema
|
|
56
78
|
if (parsed.type === 'force_diagram') {
|
|
57
79
|
return forceDiagramSchema.parse(parsed);
|
package/dist/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAazC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAazC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAcnD;AA+BD,mBAAmB,SAAS,CAAC;AAG7B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/render.js
CHANGED
|
@@ -29,8 +29,30 @@ export function renderFigure(dsl) {
|
|
|
29
29
|
if (renderer) {
|
|
30
30
|
return renderer(config);
|
|
31
31
|
}
|
|
32
|
-
// For unregistered types,
|
|
33
|
-
|
|
32
|
+
// For unregistered types, render a generic placeholder instead of throwing error
|
|
33
|
+
// This allows the package to work with ANY figure type without requiring pre-registration
|
|
34
|
+
return renderGenericFigure(config);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render a generic placeholder for unknown figure types
|
|
38
|
+
* This allows the package to work with any type without errors
|
|
39
|
+
*/
|
|
40
|
+
function renderGenericFigure(dsl) {
|
|
41
|
+
const { width = 400, height = 300, title, alt, type } = dsl;
|
|
42
|
+
const elements = [];
|
|
43
|
+
// Add a placeholder box
|
|
44
|
+
elements.push(`<rect x="${width / 2 - 100}" y="${height / 2 - 50}" width="200" height="100" fill="#f3f4f6" stroke="#9ca3af" stroke-width="2" rx="8"/>`);
|
|
45
|
+
// Add type label
|
|
46
|
+
elements.push(`<text x="${width / 2}" y="${height / 2 - 10}" text-anchor="middle" dominant-baseline="middle" font-size="16" font-weight="600" fill="#6b7280">Figure Type: ${type}</text>`);
|
|
47
|
+
// Add info text
|
|
48
|
+
elements.push(`<text x="${width / 2}" y="${height / 2 + 15}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#9ca3af">Custom renderer needed</text>`);
|
|
49
|
+
// Add title if present
|
|
50
|
+
if (title) {
|
|
51
|
+
elements.push(`<text x="${width / 2}" y="30" text-anchor="middle" font-size="18" font-weight="bold" fill="#374151">${title}</text>`);
|
|
52
|
+
}
|
|
53
|
+
return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${alt || title || type}">
|
|
54
|
+
${elements.join('\n')}
|
|
55
|
+
</svg>`;
|
|
34
56
|
}
|
|
35
57
|
// Re-export parser
|
|
36
58
|
export { parseFigureDSL, validateFigureDSL } from './parse';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forceDiagram.d.ts","sourceRoot":"","sources":["../../src/renderers/forceDiagram.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAyB,MAAM,UAAU,CAAC;AAKvE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"forceDiagram.d.ts","sourceRoot":"","sources":["../../src/renderers/forceDiagram.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAyB,MAAM,UAAU,CAAC;AAKvE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CA6DjE"}
|
|
@@ -26,9 +26,20 @@ export function renderForceDiagramV2(dsl) {
|
|
|
26
26
|
elements.push(createArrowMarkers(colors));
|
|
27
27
|
// Render object
|
|
28
28
|
elements.push(renderObject(obj, width, height));
|
|
29
|
-
//
|
|
29
|
+
// Group arrows by direction for offset calculation
|
|
30
|
+
const directionGroups = new Map();
|
|
30
31
|
arrows.forEach(arrow => {
|
|
31
|
-
|
|
32
|
+
const group = directionGroups.get(arrow.direction) || [];
|
|
33
|
+
group.push(arrow);
|
|
34
|
+
directionGroups.set(arrow.direction, group);
|
|
35
|
+
});
|
|
36
|
+
// Render arrows with automatic offset for grouped arrows
|
|
37
|
+
directionGroups.forEach((groupArrows, direction) => {
|
|
38
|
+
groupArrows.forEach((arrow, index) => {
|
|
39
|
+
// Calculate offset for multiple arrows in same direction
|
|
40
|
+
const offset = calculateArrowOffset(direction, index, groupArrows.length);
|
|
41
|
+
elements.push(renderArrow(arrow, centerX + offset.x, centerY + offset.y));
|
|
42
|
+
});
|
|
32
43
|
});
|
|
33
44
|
// Add title if present
|
|
34
45
|
if (title) {
|
|
@@ -107,11 +118,49 @@ function renderObject(obj, canvasWidth, canvasHeight) {
|
|
|
107
118
|
}
|
|
108
119
|
return svgTag('g', { class: 'object' }, elements.join(''));
|
|
109
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Calculate offset for arrows to prevent overlap
|
|
123
|
+
* When multiple arrows point in the same direction, they're stacked with spacing
|
|
124
|
+
*/
|
|
125
|
+
function calculateArrowOffset(direction, index, totalCount) {
|
|
126
|
+
// If only one arrow, no offset needed
|
|
127
|
+
if (totalCount <= 1) {
|
|
128
|
+
return { x: 0, y: 0 };
|
|
129
|
+
}
|
|
130
|
+
const spacing = 25; // Pixel spacing between parallel arrows
|
|
131
|
+
const totalOffset = ((totalCount - 1) * spacing) / 2;
|
|
132
|
+
// Determine offset direction based on arrow direction
|
|
133
|
+
// For horizontal arrows, offset vertically
|
|
134
|
+
// For vertical arrows, offset horizontally
|
|
135
|
+
// For diagonal, offset perpendicular to direction
|
|
136
|
+
switch (direction) {
|
|
137
|
+
case 'left':
|
|
138
|
+
case 'right':
|
|
139
|
+
// Stack vertically
|
|
140
|
+
return { x: 0, y: (index * spacing) - totalOffset };
|
|
141
|
+
case 'up':
|
|
142
|
+
case 'down':
|
|
143
|
+
// Stack horizontally
|
|
144
|
+
return { x: (index * spacing) - totalOffset, y: 0 };
|
|
145
|
+
case 'up-left':
|
|
146
|
+
case 'down-right':
|
|
147
|
+
// Stack perpendicular (45° offset)
|
|
148
|
+
const offset45 = (index * spacing) - totalOffset;
|
|
149
|
+
return { x: offset45, y: -offset45 };
|
|
150
|
+
case 'up-right':
|
|
151
|
+
case 'down-left':
|
|
152
|
+
// Stack perpendicular (-45° offset)
|
|
153
|
+
const offset45n = (index * spacing) - totalOffset;
|
|
154
|
+
return { x: offset45n, y: offset45n };
|
|
155
|
+
default:
|
|
156
|
+
return { x: 0, y: 0 };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
110
159
|
function renderArrow(arrow, centerX, centerY) {
|
|
111
160
|
const { direction, label, magnitude, color = '#dc2626' } = arrow;
|
|
112
161
|
const elements = [];
|
|
113
|
-
// Parse magnitude to get value
|
|
114
|
-
const { value: magnitudeValue } = parseMagnitude(magnitude);
|
|
162
|
+
// Parse magnitude to get value and unit
|
|
163
|
+
const { value: magnitudeValue, unit: magnitudeUnit } = parseMagnitude(magnitude);
|
|
115
164
|
// Calculate arrow length based on magnitude (with minimum and maximum)
|
|
116
165
|
const arrowLength = Math.max(40, Math.min(100, magnitudeValue * 0.8));
|
|
117
166
|
// Convert compass direction to angle
|
|
@@ -125,9 +174,11 @@ function renderArrow(arrow, centerX, centerY) {
|
|
|
125
174
|
'stroke-width': 3,
|
|
126
175
|
'marker-end': `url(#arrow-${color.replace('#', '')})`,
|
|
127
176
|
}));
|
|
177
|
+
// Format magnitude for display (add unit if not present)
|
|
178
|
+
const magnitudeDisplay = magnitudeUnit ? `${magnitudeValue} ${magnitudeUnit}` : `${magnitudeValue}`;
|
|
128
179
|
// Draw label with magnitude
|
|
129
|
-
const labelText = label ? `${label} (${
|
|
130
|
-
// Calculate label position (offset from arrow end)
|
|
180
|
+
const labelText = label ? `${label} (${magnitudeDisplay})` : magnitudeDisplay;
|
|
181
|
+
// Calculate label position (offset from arrow end, plus stack offset)
|
|
131
182
|
const angleRad = (angle * Math.PI) / 180;
|
|
132
183
|
const labelOffset = 20;
|
|
133
184
|
const labelX = end.x + labelOffset * Math.cos(angleRad);
|
package/dist/types.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ForceDiagramDSL extends FigureDSL {
|
|
|
23
23
|
export interface Arrow {
|
|
24
24
|
direction: 'up' | 'down' | 'left' | 'right' | 'up-left' | 'up-right' | 'down-left' | 'down-right';
|
|
25
25
|
label: string;
|
|
26
|
-
magnitude: string;
|
|
26
|
+
magnitude: string | number;
|
|
27
27
|
unit?: string;
|
|
28
28
|
color?: string;
|
|
29
29
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,CAAC;IAClG,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,CAAC;IAClG,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/utils/compass.d.ts
CHANGED
|
@@ -11,12 +11,13 @@ export declare function compassToAngle(direction: CompassDirection): number;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function angleToCompass(angle: number): CompassDirection;
|
|
13
13
|
/**
|
|
14
|
-
* Parse magnitude string to extract numeric value and unit
|
|
14
|
+
* Parse magnitude string or number to extract numeric value and unit
|
|
15
15
|
* Examples: "100 N" -> { value: 100, unit: "N" }
|
|
16
16
|
* "50 kg" -> { value: 50, unit: "kg" }
|
|
17
17
|
* "25" -> { value: 25, unit: "" }
|
|
18
|
+
* 25 -> { value: 25, unit: "" }
|
|
18
19
|
*/
|
|
19
|
-
export declare function parseMagnitude(magnitude: string): {
|
|
20
|
+
export declare function parseMagnitude(magnitude: string | number): {
|
|
20
21
|
value: number;
|
|
21
22
|
unit: string;
|
|
22
23
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compass.d.ts","sourceRoot":"","sources":["../../src/utils/compass.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,GACP,SAAS,GACT,UAAU,GACV,WAAW,GACX,YAAY,CAAC;AAEjB;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAalE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAa9D;AAED
|
|
1
|
+
{"version":3,"file":"compass.d.ts","sourceRoot":"","sources":["../../src/utils/compass.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,GACP,SAAS,GACT,UAAU,GACV,WAAW,GACX,YAAY,CAAC;AAEjB;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAalE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAa9D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAyB1F;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,gBAAgB,EAC3B,MAAM,EAAE,MAAM,GACb;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ1B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,gBAAgB,EAC3B,MAAM,EAAE,MAAM,GACb;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ1B"}
|
package/dist/utils/compass.js
CHANGED
|
@@ -39,12 +39,17 @@ export function angleToCompass(angle) {
|
|
|
39
39
|
return 'right'; // Default
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Parse magnitude string to extract numeric value and unit
|
|
42
|
+
* Parse magnitude string or number to extract numeric value and unit
|
|
43
43
|
* Examples: "100 N" -> { value: 100, unit: "N" }
|
|
44
44
|
* "50 kg" -> { value: 50, unit: "kg" }
|
|
45
45
|
* "25" -> { value: 25, unit: "" }
|
|
46
|
+
* 25 -> { value: 25, unit: "" }
|
|
46
47
|
*/
|
|
47
48
|
export function parseMagnitude(magnitude) {
|
|
49
|
+
// If it's already a number, return it
|
|
50
|
+
if (typeof magnitude === 'number') {
|
|
51
|
+
return { value: magnitude, unit: '' };
|
|
52
|
+
}
|
|
48
53
|
const trimmed = magnitude.trim();
|
|
49
54
|
// Try to extract numeric value and unit
|
|
50
55
|
const match = trimmed.match(/^(-?\d+\.?\d*)\s*(.*)$/);
|
package/package.json
CHANGED