@xrn07/figure-renderer 0.1.2 → 0.2.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/README.md +67 -15
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +50 -7
- package/dist/props.d.ts +42 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +52 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +25 -30
- package/dist/renderers/forceDiagram.d.ts +4 -1
- package/dist/renderers/forceDiagram.d.ts.map +1 -1
- package/dist/renderers/forceDiagram.js +92 -66
- package/dist/types.d.ts +20 -203
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/utils/compass.d.ts +37 -0
- package/dist/utils/compass.d.ts.map +1 -0
- package/dist/utils/compass.js +85 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Zero-dependency pure TypeScript figure renderer - pure SVG string generation.
|
|
4
4
|
|
|
5
|
+
> **⚠️ Version 0.2.0 Breaking Change**
|
|
6
|
+
> This version introduces a new flexible DSL format. See [MIGRATION_V0.2.0.md](MIGRATION_V0.2.0.md) for details.
|
|
7
|
+
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
10
|
- ✅ **Zero runtime dependencies** - Only ~8KB minified
|
|
8
11
|
- ✅ **Universal** - Works in Node.js, browser, Next.js SSR, PDF export
|
|
9
12
|
- ✅ **Type-safe** - Full TypeScript support
|
|
13
|
+
- ✅ **Flexible** - Generic type system supports any figure type
|
|
10
14
|
- ✅ **Testable** - SVG strings are easily snapshot-able
|
|
11
15
|
- ✅ **Accessible** - React component with proper ARIA labels
|
|
16
|
+
- ✅ **International** - Localization support via `alt` field
|
|
12
17
|
|
|
13
18
|
## Installation
|
|
14
19
|
|
|
@@ -74,27 +79,74 @@ React wrapper component for rendering figures.
|
|
|
74
79
|
|
|
75
80
|
## Supported Figure Types
|
|
76
81
|
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
- `forceDiagram` - Force vector diagrams
|
|
80
|
-
- `geometry` - Geometric shapes
|
|
81
|
-
- `coordinatePlane` - XY coordinate systems
|
|
82
|
-
- `numberLine` - Number lines
|
|
83
|
-
- `graph` - Function graphs
|
|
84
|
-
- `venn` - Venn diagrams
|
|
85
|
-
- `bioTable` - Biology comparison tables
|
|
82
|
+
- **`force_diagram`** - Force vector diagrams with compass directions ✨ **NEW**
|
|
83
|
+
- More types coming soon (extensible via custom renderers)
|
|
86
84
|
|
|
87
|
-
## DSL Schema
|
|
85
|
+
## DSL Schema (v0.2.0)
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
The DSL is now **generic and flexible**. All figures share common base properties:
|
|
90
88
|
|
|
91
89
|
```typescript
|
|
92
90
|
interface FigureDSL {
|
|
93
|
-
type:
|
|
94
|
-
width
|
|
95
|
-
height
|
|
91
|
+
type: string; // Generic - any figure type supported
|
|
92
|
+
width?: number; // Optional - defaults to 400
|
|
93
|
+
height?: number; // Optional - defaults to 300
|
|
96
94
|
title?: string;
|
|
97
|
-
alt?: string;
|
|
95
|
+
alt?: string; // Localization support
|
|
96
|
+
[key: string]: any; // Additional properties per figure type
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Force Diagram DSL
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"type": "force_diagram",
|
|
105
|
+
"alt": "বস্তুটির উপর ক্রিয়াশীল বলগুলোর চিত্র",
|
|
106
|
+
"width": 400,
|
|
107
|
+
"height": 300,
|
|
108
|
+
"arrows": [
|
|
109
|
+
{
|
|
110
|
+
"direction": "down",
|
|
111
|
+
"label": "মাধ্যাকর্ষণ",
|
|
112
|
+
"magnitude": "100 N",
|
|
113
|
+
"unit": "N",
|
|
114
|
+
"color": "#dc2626"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"direction": "up",
|
|
118
|
+
"label": "সমতলের প্রতিক্রিয়া বল",
|
|
119
|
+
"magnitude": "100 N",
|
|
120
|
+
"unit": "N",
|
|
121
|
+
"color": "#2563eb"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"direction": "right",
|
|
125
|
+
"label": "ঘর্ষণ বল",
|
|
126
|
+
"magnitude": "50 N",
|
|
127
|
+
"unit": "N",
|
|
128
|
+
"color": "#16a34a"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Compass Directions:** `up`, `down`, `left`, `right`, `up-left`, `up-right`, `down-left`, `down-right`
|
|
135
|
+
|
|
136
|
+
**Optional Object:**
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"type": "force_diagram",
|
|
141
|
+
"object": {
|
|
142
|
+
"type": "box",
|
|
143
|
+
"x": 200,
|
|
144
|
+
"y": 150,
|
|
145
|
+
"width": 60,
|
|
146
|
+
"height": 60,
|
|
147
|
+
"label": "5kg"
|
|
148
|
+
},
|
|
149
|
+
"arrows": [...]
|
|
98
150
|
}
|
|
99
151
|
```
|
|
100
152
|
|
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,
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAS,MAAM,SAAS,CAAC;AAqDhD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAenE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,SAAS,CAahE"}
|
package/dist/parse.js
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
// Safe DSL parser with Zod validation
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Compass direction validation
|
|
5
|
+
*/
|
|
6
|
+
const compassDirectionSchema = z.enum(['up', 'down', 'left', 'right', 'up-left', 'up-right', 'down-left', 'down-right']);
|
|
7
|
+
/**
|
|
8
|
+
* Arrow validation schema
|
|
9
|
+
*/
|
|
10
|
+
const arrowSchema = z.object({
|
|
11
|
+
direction: compassDirectionSchema,
|
|
12
|
+
label: z.string(),
|
|
13
|
+
magnitude: z.string(),
|
|
14
|
+
unit: z.string().optional(),
|
|
15
|
+
color: z.string().optional()
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Base FigureDSL validation schema - flexible and generic
|
|
5
19
|
*/
|
|
6
20
|
const figureDSLSchema = z.lazy(() => z.object({
|
|
7
|
-
type: z.
|
|
8
|
-
width: z.number(),
|
|
9
|
-
height: z.number(),
|
|
21
|
+
type: z.string(), // Generic type - any string allowed
|
|
22
|
+
width: z.number().optional(), // Optional with defaults
|
|
23
|
+
height: z.number().optional(), // Optional with defaults
|
|
10
24
|
title: z.string().optional(),
|
|
11
25
|
alt: z.string().optional(),
|
|
12
|
-
// Allow additional properties for
|
|
26
|
+
// Allow additional properties for flexibility
|
|
13
27
|
}).passthrough());
|
|
28
|
+
/**
|
|
29
|
+
* Force diagram specific validation
|
|
30
|
+
*/
|
|
31
|
+
const forceDiagramSchema = z.object({
|
|
32
|
+
type: z.literal('force_diagram'),
|
|
33
|
+
width: z.number().optional(),
|
|
34
|
+
height: z.number().optional(),
|
|
35
|
+
title: z.string().optional(),
|
|
36
|
+
alt: z.string().optional(),
|
|
37
|
+
arrows: z.array(arrowSchema),
|
|
38
|
+
object: z.object({
|
|
39
|
+
type: z.enum(['box', 'sphere', 'point']),
|
|
40
|
+
x: z.number().optional(),
|
|
41
|
+
y: z.number().optional(),
|
|
42
|
+
width: z.number().optional(),
|
|
43
|
+
height: z.number().optional(),
|
|
44
|
+
radius: z.number().optional(),
|
|
45
|
+
label: z.string().optional()
|
|
46
|
+
}).optional()
|
|
47
|
+
}).passthrough();
|
|
14
48
|
/**
|
|
15
49
|
* Safely parse FigureDSL from JSON string
|
|
16
50
|
* Returns null if parsing or validation fails
|
|
@@ -18,6 +52,11 @@ const figureDSLSchema = z.lazy(() => z.object({
|
|
|
18
52
|
export function parseFigureDSL(jsonString) {
|
|
19
53
|
try {
|
|
20
54
|
const parsed = JSON.parse(jsonString);
|
|
55
|
+
// Detect type and use appropriate schema
|
|
56
|
+
if (parsed.type === 'force_diagram') {
|
|
57
|
+
return forceDiagramSchema.parse(parsed);
|
|
58
|
+
}
|
|
59
|
+
// Use generic schema for other types
|
|
21
60
|
return figureDSLSchema.parse(parsed);
|
|
22
61
|
}
|
|
23
62
|
catch (error) {
|
|
@@ -30,8 +69,12 @@ export function parseFigureDSL(jsonString) {
|
|
|
30
69
|
*/
|
|
31
70
|
export function validateFigureDSL(obj) {
|
|
32
71
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
const parsed = obj;
|
|
73
|
+
// Detect type and use appropriate schema
|
|
74
|
+
if (parsed?.type === 'force_diagram') {
|
|
75
|
+
return forceDiagramSchema.safeParse(parsed).success;
|
|
76
|
+
}
|
|
77
|
+
return figureDSLSchema.safeParse(obj).success;
|
|
35
78
|
}
|
|
36
79
|
catch {
|
|
37
80
|
return false;
|
package/dist/props.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FigureDSL, PhysicalObject } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Default configuration for force diagrams
|
|
4
|
+
*/
|
|
5
|
+
export interface ForceDiagramDefaults {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
object: PhysicalObject;
|
|
9
|
+
arrowLength: number;
|
|
10
|
+
arrowHeadSize: number;
|
|
11
|
+
colors: {
|
|
12
|
+
object: string;
|
|
13
|
+
arrowDefault: string;
|
|
14
|
+
arrowText: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Global defaults configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface FigureDefaults {
|
|
21
|
+
force_diagram: ForceDiagramDefaults;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Default props for force diagrams
|
|
25
|
+
*/
|
|
26
|
+
export declare const defaultForceDiagramProps: ForceDiagramDefaults;
|
|
27
|
+
/**
|
|
28
|
+
* Get default props for a specific figure type
|
|
29
|
+
*/
|
|
30
|
+
export declare function getDefaults(type: string): Partial<FigureDefaults[keyof FigureDefaults]>;
|
|
31
|
+
/**
|
|
32
|
+
* Merge user config with defaults
|
|
33
|
+
*/
|
|
34
|
+
export declare function withDefaults<T extends FigureDSL>(config: T, defaults?: Partial<FigureDefaults[keyof FigureDefaults]>): T;
|
|
35
|
+
/**
|
|
36
|
+
* Calculate center position for object
|
|
37
|
+
*/
|
|
38
|
+
export declare function calculateCenter(width: number, height: number): {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QACN,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,oBAAoB,CAAC;CACrC;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB,EAAE,oBAgBtC,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,cAAc,CAAC,CAAC,CAMvF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAC9C,MAAM,EAAE,CAAC,EACT,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,MAAM,cAAc,CAAC,CAAC,GACvD,CAAC,CAWH;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAKvF"}
|
package/dist/props.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Default props system for figure rendering
|
|
2
|
+
/**
|
|
3
|
+
* Default props for force diagrams
|
|
4
|
+
*/
|
|
5
|
+
export const defaultForceDiagramProps = {
|
|
6
|
+
width: 400,
|
|
7
|
+
height: 300,
|
|
8
|
+
object: {
|
|
9
|
+
type: 'box',
|
|
10
|
+
width: 60,
|
|
11
|
+
height: 60,
|
|
12
|
+
label: ''
|
|
13
|
+
},
|
|
14
|
+
arrowLength: 60,
|
|
15
|
+
arrowHeadSize: 10,
|
|
16
|
+
colors: {
|
|
17
|
+
object: '#374151',
|
|
18
|
+
arrowDefault: '#dc2626',
|
|
19
|
+
arrowText: '#1f2937'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Get default props for a specific figure type
|
|
24
|
+
*/
|
|
25
|
+
export function getDefaults(type) {
|
|
26
|
+
const defaults = {
|
|
27
|
+
force_diagram: defaultForceDiagramProps
|
|
28
|
+
};
|
|
29
|
+
return defaults[type] || {};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge user config with defaults
|
|
33
|
+
*/
|
|
34
|
+
export function withDefaults(config, defaults) {
|
|
35
|
+
const typeDefaults = getDefaults(config.type);
|
|
36
|
+
// Merge defaults with user config
|
|
37
|
+
const merged = {
|
|
38
|
+
...config,
|
|
39
|
+
width: config.width ?? (defaults?.width ?? typeDefaults?.width ?? 400),
|
|
40
|
+
height: config.height ?? (defaults?.height ?? typeDefaults?.height ?? 300)
|
|
41
|
+
};
|
|
42
|
+
return merged;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Calculate center position for object
|
|
46
|
+
*/
|
|
47
|
+
export function calculateCenter(width, height) {
|
|
48
|
+
return {
|
|
49
|
+
x: width / 2,
|
|
50
|
+
y: height / 2
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { FigureDSL } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Register a custom renderer for a specific figure type
|
|
4
|
+
* @param type - Figure type identifier
|
|
5
|
+
* @param renderer - Rendering function that takes DSL and returns SVG string
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerRenderer(type: string, renderer: (dsl: any) => string): void;
|
|
2
8
|
/**
|
|
3
9
|
* Main rendering function - dispatches to appropriate renderer based on DSL type
|
|
4
10
|
* @param dsl - Figure DSL object
|
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;
|
|
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,CAanD;AAGD,mBAAmB,SAAS,CAAC;AAG7B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/render.js
CHANGED
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
// Main entry point for figure rendering
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import { renderForceDiagramV2 } from './renderers/forceDiagram';
|
|
3
|
+
import { withDefaults } from './props';
|
|
4
|
+
/**
|
|
5
|
+
* Renderer registry for dynamic renderer lookup
|
|
6
|
+
* Allows adding new renderers without modifying the switch statement
|
|
7
|
+
*/
|
|
8
|
+
const renderRegistry = new Map();
|
|
9
|
+
// Register built-in renderers
|
|
10
|
+
renderRegistry.set('force_diagram', renderForceDiagramV2);
|
|
11
|
+
/**
|
|
12
|
+
* Register a custom renderer for a specific figure type
|
|
13
|
+
* @param type - Figure type identifier
|
|
14
|
+
* @param renderer - Rendering function that takes DSL and returns SVG string
|
|
15
|
+
*/
|
|
16
|
+
export function registerRenderer(type, renderer) {
|
|
17
|
+
renderRegistry.set(type, renderer);
|
|
18
|
+
}
|
|
11
19
|
/**
|
|
12
20
|
* Main rendering function - dispatches to appropriate renderer based on DSL type
|
|
13
21
|
* @param dsl - Figure DSL object
|
|
14
22
|
* @returns SVG string
|
|
15
23
|
*/
|
|
16
24
|
export function renderFigure(dsl) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return renderForceDiagram(dsl);
|
|
24
|
-
case 'geometry':
|
|
25
|
-
return renderGeometry(dsl);
|
|
26
|
-
case 'coordinatePlane':
|
|
27
|
-
return renderCoordinatePlane(dsl);
|
|
28
|
-
case 'numberLine':
|
|
29
|
-
return renderNumberLine(dsl);
|
|
30
|
-
case 'graph':
|
|
31
|
-
return renderGraph(dsl);
|
|
32
|
-
case 'venn':
|
|
33
|
-
return renderVenn(dsl);
|
|
34
|
-
case 'bioTable':
|
|
35
|
-
return renderBioTable(dsl);
|
|
36
|
-
default:
|
|
37
|
-
throw new Error(`Unknown figure type: ${dsl.type}`);
|
|
25
|
+
// Apply defaults if dimensions are not specified
|
|
26
|
+
const config = withDefaults(dsl);
|
|
27
|
+
// Check if we have a registered renderer for this type
|
|
28
|
+
const renderer = renderRegistry.get(config.type);
|
|
29
|
+
if (renderer) {
|
|
30
|
+
return renderer(config);
|
|
38
31
|
}
|
|
32
|
+
// For unregistered types, throw a helpful error
|
|
33
|
+
throw new Error(`Unknown figure type: "${config.type}". Register a renderer using registerRenderer("${config.type}", renderer)`);
|
|
39
34
|
}
|
|
40
35
|
// Re-export parser
|
|
41
36
|
export { parseFigureDSL, validateFigureDSL } from './parse';
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { ForceDiagramDSL } from '../types';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Render force diagram with new compass-based format
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderForceDiagramV2(dsl: ForceDiagramDSL): string;
|
|
3
6
|
//# sourceMappingURL=forceDiagram.d.ts.map
|
|
@@ -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,wBAAgB,
|
|
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,CAiDjE"}
|
|
@@ -1,31 +1,64 @@
|
|
|
1
|
-
// Force diagram renderer
|
|
1
|
+
// Force diagram renderer (new format with compass directions)
|
|
2
2
|
import { svgTag } from '../utils/svgTag';
|
|
3
3
|
import { rect, circle, text, line } from '../utils/shapes';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { compassToAngle, parseMagnitude, calculateArrowEndpoint, calculateArrowStart } from '../utils/compass';
|
|
5
|
+
/**
|
|
6
|
+
* Render force diagram with new compass-based format
|
|
7
|
+
*/
|
|
8
|
+
export function renderForceDiagramV2(dsl) {
|
|
9
|
+
const { width = 400, height = 300, arrows = [], object, title, alt } = dsl;
|
|
10
|
+
// Use default object if not specified
|
|
11
|
+
const defaultObject = {
|
|
12
|
+
type: 'box',
|
|
13
|
+
x: width / 2,
|
|
14
|
+
y: height / 2,
|
|
15
|
+
width: 60,
|
|
16
|
+
height: 60
|
|
17
|
+
};
|
|
18
|
+
const obj = object || defaultObject;
|
|
19
|
+
const centerX = obj.x ?? width / 2;
|
|
20
|
+
const centerY = obj.y ?? height / 2;
|
|
6
21
|
const elements = [];
|
|
7
|
-
// Add arrow markers
|
|
8
|
-
|
|
22
|
+
// Add arrow markers for all colors used
|
|
23
|
+
const colors = [...new Set(arrows.map(a => a.color).filter(Boolean))];
|
|
24
|
+
if (colors.length === 0)
|
|
25
|
+
colors.push('#dc2626'); // Default color
|
|
26
|
+
elements.push(createArrowMarkers(colors));
|
|
9
27
|
// Render object
|
|
10
|
-
elements.push(renderObject(
|
|
11
|
-
// Render
|
|
12
|
-
|
|
13
|
-
elements.push(
|
|
28
|
+
elements.push(renderObject(obj, width, height));
|
|
29
|
+
// Render arrows
|
|
30
|
+
arrows.forEach(arrow => {
|
|
31
|
+
elements.push(renderArrow(arrow, centerX, centerY));
|
|
14
32
|
});
|
|
33
|
+
// Add title if present
|
|
34
|
+
if (title) {
|
|
35
|
+
elements.push(text(width / 2, 20, title, {
|
|
36
|
+
'text-anchor': 'middle',
|
|
37
|
+
'font-size': 18,
|
|
38
|
+
'font-weight': 'bold',
|
|
39
|
+
fill: '#333',
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
15
42
|
const svg = svgTag('svg', {
|
|
16
43
|
viewBox: `0 0 ${width} ${height}`,
|
|
17
44
|
xmlns: 'http://www.w3.org/2000/svg',
|
|
45
|
+
role: 'img',
|
|
46
|
+
'aria-label': alt || title || 'Force diagram',
|
|
18
47
|
}, elements.join(''));
|
|
19
48
|
return svg;
|
|
20
49
|
}
|
|
21
|
-
function renderObject(obj) {
|
|
50
|
+
function renderObject(obj, canvasWidth, canvasHeight) {
|
|
22
51
|
const elements = [];
|
|
52
|
+
const x = obj.x ?? canvasWidth / 2;
|
|
53
|
+
const y = obj.y ?? canvasHeight / 2;
|
|
23
54
|
switch (obj.type) {
|
|
24
55
|
case 'box': {
|
|
25
|
-
const
|
|
56
|
+
const w = obj.width ?? 60;
|
|
57
|
+
const h = obj.height ?? 60;
|
|
58
|
+
const label = obj.label;
|
|
26
59
|
elements.push(rect(x - w / 2, y - h / 2, w, h, {
|
|
27
|
-
fill: '#
|
|
28
|
-
stroke: '#
|
|
60
|
+
fill: '#f3f4f6',
|
|
61
|
+
stroke: '#374151',
|
|
29
62
|
'stroke-width': 2,
|
|
30
63
|
}));
|
|
31
64
|
if (label) {
|
|
@@ -33,17 +66,17 @@ function renderObject(obj) {
|
|
|
33
66
|
'text-anchor': 'middle',
|
|
34
67
|
'dominant-baseline': 'middle',
|
|
35
68
|
'font-size': 14,
|
|
36
|
-
fill: '#
|
|
69
|
+
fill: '#1f2937',
|
|
37
70
|
}));
|
|
38
71
|
}
|
|
39
72
|
break;
|
|
40
73
|
}
|
|
41
74
|
case 'sphere': {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
75
|
+
const radius = obj.radius ?? 30;
|
|
76
|
+
const label = obj.label;
|
|
44
77
|
elements.push(circle(x, y, radius, {
|
|
45
|
-
fill: '#
|
|
46
|
-
stroke: '#
|
|
78
|
+
fill: '#dbeafe',
|
|
79
|
+
stroke: '#1e40af',
|
|
47
80
|
'stroke-width': 2,
|
|
48
81
|
}));
|
|
49
82
|
if (label) {
|
|
@@ -51,22 +84,22 @@ function renderObject(obj) {
|
|
|
51
84
|
'text-anchor': 'middle',
|
|
52
85
|
'dominant-baseline': 'middle',
|
|
53
86
|
'font-size': 12,
|
|
54
|
-
fill: '#
|
|
87
|
+
fill: '#1e3a8a',
|
|
55
88
|
}));
|
|
56
89
|
}
|
|
57
90
|
break;
|
|
58
91
|
}
|
|
59
92
|
case 'point': {
|
|
60
|
-
const
|
|
93
|
+
const label = obj.label;
|
|
61
94
|
elements.push(circle(x, y, 5, {
|
|
62
|
-
fill: '#
|
|
95
|
+
fill: '#1f2937',
|
|
63
96
|
}));
|
|
64
97
|
if (label) {
|
|
65
98
|
elements.push(text(x + 10, y, label, {
|
|
66
99
|
'text-anchor': 'start',
|
|
67
100
|
'dominant-baseline': 'middle',
|
|
68
101
|
'font-size': 12,
|
|
69
|
-
fill: '#
|
|
102
|
+
fill: '#374151',
|
|
70
103
|
}));
|
|
71
104
|
}
|
|
72
105
|
break;
|
|
@@ -74,57 +107,50 @@ function renderObject(obj) {
|
|
|
74
107
|
}
|
|
75
108
|
return svgTag('g', { class: 'object' }, elements.join(''));
|
|
76
109
|
}
|
|
77
|
-
function
|
|
78
|
-
const {
|
|
110
|
+
function renderArrow(arrow, centerX, centerY) {
|
|
111
|
+
const { direction, label, magnitude, color = '#dc2626' } = arrow;
|
|
79
112
|
const elements = [];
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
// Parse magnitude to get value
|
|
114
|
+
const { value: magnitudeValue } = parseMagnitude(magnitude);
|
|
115
|
+
// Calculate arrow length based on magnitude (with minimum and maximum)
|
|
116
|
+
const arrowLength = Math.max(40, Math.min(100, magnitudeValue * 0.8));
|
|
117
|
+
// Convert compass direction to angle
|
|
118
|
+
const angle = compassToAngle(direction);
|
|
119
|
+
// Calculate start and end points
|
|
120
|
+
const start = calculateArrowStart(centerX, centerY, direction, arrowLength);
|
|
121
|
+
const end = calculateArrowEndpoint(centerX, centerY, direction, arrowLength);
|
|
122
|
+
// Draw arrow line
|
|
123
|
+
elements.push(line(start.x, start.y, end.x, end.y, {
|
|
87
124
|
stroke: color,
|
|
88
125
|
'stroke-width': 3,
|
|
89
|
-
'marker-end': `url(#arrow-${color})`,
|
|
126
|
+
'marker-end': `url(#arrow-${color.replace('#', '')})`,
|
|
90
127
|
}));
|
|
91
|
-
// Draw label
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
return svgTag('g', { class: '
|
|
128
|
+
// Draw label with magnitude
|
|
129
|
+
const labelText = label ? `${label} (${magnitude})` : magnitude;
|
|
130
|
+
// Calculate label position (offset from arrow end)
|
|
131
|
+
const angleRad = (angle * Math.PI) / 180;
|
|
132
|
+
const labelOffset = 20;
|
|
133
|
+
const labelX = end.x + labelOffset * Math.cos(angleRad);
|
|
134
|
+
const labelY = end.y + labelOffset * Math.sin(angleRad);
|
|
135
|
+
elements.push(text(labelX, labelY, labelText, {
|
|
136
|
+
'text-anchor': 'start',
|
|
137
|
+
'dominant-baseline': 'middle',
|
|
138
|
+
'font-size': 14,
|
|
139
|
+
'font-weight': '600',
|
|
140
|
+
fill: color,
|
|
141
|
+
}));
|
|
142
|
+
return svgTag('g', { class: 'arrow' }, elements.join(''));
|
|
106
143
|
}
|
|
107
|
-
function
|
|
144
|
+
function createArrowMarkers(colors) {
|
|
145
|
+
const markers = colors.map(color => {
|
|
146
|
+
const colorId = color.replace('#', '');
|
|
147
|
+
return ` <marker id="arrow-${colorId}" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
148
|
+
<path d="M0,0 L10,5 L0,10" fill="${color}" />
|
|
149
|
+
</marker>`;
|
|
150
|
+
}).join('\n');
|
|
108
151
|
return `
|
|
109
152
|
<defs>
|
|
110
|
-
|
|
111
|
-
<path d="M0,0 L10,5 L0,10" fill="#e53e3e" />
|
|
112
|
-
</marker>
|
|
113
|
-
<marker id="arrow-#3182ce" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
114
|
-
<path d="M0,0 L10,5 L0,10" fill="#3182ce" />
|
|
115
|
-
</marker>
|
|
116
|
-
<marker id="arrow-#38a169" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
117
|
-
<path d="M0,0 L10,5 L0,10" fill="#38a169" />
|
|
118
|
-
</marker>
|
|
119
|
-
<marker id="arrow-#d69e2e" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
120
|
-
<path d="M0,0 L10,5 L0,10" fill="#d69e2e" />
|
|
121
|
-
</marker>
|
|
122
|
-
<marker id="arrow-#805ad5" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
123
|
-
<path d="M0,0 L10,5 L0,10" fill="#805ad5" />
|
|
124
|
-
</marker>
|
|
125
|
-
<marker id="arrow-#333" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
126
|
-
<path d="M0,0 L10,5 L0,10" fill="#333" />
|
|
127
|
-
</marker>
|
|
153
|
+
${markers}
|
|
128
154
|
</defs>
|
|
129
155
|
`;
|
|
130
156
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,225 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Base figure DSL structure
|
|
2
|
+
* Base figure DSL structure - generic and flexible
|
|
3
3
|
*/
|
|
4
4
|
export interface FigureDSL {
|
|
5
|
-
type:
|
|
6
|
-
width
|
|
7
|
-
height
|
|
5
|
+
type: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
8
|
title?: string;
|
|
9
9
|
alt?: string;
|
|
10
|
+
[key: string]: any;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
14
|
-
export type FigureType = 'circuit' | 'rayDiagram' | 'forceDiagram' | 'geometry' | 'coordinatePlane' | 'numberLine' | 'graph' | 'venn' | 'bioTable';
|
|
15
|
-
/**
|
|
16
|
-
* Circuit diagram DSL
|
|
13
|
+
* Force diagram DSL (new format)
|
|
17
14
|
*/
|
|
18
|
-
export interface
|
|
19
|
-
type: '
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export interface CircuitComponent {
|
|
23
|
-
type: 'battery' | 'resistor' | 'switch' | 'bulb' | 'wire' | 'ammeter' | 'voltmeter';
|
|
24
|
-
x: number;
|
|
25
|
-
y: number;
|
|
26
|
-
rotation?: number;
|
|
27
|
-
label?: string;
|
|
28
|
-
value?: string;
|
|
15
|
+
export interface ForceDiagramDSL extends FigureDSL {
|
|
16
|
+
type: 'force_diagram';
|
|
17
|
+
arrows: Arrow[];
|
|
18
|
+
object?: PhysicalObject;
|
|
29
19
|
}
|
|
30
20
|
/**
|
|
31
|
-
*
|
|
21
|
+
* Arrow configuration with compass directions
|
|
32
22
|
*/
|
|
33
|
-
export interface
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export interface OpticalElement {
|
|
39
|
-
type: 'lens' | 'mirror' | 'prism' | 'screen';
|
|
40
|
-
x: number;
|
|
41
|
-
y: number;
|
|
42
|
-
width?: number;
|
|
43
|
-
height?: number;
|
|
44
|
-
focalLength?: number;
|
|
45
|
-
curvature?: string;
|
|
46
|
-
}
|
|
47
|
-
export interface Ray {
|
|
48
|
-
startX: number;
|
|
49
|
-
startY: number;
|
|
50
|
-
angle: number;
|
|
23
|
+
export interface Arrow {
|
|
24
|
+
direction: 'up' | 'down' | 'left' | 'right' | 'up-left' | 'up-right' | 'down-left' | 'down-right';
|
|
25
|
+
label: string;
|
|
26
|
+
magnitude: string;
|
|
27
|
+
unit?: string;
|
|
51
28
|
color?: string;
|
|
52
|
-
label?: string;
|
|
53
29
|
}
|
|
54
30
|
/**
|
|
55
|
-
*
|
|
31
|
+
* Physical object definition
|
|
56
32
|
*/
|
|
57
|
-
export interface ForceDiagramDSL extends FigureDSL {
|
|
58
|
-
type: 'forceDiagram';
|
|
59
|
-
object: PhysicalObject;
|
|
60
|
-
forces: Force[];
|
|
61
|
-
}
|
|
62
33
|
export interface PhysicalObject {
|
|
63
34
|
type: 'box' | 'sphere' | 'point';
|
|
64
|
-
x
|
|
65
|
-
y
|
|
35
|
+
x?: number;
|
|
36
|
+
y?: number;
|
|
66
37
|
width?: number;
|
|
67
38
|
height?: number;
|
|
39
|
+
radius?: number;
|
|
68
40
|
label?: string;
|
|
69
41
|
}
|
|
70
|
-
export interface Force {
|
|
71
|
-
originX: number;
|
|
72
|
-
originY: number;
|
|
73
|
-
magnitude: number;
|
|
74
|
-
angle: number;
|
|
75
|
-
label: string;
|
|
76
|
-
color?: string;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Geometry DSL
|
|
80
|
-
*/
|
|
81
|
-
export interface GeometryDSL extends FigureDSL {
|
|
82
|
-
type: 'geometry';
|
|
83
|
-
shapes: GeometryShape[];
|
|
84
|
-
}
|
|
85
|
-
export type GeometryShape = {
|
|
86
|
-
type: 'line';
|
|
87
|
-
x1: number;
|
|
88
|
-
y1: number;
|
|
89
|
-
x2: number;
|
|
90
|
-
y2: number;
|
|
91
|
-
label?: string;
|
|
92
|
-
} | {
|
|
93
|
-
type: 'circle';
|
|
94
|
-
cx: number;
|
|
95
|
-
cy: number;
|
|
96
|
-
r: number;
|
|
97
|
-
label?: string;
|
|
98
|
-
} | {
|
|
99
|
-
type: 'rect';
|
|
100
|
-
x: number;
|
|
101
|
-
y: number;
|
|
102
|
-
width: number;
|
|
103
|
-
height: number;
|
|
104
|
-
label?: string;
|
|
105
|
-
} | {
|
|
106
|
-
type: 'triangle';
|
|
107
|
-
points: [number, number][];
|
|
108
|
-
label?: string;
|
|
109
|
-
} | {
|
|
110
|
-
type: 'angle';
|
|
111
|
-
vertex: [number, number];
|
|
112
|
-
rays: [[number, number], [number, number]];
|
|
113
|
-
label?: string;
|
|
114
|
-
};
|
|
115
|
-
/**
|
|
116
|
-
* Coordinate plane DSL
|
|
117
|
-
*/
|
|
118
|
-
export interface CoordinatePlaneDSL extends FigureDSL {
|
|
119
|
-
type: 'coordinatePlane';
|
|
120
|
-
xRange: [number, number];
|
|
121
|
-
yRange: [number, number];
|
|
122
|
-
showGrid?: boolean;
|
|
123
|
-
showLabels?: boolean;
|
|
124
|
-
elements: PlaneElement[];
|
|
125
|
-
}
|
|
126
|
-
export interface PlaneElement {
|
|
127
|
-
type: 'point' | 'line' | 'curve' | 'label';
|
|
128
|
-
data: PointElementData | LineElementData | CurveElementData | LabelElementData;
|
|
129
|
-
}
|
|
130
|
-
export interface PointElementData {
|
|
131
|
-
x: number;
|
|
132
|
-
y: number;
|
|
133
|
-
label?: string;
|
|
134
|
-
color?: string;
|
|
135
|
-
}
|
|
136
|
-
export interface LineElementData {
|
|
137
|
-
x1: number;
|
|
138
|
-
y1: number;
|
|
139
|
-
x2: number;
|
|
140
|
-
y2: number;
|
|
141
|
-
color?: string;
|
|
142
|
-
style?: 'solid' | 'dashed' | 'dotted';
|
|
143
|
-
}
|
|
144
|
-
export interface CurveElementData {
|
|
145
|
-
points: [number, number][];
|
|
146
|
-
color?: string;
|
|
147
|
-
style?: 'solid' | 'dashed' | 'dotted';
|
|
148
|
-
}
|
|
149
|
-
export interface LabelElementData {
|
|
150
|
-
x: number;
|
|
151
|
-
y: number;
|
|
152
|
-
text: string;
|
|
153
|
-
align?: 'left' | 'center' | 'right';
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Number line DSL
|
|
157
|
-
*/
|
|
158
|
-
export interface NumberLineDSL extends FigureDSL {
|
|
159
|
-
type: 'numberLine';
|
|
160
|
-
range: [number, number];
|
|
161
|
-
ticks: number[];
|
|
162
|
-
marks: NumberLineMark[];
|
|
163
|
-
arrowEnds?: 'left' | 'right' | 'both' | 'none';
|
|
164
|
-
}
|
|
165
|
-
export interface NumberLineMark {
|
|
166
|
-
value: number;
|
|
167
|
-
label: string;
|
|
168
|
-
style?: 'open' | 'closed' | 'arrow';
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Graph DSL
|
|
172
|
-
*/
|
|
173
|
-
export interface GraphDSL extends FigureDSL {
|
|
174
|
-
type: 'graph';
|
|
175
|
-
xRange: [number, number];
|
|
176
|
-
yRange: [number, number];
|
|
177
|
-
functions: GraphFunction[];
|
|
178
|
-
}
|
|
179
|
-
export interface GraphFunction {
|
|
180
|
-
equation: string;
|
|
181
|
-
color: string;
|
|
182
|
-
style?: 'solid' | 'dashed' | 'dotted';
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Venn diagram DSL
|
|
186
|
-
*/
|
|
187
|
-
export interface VennDSL extends FigureDSL {
|
|
188
|
-
type: 'venn';
|
|
189
|
-
circles: VennCircle[];
|
|
190
|
-
labels: VennLabel[];
|
|
191
|
-
}
|
|
192
|
-
export interface VennCircle {
|
|
193
|
-
label: string;
|
|
194
|
-
cx: number;
|
|
195
|
-
cy: number;
|
|
196
|
-
r: number;
|
|
197
|
-
color: string;
|
|
198
|
-
}
|
|
199
|
-
export interface VennLabel {
|
|
200
|
-
text: string;
|
|
201
|
-
x: number;
|
|
202
|
-
y: number;
|
|
203
|
-
region: string;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Biology table DSL
|
|
207
|
-
*/
|
|
208
|
-
export interface BioTableDSL extends FigureDSL {
|
|
209
|
-
type: 'bioTable';
|
|
210
|
-
columns: BioColumn[];
|
|
211
|
-
rows: BioRow[];
|
|
212
|
-
}
|
|
213
|
-
export interface BioColumn {
|
|
214
|
-
header: string;
|
|
215
|
-
width: number;
|
|
216
|
-
}
|
|
217
|
-
export interface BioRow {
|
|
218
|
-
cells: (string | BioCellImage)[];
|
|
219
|
-
}
|
|
220
|
-
export interface BioCellImage {
|
|
221
|
-
type: 'image';
|
|
222
|
-
src: string;
|
|
223
|
-
alt: string;
|
|
224
|
-
}
|
|
225
42
|
//# sourceMappingURL=types.d.ts.map
|
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,
|
|
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;IAClB,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/types.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compass directions supported
|
|
3
|
+
*/
|
|
4
|
+
export type CompassDirection = 'up' | 'down' | 'left' | 'right' | 'up-left' | 'up-right' | 'down-left' | 'down-right';
|
|
5
|
+
/**
|
|
6
|
+
* Convert compass direction to angle in degrees
|
|
7
|
+
*/
|
|
8
|
+
export declare function compassToAngle(direction: CompassDirection): number;
|
|
9
|
+
/**
|
|
10
|
+
* Convert angle in degrees to compass direction
|
|
11
|
+
*/
|
|
12
|
+
export declare function angleToCompass(angle: number): CompassDirection;
|
|
13
|
+
/**
|
|
14
|
+
* Parse magnitude string to extract numeric value and unit
|
|
15
|
+
* Examples: "100 N" -> { value: 100, unit: "N" }
|
|
16
|
+
* "50 kg" -> { value: 50, unit: "kg" }
|
|
17
|
+
* "25" -> { value: 25, unit: "" }
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseMagnitude(magnitude: string): {
|
|
20
|
+
value: number;
|
|
21
|
+
unit: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Calculate arrow endpoint based on center, direction, and length
|
|
25
|
+
*/
|
|
26
|
+
export declare function calculateArrowEndpoint(centerX: number, centerY: number, direction: CompassDirection, length: number): {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Calculate arrow start point (opposite of endpoint)
|
|
32
|
+
*/
|
|
33
|
+
export declare function calculateArrowStart(centerX: number, centerY: number, direction: CompassDirection, length: number): {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=compass.d.ts.map
|
|
@@ -0,0 +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;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAoBjF;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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Compass direction utilities for force diagram rendering
|
|
2
|
+
/**
|
|
3
|
+
* Convert compass direction to angle in degrees
|
|
4
|
+
*/
|
|
5
|
+
export function compassToAngle(direction) {
|
|
6
|
+
const mapping = {
|
|
7
|
+
'up': -90,
|
|
8
|
+
'down': 90,
|
|
9
|
+
'left': 180,
|
|
10
|
+
'right': 0,
|
|
11
|
+
'up-left': -135,
|
|
12
|
+
'up-right': -45,
|
|
13
|
+
'down-left': 135,
|
|
14
|
+
'down-right': 45
|
|
15
|
+
};
|
|
16
|
+
return mapping[direction] ?? 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Convert angle in degrees to compass direction
|
|
20
|
+
*/
|
|
21
|
+
export function angleToCompass(angle) {
|
|
22
|
+
const normalizedAngle = ((angle % 360) + 360) % 360;
|
|
23
|
+
if (normalizedAngle === 0 || normalizedAngle === 360)
|
|
24
|
+
return 'right';
|
|
25
|
+
if (normalizedAngle === 90)
|
|
26
|
+
return 'down';
|
|
27
|
+
if (normalizedAngle === 180)
|
|
28
|
+
return 'left';
|
|
29
|
+
if (normalizedAngle === 270)
|
|
30
|
+
return 'up';
|
|
31
|
+
if (normalizedAngle > 270 && normalizedAngle < 360)
|
|
32
|
+
return 'up-right';
|
|
33
|
+
if (normalizedAngle > 180 && normalizedAngle < 270)
|
|
34
|
+
return 'up-left';
|
|
35
|
+
if (normalizedAngle > 90 && normalizedAngle < 180)
|
|
36
|
+
return 'down-left';
|
|
37
|
+
if (normalizedAngle > 0 && normalizedAngle < 90)
|
|
38
|
+
return 'down-right';
|
|
39
|
+
return 'right'; // Default
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse magnitude string to extract numeric value and unit
|
|
43
|
+
* Examples: "100 N" -> { value: 100, unit: "N" }
|
|
44
|
+
* "50 kg" -> { value: 50, unit: "kg" }
|
|
45
|
+
* "25" -> { value: 25, unit: "" }
|
|
46
|
+
*/
|
|
47
|
+
export function parseMagnitude(magnitude) {
|
|
48
|
+
const trimmed = magnitude.trim();
|
|
49
|
+
// Try to extract numeric value and unit
|
|
50
|
+
const match = trimmed.match(/^(-?\d+\.?\d*)\s*(.*)$/);
|
|
51
|
+
if (match) {
|
|
52
|
+
return {
|
|
53
|
+
value: parseFloat(match[1]),
|
|
54
|
+
unit: match[2].trim()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// If no unit, just parse the number
|
|
58
|
+
const value = parseFloat(trimmed);
|
|
59
|
+
if (!isNaN(value)) {
|
|
60
|
+
return { value, unit: '' };
|
|
61
|
+
}
|
|
62
|
+
return { value: 0, unit: '' };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Calculate arrow endpoint based on center, direction, and length
|
|
66
|
+
*/
|
|
67
|
+
export function calculateArrowEndpoint(centerX, centerY, direction, length) {
|
|
68
|
+
const angle = compassToAngle(direction);
|
|
69
|
+
const radians = (angle * Math.PI) / 180;
|
|
70
|
+
return {
|
|
71
|
+
x: centerX + length * Math.cos(radians),
|
|
72
|
+
y: centerY + length * Math.sin(radians)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calculate arrow start point (opposite of endpoint)
|
|
77
|
+
*/
|
|
78
|
+
export function calculateArrowStart(centerX, centerY, direction, length) {
|
|
79
|
+
const angle = compassToAngle(direction);
|
|
80
|
+
const radians = (angle * Math.PI) / 180;
|
|
81
|
+
return {
|
|
82
|
+
x: centerX - length * Math.cos(radians),
|
|
83
|
+
y: centerY - length * Math.sin(radians)
|
|
84
|
+
};
|
|
85
|
+
}
|
package/package.json
CHANGED