@holoscript/plugin-civil-engineering 2.0.1 → 2.0.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/package.json +3 -3
- package/src/__tests__/frame2d.test.ts +118 -34
- package/src/__tests__/runtime-integration.test.ts +27 -4
- package/src/frame2d.ts +71 -54
- package/src/index.ts +26 -5
- package/src/runtime.ts +2 -2
- package/src/traits/LoadBearingTrait.ts +41 -8
- package/src/traits/MaterialFatigueTrait.ts +50 -10
- package/src/traits/StructuralAnalysisTrait.ts +46 -9
- package/src/traits/types.ts +22 -4
- package/tsconfig.json +5 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-civil-engineering",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@holoscript/core": "8.0.
|
|
6
|
+
"@holoscript/core": ">=8.0.0"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "vitest run --passWithNoTests",
|
|
11
11
|
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
12
12
|
}
|
|
13
|
-
}
|
|
13
|
+
}
|
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
} from '../frame2d';
|
|
19
19
|
|
|
20
20
|
// ─── Steel W-section defaults used across tests ────────────────────────────────
|
|
21
|
-
const E_GPa = 200;
|
|
22
|
-
const I_m4
|
|
23
|
-
const A_m2
|
|
21
|
+
const E_GPa = 200; // GPa (structural steel)
|
|
22
|
+
const I_m4 = 1e-4; // m⁴ (moderate W-section)
|
|
23
|
+
const A_m2 = 5e-3; // m² (moderate W-section area)
|
|
24
24
|
|
|
25
25
|
// ─── Simply-supported beam ─────────────────────────────────────────────────────
|
|
26
26
|
|
|
@@ -47,15 +47,29 @@ describe('simply-supported beam under point load', () => {
|
|
|
47
47
|
{ id: 'B', x: 6, y: 0 },
|
|
48
48
|
],
|
|
49
49
|
elements: [
|
|
50
|
-
{
|
|
51
|
-
|
|
50
|
+
{
|
|
51
|
+
id: 'e1',
|
|
52
|
+
fromNodeId: 'A',
|
|
53
|
+
toNodeId: 'M',
|
|
54
|
+
elasticModulusGPa: E_GPa,
|
|
55
|
+
momentOfInertiaM4: I_m4,
|
|
56
|
+
areaM2: A_m2,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'e2',
|
|
60
|
+
fromNodeId: 'M',
|
|
61
|
+
toNodeId: 'B',
|
|
62
|
+
elasticModulusGPa: E_GPa,
|
|
63
|
+
momentOfInertiaM4: I_m4,
|
|
64
|
+
areaM2: A_m2,
|
|
65
|
+
},
|
|
52
66
|
],
|
|
53
67
|
supports: [
|
|
54
|
-
{ nodeId: 'A', ux: true, uy: true },
|
|
55
|
-
{ nodeId: 'B', uy: true },
|
|
68
|
+
{ nodeId: 'A', ux: true, uy: true }, // pin
|
|
69
|
+
{ nodeId: 'B', uy: true }, // roller
|
|
56
70
|
],
|
|
57
71
|
nodalLoads: [
|
|
58
|
-
{ nodeId: 'M', Fy: -P },
|
|
72
|
+
{ nodeId: 'M', Fy: -P }, // downward
|
|
59
73
|
],
|
|
60
74
|
};
|
|
61
75
|
|
|
@@ -107,14 +121,19 @@ describe('cantilever beam under tip load', () => {
|
|
|
107
121
|
{ id: 'B', x: L, y: 0 },
|
|
108
122
|
],
|
|
109
123
|
elements: [
|
|
110
|
-
{
|
|
124
|
+
{
|
|
125
|
+
id: 'e1',
|
|
126
|
+
fromNodeId: 'A',
|
|
127
|
+
toNodeId: 'B',
|
|
128
|
+
elasticModulusGPa: E_GPa,
|
|
129
|
+
momentOfInertiaM4: I_m4,
|
|
130
|
+
areaM2: A_m2,
|
|
131
|
+
},
|
|
111
132
|
],
|
|
112
133
|
supports: [
|
|
113
134
|
{ nodeId: 'A', ux: true, uy: true, theta: true }, // fixed
|
|
114
135
|
],
|
|
115
|
-
nodalLoads: [
|
|
116
|
-
{ nodeId: 'B', Fy: -P },
|
|
117
|
-
],
|
|
136
|
+
nodalLoads: [{ nodeId: 'B', Fy: -P }],
|
|
118
137
|
};
|
|
119
138
|
|
|
120
139
|
it('tip deflection matches closed-form PL³/(3EI) within 1%', () => {
|
|
@@ -158,14 +177,17 @@ describe('inclined element — coordinate transformation', () => {
|
|
|
158
177
|
{ id: 'B', x: 3, y: 4 },
|
|
159
178
|
],
|
|
160
179
|
elements: [
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
{
|
|
181
|
+
id: 'e1',
|
|
182
|
+
fromNodeId: 'A',
|
|
183
|
+
toNodeId: 'B',
|
|
184
|
+
elasticModulusGPa: E_GPa,
|
|
185
|
+
momentOfInertiaM4: I_m4,
|
|
186
|
+
areaM2: A_m2,
|
|
187
|
+
},
|
|
168
188
|
],
|
|
189
|
+
supports: [{ nodeId: 'A', ux: true, uy: true, theta: true }],
|
|
190
|
+
nodalLoads: [{ nodeId: 'B', Fx: 100 }],
|
|
169
191
|
};
|
|
170
192
|
|
|
171
193
|
it('converges for inclined element', () => {
|
|
@@ -203,8 +225,22 @@ describe('simply-supported beam under UDL', () => {
|
|
|
203
225
|
{ id: 'B', x: 6, y: 0 },
|
|
204
226
|
],
|
|
205
227
|
elements: [
|
|
206
|
-
{
|
|
207
|
-
|
|
228
|
+
{
|
|
229
|
+
id: 'e1',
|
|
230
|
+
fromNodeId: 'A',
|
|
231
|
+
toNodeId: 'M',
|
|
232
|
+
elasticModulusGPa: E_GPa,
|
|
233
|
+
momentOfInertiaM4: I_m4,
|
|
234
|
+
areaM2: A_m2,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'e2',
|
|
238
|
+
fromNodeId: 'M',
|
|
239
|
+
toNodeId: 'B',
|
|
240
|
+
elasticModulusGPa: E_GPa,
|
|
241
|
+
momentOfInertiaM4: I_m4,
|
|
242
|
+
areaM2: A_m2,
|
|
243
|
+
},
|
|
208
244
|
],
|
|
209
245
|
supports: [
|
|
210
246
|
{ nodeId: 'A', ux: true, uy: true },
|
|
@@ -240,8 +276,20 @@ describe('validateFrame2DModel', () => {
|
|
|
240
276
|
it('returns valid for a well-formed model', () => {
|
|
241
277
|
const model: Frame2DModel = {
|
|
242
278
|
id: 'valid',
|
|
243
|
-
nodes: [
|
|
244
|
-
|
|
279
|
+
nodes: [
|
|
280
|
+
{ id: 'A', x: 0, y: 0 },
|
|
281
|
+
{ id: 'B', x: 5, y: 0 },
|
|
282
|
+
],
|
|
283
|
+
elements: [
|
|
284
|
+
{
|
|
285
|
+
id: 'e1',
|
|
286
|
+
fromNodeId: 'A',
|
|
287
|
+
toNodeId: 'B',
|
|
288
|
+
elasticModulusGPa: 200,
|
|
289
|
+
momentOfInertiaM4: 1e-4,
|
|
290
|
+
areaM2: 5e-3,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
245
293
|
supports: [{ nodeId: 'A', ux: true, uy: true, theta: true }],
|
|
246
294
|
};
|
|
247
295
|
const v = validateFrame2DModel(model);
|
|
@@ -253,7 +301,16 @@ describe('validateFrame2DModel', () => {
|
|
|
253
301
|
const model: Frame2DModel = {
|
|
254
302
|
id: 'bad-elem',
|
|
255
303
|
nodes: [{ id: 'A', x: 0, y: 0 }],
|
|
256
|
-
elements: [
|
|
304
|
+
elements: [
|
|
305
|
+
{
|
|
306
|
+
id: 'e1',
|
|
307
|
+
fromNodeId: 'A',
|
|
308
|
+
toNodeId: 'MISSING',
|
|
309
|
+
elasticModulusGPa: 200,
|
|
310
|
+
momentOfInertiaM4: 1e-4,
|
|
311
|
+
areaM2: 5e-3,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
257
314
|
supports: [{ nodeId: 'A', ux: true, uy: true, theta: true }],
|
|
258
315
|
};
|
|
259
316
|
const v = validateFrame2DModel(model);
|
|
@@ -264,8 +321,20 @@ describe('validateFrame2DModel', () => {
|
|
|
264
321
|
it('errors when fewer than 3 DOF are restrained', () => {
|
|
265
322
|
const model: Frame2DModel = {
|
|
266
323
|
id: 'unstable',
|
|
267
|
-
nodes: [
|
|
268
|
-
|
|
324
|
+
nodes: [
|
|
325
|
+
{ id: 'A', x: 0, y: 0 },
|
|
326
|
+
{ id: 'B', x: 5, y: 0 },
|
|
327
|
+
],
|
|
328
|
+
elements: [
|
|
329
|
+
{
|
|
330
|
+
id: 'e1',
|
|
331
|
+
fromNodeId: 'A',
|
|
332
|
+
toNodeId: 'B',
|
|
333
|
+
elasticModulusGPa: 200,
|
|
334
|
+
momentOfInertiaM4: 1e-4,
|
|
335
|
+
areaM2: 5e-3,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
269
338
|
supports: [{ nodeId: 'A', ux: true }], // only 1 DOF
|
|
270
339
|
};
|
|
271
340
|
const v = validateFrame2DModel(model);
|
|
@@ -276,8 +345,20 @@ describe('validateFrame2DModel', () => {
|
|
|
276
345
|
it('solver throws for invalid model', () => {
|
|
277
346
|
const model: Frame2DModel = {
|
|
278
347
|
id: 'invalid',
|
|
279
|
-
nodes: [
|
|
280
|
-
|
|
348
|
+
nodes: [
|
|
349
|
+
{ id: 'A', x: 0, y: 0 },
|
|
350
|
+
{ id: 'B', x: 5, y: 0 },
|
|
351
|
+
],
|
|
352
|
+
elements: [
|
|
353
|
+
{
|
|
354
|
+
id: 'e1',
|
|
355
|
+
fromNodeId: 'A',
|
|
356
|
+
toNodeId: 'B',
|
|
357
|
+
elasticModulusGPa: 200,
|
|
358
|
+
momentOfInertiaM4: 1e-4,
|
|
359
|
+
areaM2: 5e-3,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
281
362
|
supports: [], // no supports
|
|
282
363
|
};
|
|
283
364
|
expect(() => solveFrame2D(model)).toThrow();
|
|
@@ -295,14 +376,17 @@ describe('buildFrame2DReceipt', () => {
|
|
|
295
376
|
],
|
|
296
377
|
elements: [
|
|
297
378
|
{
|
|
298
|
-
id: 'e1',
|
|
299
|
-
|
|
300
|
-
|
|
379
|
+
id: 'e1',
|
|
380
|
+
fromNodeId: 'A',
|
|
381
|
+
toNodeId: 'B',
|
|
382
|
+
elasticModulusGPa: E_GPa,
|
|
383
|
+
momentOfInertiaM4: I_m4,
|
|
384
|
+
areaM2: A_m2,
|
|
385
|
+
plasticModulusM3: 5e-4,
|
|
386
|
+
yieldStrengthMPa: 250,
|
|
301
387
|
},
|
|
302
388
|
],
|
|
303
|
-
supports: [
|
|
304
|
-
{ nodeId: 'A', ux: true, uy: true, theta: true },
|
|
305
|
-
],
|
|
389
|
+
supports: [{ nodeId: 'A', ux: true, uy: true, theta: true }],
|
|
306
390
|
nodalLoads: [{ nodeId: 'B', Fy: -10 }],
|
|
307
391
|
};
|
|
308
392
|
|
|
@@ -59,7 +59,14 @@ const CANTILEVER: Frame2DModel = {
|
|
|
59
59
|
{ id: 'B', x: L, y: 0 },
|
|
60
60
|
],
|
|
61
61
|
elements: [
|
|
62
|
-
{
|
|
62
|
+
{
|
|
63
|
+
id: 'e1',
|
|
64
|
+
fromNodeId: 'A',
|
|
65
|
+
toNodeId: 'B',
|
|
66
|
+
elasticModulusGPa: E_GPa,
|
|
67
|
+
momentOfInertiaM4: I_m4,
|
|
68
|
+
areaM2: A_m2,
|
|
69
|
+
},
|
|
63
70
|
],
|
|
64
71
|
supports: [
|
|
65
72
|
{ nodeId: 'A', ux: true, uy: true, theta: true }, // fixed
|
|
@@ -77,7 +84,14 @@ const UNDER_CONSTRAINED: Frame2DModel = {
|
|
|
77
84
|
{ id: 'B', x: L, y: 0 },
|
|
78
85
|
],
|
|
79
86
|
elements: [
|
|
80
|
-
{
|
|
87
|
+
{
|
|
88
|
+
id: 'e1',
|
|
89
|
+
fromNodeId: 'A',
|
|
90
|
+
toNodeId: 'B',
|
|
91
|
+
elasticModulusGPa: E_GPa,
|
|
92
|
+
momentOfInertiaM4: I_m4,
|
|
93
|
+
areaM2: A_m2,
|
|
94
|
+
},
|
|
81
95
|
],
|
|
82
96
|
supports: [{ nodeId: 'A', ux: true }], // only 1 DOF restrained
|
|
83
97
|
nodalLoads: [{ nodeId: 'B', Fy: -P }],
|
|
@@ -119,7 +133,12 @@ describe('civil-engineering -> HoloScript runtime integration (dsm_frame_2d)', (
|
|
|
119
133
|
|
|
120
134
|
// Hand-checked against global static equilibrium of the cantilever:
|
|
121
135
|
// Ry_A = +P = +50 kN ; |Mz_A| = P·L = 200 kN·m ; Rx_A = 0.
|
|
122
|
-
const reactions = summary.reactions as Array<{
|
|
136
|
+
const reactions = summary.reactions as Array<{
|
|
137
|
+
nodeId: string;
|
|
138
|
+
Rx: number;
|
|
139
|
+
Ry: number;
|
|
140
|
+
Mz: number;
|
|
141
|
+
}>;
|
|
123
142
|
const rA = reactions.find((r) => r.nodeId === 'A')!;
|
|
124
143
|
expect(rA.Rx).toBeCloseTo(0, 1);
|
|
125
144
|
expect(rA.Ry).toBeCloseTo(EXPECTED_RY, 1); // +50 kN
|
|
@@ -165,7 +184,11 @@ describe('civil-engineering -> HoloScript runtime integration (dsm_frame_2d)', (
|
|
|
165
184
|
|
|
166
185
|
const state = runtime.getState() as Record<string, unknown>;
|
|
167
186
|
const persisted = state['dsm_frame_2d:frame'] as
|
|
168
|
-
| {
|
|
187
|
+
| {
|
|
188
|
+
converged?: boolean;
|
|
189
|
+
nodeCount?: number;
|
|
190
|
+
reactions?: Array<{ nodeId: string; Ry: number }>;
|
|
191
|
+
}
|
|
169
192
|
| undefined;
|
|
170
193
|
expect(persisted).toBeDefined();
|
|
171
194
|
expect(persisted?.converged).toBe(true);
|
package/src/frame2d.ts
CHANGED
|
@@ -241,10 +241,12 @@ export function validateFrame2DModel(model: Frame2DModel): Frame2DValidation {
|
|
|
241
241
|
// Minimum stability check: need enough supports to prevent rigid body motion
|
|
242
242
|
const totalRestrainedDOF = model.supports.reduce(
|
|
243
243
|
(sum, s) => sum + (s.ux ? 1 : 0) + (s.uy ? 1 : 0) + (s.theta ? 1 : 0),
|
|
244
|
-
0
|
|
244
|
+
0
|
|
245
245
|
);
|
|
246
246
|
if (totalRestrainedDOF < 3) {
|
|
247
|
-
errors.push(
|
|
247
|
+
errors.push(
|
|
248
|
+
'insufficient supports: need at least 3 restrained DOF to prevent rigid body motion'
|
|
249
|
+
);
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
return { valid: errors.length === 0, errors, warnings };
|
|
@@ -264,9 +266,7 @@ function matMul(A: Matrix, B: Matrix): Matrix {
|
|
|
264
266
|
const k = B.length;
|
|
265
267
|
const C = zeros(m, n);
|
|
266
268
|
for (let i = 0; i < m; i++)
|
|
267
|
-
for (let j = 0; j < n; j++)
|
|
268
|
-
for (let l = 0; l < k; l++)
|
|
269
|
-
C[i][j] += A[i][l] * B[l][j];
|
|
269
|
+
for (let j = 0; j < n; j++) for (let l = 0; l < k; l++) C[i][j] += A[i][l] * B[l][j];
|
|
270
270
|
return C;
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -274,9 +274,7 @@ function transpose(A: Matrix): Matrix {
|
|
|
274
274
|
const m = A.length;
|
|
275
275
|
const n = A[0].length;
|
|
276
276
|
const T = zeros(n, m);
|
|
277
|
-
for (let i = 0; i < m; i++)
|
|
278
|
-
for (let j = 0; j < n; j++)
|
|
279
|
-
T[j][i] = A[i][j];
|
|
277
|
+
for (let i = 0; i < m; i++) for (let j = 0; j < n; j++) T[j][i] = A[i][j];
|
|
280
278
|
return T;
|
|
281
279
|
}
|
|
282
280
|
|
|
@@ -292,9 +290,13 @@ function gaussSolve(A: Matrix, b: number[]): number[] {
|
|
|
292
290
|
let bestAbs = Math.abs(a[pivot][pivot]);
|
|
293
291
|
for (let row = pivot + 1; row < n; row++) {
|
|
294
292
|
const v = Math.abs(a[row][pivot]);
|
|
295
|
-
if (v > bestAbs) {
|
|
293
|
+
if (v > bestAbs) {
|
|
294
|
+
bestAbs = v;
|
|
295
|
+
bestRow = row;
|
|
296
|
+
}
|
|
296
297
|
}
|
|
297
|
-
if (bestAbs < 1e-14)
|
|
298
|
+
if (bestAbs < 1e-14)
|
|
299
|
+
throw new Error('[frame2d] singular stiffness matrix — check boundary conditions');
|
|
298
300
|
if (bestRow !== pivot) {
|
|
299
301
|
[a[pivot], a[bestRow]] = [a[bestRow], a[pivot]];
|
|
300
302
|
[rhs[pivot], rhs[bestRow]] = [rhs[bestRow], rhs[pivot]];
|
|
@@ -333,12 +335,12 @@ function localStiffness(E: number, I: number, A: number, L: number): Matrix {
|
|
|
333
335
|
const L3 = L * L * L;
|
|
334
336
|
|
|
335
337
|
return [
|
|
336
|
-
[
|
|
337
|
-
[
|
|
338
|
-
[
|
|
339
|
-
[-EA/L,
|
|
340
|
-
[
|
|
341
|
-
[
|
|
338
|
+
[EA / L, 0, 0, -EA / L, 0, 0],
|
|
339
|
+
[0, (12 * EI) / L3, (6 * EI) / L2, 0, (-12 * EI) / L3, (6 * EI) / L2],
|
|
340
|
+
[0, (6 * EI) / L2, (4 * EI) / L, 0, (-6 * EI) / L2, (2 * EI) / L],
|
|
341
|
+
[-EA / L, 0, 0, EA / L, 0, 0],
|
|
342
|
+
[0, (-12 * EI) / L3, (-6 * EI) / L2, 0, (12 * EI) / L3, (-6 * EI) / L2],
|
|
343
|
+
[0, (6 * EI) / L2, (2 * EI) / L, 0, (-6 * EI) / L2, (4 * EI) / L],
|
|
342
344
|
];
|
|
343
345
|
}
|
|
344
346
|
|
|
@@ -349,12 +351,12 @@ function localStiffness(E: number, I: number, A: number, L: number): Matrix {
|
|
|
349
351
|
*/
|
|
350
352
|
function transformMatrix(c: number, s: number): Matrix {
|
|
351
353
|
return [
|
|
352
|
-
[
|
|
353
|
-
[-s,
|
|
354
|
-
[
|
|
355
|
-
[
|
|
356
|
-
[
|
|
357
|
-
[
|
|
354
|
+
[c, s, 0, 0, 0, 0],
|
|
355
|
+
[-s, c, 0, 0, 0, 0],
|
|
356
|
+
[0, 0, 1, 0, 0, 0],
|
|
357
|
+
[0, 0, 0, c, s, 0],
|
|
358
|
+
[0, 0, 0, -s, c, 0],
|
|
359
|
+
[0, 0, 0, 0, 0, 1],
|
|
358
360
|
];
|
|
359
361
|
}
|
|
360
362
|
|
|
@@ -389,7 +391,7 @@ function equivalentNodalForces(
|
|
|
389
391
|
dl: DistributedLoad,
|
|
390
392
|
elem: BeamElement,
|
|
391
393
|
fromNode: Node2D,
|
|
392
|
-
toNode: Node2D
|
|
394
|
+
toNode: Node2D
|
|
393
395
|
): number[] /* 6-element global force vector */ {
|
|
394
396
|
const dx = toNode.x - fromNode.x;
|
|
395
397
|
const dy = toNode.y - fromNode.y;
|
|
@@ -429,14 +431,12 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
429
431
|
const K = zeros(nDOF, nDOF);
|
|
430
432
|
for (const elem of model.elements) {
|
|
431
433
|
const fromNode = nodeById.get(elem.fromNodeId)!;
|
|
432
|
-
const toNode
|
|
434
|
+
const toNode = nodeById.get(elem.toNodeId)!;
|
|
433
435
|
const Ke = globalElementStiffness(elem, fromNode, toNode);
|
|
434
436
|
const i0 = nodeIndex.get(elem.fromNodeId)! * 3;
|
|
435
437
|
const i1 = nodeIndex.get(elem.toNodeId)! * 3;
|
|
436
|
-
const dofs = [i0, i0+1, i0+2, i1, i1+1, i1+2];
|
|
437
|
-
for (let r = 0; r < 6; r++)
|
|
438
|
-
for (let c_ = 0; c_ < 6; c_++)
|
|
439
|
-
K[dofs[r]][dofs[c_]] += Ke[r][c_];
|
|
438
|
+
const dofs = [i0, i0 + 1, i0 + 2, i1, i1 + 1, i1 + 2];
|
|
439
|
+
for (let r = 0; r < 6; r++) for (let c_ = 0; c_ < 6; c_++) K[dofs[r]][dofs[c_]] += Ke[r][c_];
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
// Assemble global force vector
|
|
@@ -444,7 +444,7 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
444
444
|
|
|
445
445
|
for (const load of model.nodalLoads ?? []) {
|
|
446
446
|
const idx = nodeIndex.get(load.nodeId)! * 3;
|
|
447
|
-
F[idx]
|
|
447
|
+
F[idx] += load.Fx ?? 0;
|
|
448
448
|
F[idx + 1] += load.Fy ?? 0;
|
|
449
449
|
F[idx + 2] += load.Mz ?? 0;
|
|
450
450
|
}
|
|
@@ -452,11 +452,11 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
452
452
|
for (const dl of model.distributedLoads ?? []) {
|
|
453
453
|
const elem = elemById.get(dl.elementId)!;
|
|
454
454
|
const fromNode = nodeById.get(elem.fromNodeId)!;
|
|
455
|
-
const toNode
|
|
455
|
+
const toNode = nodeById.get(elem.toNodeId)!;
|
|
456
456
|
const f_global = equivalentNodalForces(dl, elem, fromNode, toNode);
|
|
457
457
|
const i0 = nodeIndex.get(elem.fromNodeId)! * 3;
|
|
458
458
|
const i1 = nodeIndex.get(elem.toNodeId)! * 3;
|
|
459
|
-
const dofs = [i0, i0+1, i0+2, i1, i1+1, i1+2];
|
|
459
|
+
const dofs = [i0, i0 + 1, i0 + 2, i1, i1 + 1, i1 + 2];
|
|
460
460
|
for (let r = 0; r < 6; r++) F[dofs[r]] += f_global[r];
|
|
461
461
|
}
|
|
462
462
|
|
|
@@ -466,9 +466,18 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
466
466
|
const restraints: boolean[] = new Array<boolean>(nDOF).fill(false);
|
|
467
467
|
for (const support of model.supports) {
|
|
468
468
|
const idx = nodeIndex.get(support.nodeId)! * 3;
|
|
469
|
-
if (support.ux)
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
if (support.ux) {
|
|
470
|
+
K[idx][idx] += PENALTY;
|
|
471
|
+
restraints[idx] = true;
|
|
472
|
+
}
|
|
473
|
+
if (support.uy) {
|
|
474
|
+
K[idx + 1][idx + 1] += PENALTY;
|
|
475
|
+
restraints[idx + 1] = true;
|
|
476
|
+
}
|
|
477
|
+
if (support.theta) {
|
|
478
|
+
K[idx + 2][idx + 2] += PENALTY;
|
|
479
|
+
restraints[idx + 2] = true;
|
|
480
|
+
}
|
|
472
481
|
}
|
|
473
482
|
|
|
474
483
|
// Solve K * u = F
|
|
@@ -479,8 +488,8 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
479
488
|
const idx = nodeIndex.get(node.id)! * 3;
|
|
480
489
|
return {
|
|
481
490
|
nodeId: node.id,
|
|
482
|
-
ux:
|
|
483
|
-
uy:
|
|
491
|
+
ux: u[idx],
|
|
492
|
+
uy: u[idx + 1],
|
|
484
493
|
theta: u[idx + 2],
|
|
485
494
|
};
|
|
486
495
|
});
|
|
@@ -490,14 +499,13 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
490
499
|
const K_clean = zeros(nDOF, nDOF);
|
|
491
500
|
for (const elem of model.elements) {
|
|
492
501
|
const fromNode = nodeById.get(elem.fromNodeId)!;
|
|
493
|
-
const toNode
|
|
502
|
+
const toNode = nodeById.get(elem.toNodeId)!;
|
|
494
503
|
const Ke = globalElementStiffness(elem, fromNode, toNode);
|
|
495
504
|
const i0 = nodeIndex.get(elem.fromNodeId)! * 3;
|
|
496
505
|
const i1 = nodeIndex.get(elem.toNodeId)! * 3;
|
|
497
|
-
const dofs = [i0, i0+1, i0+2, i1, i1+1, i1+2];
|
|
506
|
+
const dofs = [i0, i0 + 1, i0 + 2, i1, i1 + 1, i1 + 2];
|
|
498
507
|
for (let r = 0; r < 6; r++)
|
|
499
|
-
for (let c_ = 0; c_ < 6; c_++)
|
|
500
|
-
K_clean[dofs[r]][dofs[c_]] += Ke[r][c_];
|
|
508
|
+
for (let c_ = 0; c_ < 6; c_++) K_clean[dofs[r]][dofs[c_]] += Ke[r][c_];
|
|
501
509
|
}
|
|
502
510
|
|
|
503
511
|
const reactions: SupportReaction[] = model.supports.map((support) => {
|
|
@@ -505,8 +513,8 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
505
513
|
const Ku_row = (row: number) => K_clean[row].reduce((s, kij, j) => s + kij * u[j], 0);
|
|
506
514
|
return {
|
|
507
515
|
nodeId: support.nodeId,
|
|
508
|
-
Rx: support.ux
|
|
509
|
-
Ry: support.uy
|
|
516
|
+
Rx: support.ux ? Ku_row(idx) - F[idx] : 0,
|
|
517
|
+
Ry: support.uy ? Ku_row(idx + 1) - F[idx + 1] : 0,
|
|
510
518
|
Mz: support.theta ? Ku_row(idx + 2) - F[idx + 2] : 0,
|
|
511
519
|
};
|
|
512
520
|
});
|
|
@@ -514,7 +522,7 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
514
522
|
// Recover element internal forces
|
|
515
523
|
const elementForces: ElementForces[] = model.elements.map((elem) => {
|
|
516
524
|
const fromNode = nodeById.get(elem.fromNodeId)!;
|
|
517
|
-
const toNode
|
|
525
|
+
const toNode = nodeById.get(elem.toNodeId)!;
|
|
518
526
|
const dx = toNode.x - fromNode.x;
|
|
519
527
|
const dy = toNode.y - fromNode.y;
|
|
520
528
|
const L = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -526,11 +534,13 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
526
534
|
|
|
527
535
|
const i0 = nodeIndex.get(elem.fromNodeId)! * 3;
|
|
528
536
|
const i1 = nodeIndex.get(elem.toNodeId)! * 3;
|
|
529
|
-
const u_global = [u[i0], u[i0+1], u[i0+2], u[i1], u[i1+1], u[i1+2]];
|
|
537
|
+
const u_global = [u[i0], u[i0 + 1], u[i0 + 2], u[i1], u[i1 + 1], u[i1 + 2]];
|
|
530
538
|
|
|
531
539
|
// Transform to local: u_local = T * u_global
|
|
532
540
|
const T = transformMatrix(c, s);
|
|
533
|
-
const u_local = u_global.map((_, i) =>
|
|
541
|
+
const u_local = u_global.map((_, i) =>
|
|
542
|
+
T[i].reduce((sum, tij, j) => sum + tij * u_global[j], 0)
|
|
543
|
+
);
|
|
534
544
|
|
|
535
545
|
const k_local = localStiffness(E, I, A, L);
|
|
536
546
|
// f_local = k_local * u_local (internal forces in local frame)
|
|
@@ -544,7 +554,14 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
544
554
|
const dl = (model.distributedLoads ?? []).find((d) => d.elementId === elem.id);
|
|
545
555
|
let f_local_adj = [...f_local];
|
|
546
556
|
if (dl) {
|
|
547
|
-
const f_fef_local = [
|
|
557
|
+
const f_fef_local = [
|
|
558
|
+
0,
|
|
559
|
+
(dl.w * L) / 2,
|
|
560
|
+
(dl.w * L * L) / 12,
|
|
561
|
+
0,
|
|
562
|
+
(dl.w * L) / 2,
|
|
563
|
+
-(dl.w * L * L) / 12,
|
|
564
|
+
];
|
|
548
565
|
f_local_adj = f_local.map((v, i) => v + f_fef_local[i]);
|
|
549
566
|
}
|
|
550
567
|
|
|
@@ -558,18 +575,18 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
558
575
|
|
|
559
576
|
return {
|
|
560
577
|
elementId: elem.id,
|
|
561
|
-
N_start:
|
|
562
|
-
V_start:
|
|
563
|
-
M_start:
|
|
564
|
-
N_end:
|
|
565
|
-
V_end:
|
|
566
|
-
M_end:
|
|
578
|
+
N_start: -f_local_adj[0], // sign: tension positive convention
|
|
579
|
+
V_start: -f_local_adj[1],
|
|
580
|
+
M_start: f_local_adj[2],
|
|
581
|
+
N_end: f_local_adj[3],
|
|
582
|
+
V_end: f_local_adj[4],
|
|
583
|
+
M_end: f_local_adj[5],
|
|
567
584
|
utilisationRatio,
|
|
568
585
|
};
|
|
569
586
|
});
|
|
570
587
|
|
|
571
588
|
const maxDisplacementM = Math.max(
|
|
572
|
-
...nodeDisplacements.map((d) => Math.sqrt(d.ux ** 2 + d.uy ** 2))
|
|
589
|
+
...nodeDisplacements.map((d) => Math.sqrt(d.ux ** 2 + d.uy ** 2))
|
|
573
590
|
);
|
|
574
591
|
const maxUtilisationRatio = Math.max(0, ...elementForces.map((ef) => ef.utilisationRatio));
|
|
575
592
|
|
|
@@ -590,7 +607,7 @@ export function solveFrame2D(model: Frame2DModel): Frame2DResult {
|
|
|
590
607
|
export function buildFrame2DReceipt(
|
|
591
608
|
model: Frame2DModel,
|
|
592
609
|
result: Frame2DResult,
|
|
593
|
-
options: Frame2DReceiptOptions = {}
|
|
610
|
+
options: Frame2DReceiptOptions = {}
|
|
594
611
|
): Frame2DReceipt {
|
|
595
612
|
const violations: Array<{ criterion: string; message: string }> = [];
|
|
596
613
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export * from './frame2d';
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export {
|
|
3
|
+
createStructuralAnalysisHandler,
|
|
4
|
+
type StructuralAnalysisConfig,
|
|
5
|
+
type StructureType,
|
|
6
|
+
type MaterialType,
|
|
7
|
+
} from './traits/StructuralAnalysisTrait';
|
|
8
|
+
export {
|
|
9
|
+
createLoadBearingHandler,
|
|
10
|
+
type LoadBearingConfig,
|
|
11
|
+
type LoadCase,
|
|
12
|
+
type LoadType,
|
|
13
|
+
} from './traits/LoadBearingTrait';
|
|
14
|
+
export {
|
|
15
|
+
createMaterialFatigueHandler,
|
|
16
|
+
type MaterialFatigueConfig,
|
|
17
|
+
} from './traits/MaterialFatigueTrait';
|
|
5
18
|
export * from './traits/types';
|
|
6
19
|
|
|
7
20
|
import { createStructuralAnalysisHandler } from './traits/StructuralAnalysisTrait';
|
|
@@ -10,8 +23,16 @@ import { createMaterialFatigueHandler } from './traits/MaterialFatigueTrait';
|
|
|
10
23
|
|
|
11
24
|
export * from './frame2d';
|
|
12
25
|
|
|
13
|
-
export const pluginMeta = {
|
|
14
|
-
|
|
26
|
+
export const pluginMeta = {
|
|
27
|
+
name: '@holoscript/plugin-civil-engineering',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
traits: ['structural_analysis', 'load_bearing', 'material_fatigue', 'dsm_frame_2d'],
|
|
30
|
+
};
|
|
31
|
+
export const traitHandlers = [
|
|
32
|
+
createStructuralAnalysisHandler(),
|
|
33
|
+
createLoadBearingHandler(),
|
|
34
|
+
createMaterialFatigueHandler(),
|
|
35
|
+
];
|
|
15
36
|
|
|
16
37
|
// Runtime integration — behavioral trait handler + registrar that wire the
|
|
17
38
|
// deterministic Direct-Stiffness-Method 2D frame solver into HoloScriptRuntime's
|
package/src/runtime.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface RuntimeTraitHandler {
|
|
|
88
88
|
node: unknown,
|
|
89
89
|
config: DsmFrame2dTraitConfig,
|
|
90
90
|
context: TraitDispatchContext,
|
|
91
|
-
delta: number
|
|
91
|
+
delta: number
|
|
92
92
|
) => void;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -120,7 +120,7 @@ function resolveModel(config: DsmFrame2dTraitConfig | undefined): Frame2DModel |
|
|
|
120
120
|
function solveOntoNode(
|
|
121
121
|
node: unknown,
|
|
122
122
|
config: DsmFrame2dTraitConfig | undefined,
|
|
123
|
-
context: TraitDispatchContext
|
|
123
|
+
context: TraitDispatchContext
|
|
124
124
|
): void {
|
|
125
125
|
const carrier = node as DsmFrame2dNode;
|
|
126
126
|
const nodeId = carrier.id ?? carrier.name ?? 'unknown';
|
|
@@ -2,24 +2,57 @@
|
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
4
|
export type LoadType = 'dead' | 'live' | 'wind' | 'seismic' | 'snow' | 'impact' | 'thermal';
|
|
5
|
-
export interface LoadCase {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export interface LoadCase {
|
|
6
|
+
id: string;
|
|
7
|
+
type: LoadType;
|
|
8
|
+
magnitudeKN: number;
|
|
9
|
+
direction: [number, number, number];
|
|
10
|
+
combinationFactor: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LoadBearingConfig {
|
|
13
|
+
capacityKN: number;
|
|
14
|
+
loadCases: LoadCase[];
|
|
15
|
+
redundancyFactor: number;
|
|
16
|
+
}
|
|
17
|
+
export interface LoadBearingState {
|
|
18
|
+
totalAppliedKN: number;
|
|
19
|
+
remainingCapacityKN: number;
|
|
20
|
+
criticalLoadCase: string | null;
|
|
21
|
+
isOverloaded: boolean;
|
|
22
|
+
}
|
|
8
23
|
|
|
9
24
|
const defaultConfig: LoadBearingConfig = { capacityKN: 100, loadCases: [], redundancyFactor: 1.0 };
|
|
10
25
|
|
|
11
26
|
export function createLoadBearingHandler(): TraitHandler<LoadBearingConfig> {
|
|
12
|
-
return {
|
|
27
|
+
return {
|
|
28
|
+
name: 'load_bearing',
|
|
29
|
+
defaultConfig,
|
|
13
30
|
onAttach(n: HSPlusNode, c: LoadBearingConfig, ctx: TraitContext) {
|
|
14
31
|
const total = c.loadCases.reduce((sum, lc) => sum + lc.magnitudeKN * lc.combinationFactor, 0);
|
|
15
|
-
n.__loadState = {
|
|
32
|
+
n.__loadState = {
|
|
33
|
+
totalAppliedKN: total,
|
|
34
|
+
remainingCapacityKN: c.capacityKN - total,
|
|
35
|
+
criticalLoadCase: null,
|
|
36
|
+
isOverloaded: total > c.capacityKN,
|
|
37
|
+
};
|
|
16
38
|
ctx.emit?.('load:assessed', { total, capacity: c.capacityKN });
|
|
17
39
|
},
|
|
18
|
-
onDetach(n: HSPlusNode, _c: LoadBearingConfig, ctx: TraitContext) {
|
|
40
|
+
onDetach(n: HSPlusNode, _c: LoadBearingConfig, ctx: TraitContext) {
|
|
41
|
+
delete n.__loadState;
|
|
42
|
+
ctx.emit?.('load:removed');
|
|
43
|
+
},
|
|
19
44
|
onUpdate() {},
|
|
20
45
|
onEvent(n: HSPlusNode, c: LoadBearingConfig, ctx: TraitContext, e: TraitEvent) {
|
|
21
|
-
const s = n.__loadState as LoadBearingState | undefined;
|
|
22
|
-
if (
|
|
46
|
+
const s = n.__loadState as LoadBearingState | undefined;
|
|
47
|
+
if (!s) return;
|
|
48
|
+
if (e.type === 'load:add') {
|
|
49
|
+
const kn = (e.payload?.magnitudeKN as number) ?? 0;
|
|
50
|
+
s.totalAppliedKN += kn;
|
|
51
|
+
s.remainingCapacityKN = c.capacityKN - s.totalAppliedKN;
|
|
52
|
+
s.isOverloaded = s.totalAppliedKN > c.capacityKN;
|
|
53
|
+
if (s.isOverloaded)
|
|
54
|
+
ctx.emit?.('load:overloaded', { applied: s.totalAppliedKN, capacity: c.capacityKN });
|
|
55
|
+
}
|
|
23
56
|
},
|
|
24
57
|
};
|
|
25
58
|
}
|
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
/** @material_fatigue Trait — Fatigue life estimation. @trait material_fatigue */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export interface MaterialFatigueConfig {
|
|
5
|
-
|
|
4
|
+
export interface MaterialFatigueConfig {
|
|
5
|
+
sNcurveSlope: number;
|
|
6
|
+
enduranceLimitMPa: number;
|
|
7
|
+
designLifeCycles: number;
|
|
8
|
+
stressRatio: number;
|
|
9
|
+
surfaceFinishFactor: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MaterialFatigueState {
|
|
12
|
+
accumulatedCycles: number;
|
|
13
|
+
damageFraction: number;
|
|
14
|
+
remainingLifePercent: number;
|
|
15
|
+
isFailed: boolean;
|
|
16
|
+
}
|
|
6
17
|
|
|
7
|
-
const defaultConfig: MaterialFatigueConfig = {
|
|
18
|
+
const defaultConfig: MaterialFatigueConfig = {
|
|
19
|
+
sNcurveSlope: -0.1,
|
|
20
|
+
enduranceLimitMPa: 200,
|
|
21
|
+
designLifeCycles: 1e6,
|
|
22
|
+
stressRatio: -1,
|
|
23
|
+
surfaceFinishFactor: 0.9,
|
|
24
|
+
};
|
|
8
25
|
|
|
9
26
|
export function createMaterialFatigueHandler(): TraitHandler<MaterialFatigueConfig> {
|
|
10
|
-
return {
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
return {
|
|
28
|
+
name: 'material_fatigue',
|
|
29
|
+
defaultConfig,
|
|
30
|
+
onAttach(n: HSPlusNode, _c: MaterialFatigueConfig, ctx: TraitContext) {
|
|
31
|
+
n.__fatigueState = {
|
|
32
|
+
accumulatedCycles: 0,
|
|
33
|
+
damageFraction: 0,
|
|
34
|
+
remainingLifePercent: 100,
|
|
35
|
+
isFailed: false,
|
|
36
|
+
};
|
|
37
|
+
ctx.emit?.('fatigue:monitoring_started');
|
|
38
|
+
},
|
|
39
|
+
onDetach(n: HSPlusNode, _c: MaterialFatigueConfig, ctx: TraitContext) {
|
|
40
|
+
delete n.__fatigueState;
|
|
41
|
+
ctx.emit?.('fatigue:monitoring_stopped');
|
|
42
|
+
},
|
|
13
43
|
onUpdate() {},
|
|
14
44
|
onEvent(n: HSPlusNode, c: MaterialFatigueConfig, ctx: TraitContext, e: TraitEvent) {
|
|
15
|
-
const s = n.__fatigueState as MaterialFatigueState | undefined;
|
|
45
|
+
const s = n.__fatigueState as MaterialFatigueState | undefined;
|
|
46
|
+
if (!s || s.isFailed) return;
|
|
16
47
|
if (e.type === 'fatigue:add_cycles') {
|
|
17
48
|
const cycles = (e.payload?.cycles as number) ?? 0;
|
|
18
49
|
const stressMPa = (e.payload?.stressAmplitudeMPa as number) ?? 0;
|
|
19
50
|
s.accumulatedCycles += cycles;
|
|
20
|
-
const nf =
|
|
51
|
+
const nf =
|
|
52
|
+
stressMPa > c.enduranceLimitMPa
|
|
53
|
+
? Math.pow(stressMPa / c.enduranceLimitMPa, 1 / c.sNcurveSlope) * c.designLifeCycles
|
|
54
|
+
: Infinity;
|
|
21
55
|
s.damageFraction += cycles / nf;
|
|
22
56
|
s.remainingLifePercent = Math.max(0, (1 - s.damageFraction) * 100);
|
|
23
|
-
if (s.damageFraction >= 1) {
|
|
24
|
-
|
|
57
|
+
if (s.damageFraction >= 1) {
|
|
58
|
+
s.isFailed = true;
|
|
59
|
+
ctx.emit?.('fatigue:failure', { cycles: s.accumulatedCycles });
|
|
60
|
+
} else
|
|
61
|
+
ctx.emit?.('fatigue:updated', {
|
|
62
|
+
damage: s.damageFraction,
|
|
63
|
+
remaining: s.remainingLifePercent,
|
|
64
|
+
});
|
|
25
65
|
}
|
|
26
66
|
},
|
|
27
67
|
};
|
|
@@ -3,25 +3,62 @@ import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types
|
|
|
3
3
|
|
|
4
4
|
export type StructureType = 'beam' | 'column' | 'slab' | 'truss' | 'frame' | 'shell' | 'foundation';
|
|
5
5
|
export type MaterialType = 'steel' | 'concrete' | 'timber' | 'masonry' | 'composite' | 'aluminum';
|
|
6
|
-
export interface StructuralAnalysisConfig {
|
|
7
|
-
|
|
6
|
+
export interface StructuralAnalysisConfig {
|
|
7
|
+
structureType: StructureType;
|
|
8
|
+
material: MaterialType;
|
|
9
|
+
yieldStrengthMPa: number;
|
|
10
|
+
elasticModulusGPa: number;
|
|
11
|
+
safetyFactor: number;
|
|
12
|
+
loadCasesCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface StructuralAnalysisState {
|
|
15
|
+
maxStressMPa: number;
|
|
16
|
+
maxDeflectionMm: number;
|
|
17
|
+
utilizationRatio: number;
|
|
18
|
+
isPasssing: boolean;
|
|
19
|
+
}
|
|
8
20
|
|
|
9
|
-
const defaultConfig: StructuralAnalysisConfig = {
|
|
21
|
+
const defaultConfig: StructuralAnalysisConfig = {
|
|
22
|
+
structureType: 'beam',
|
|
23
|
+
material: 'steel',
|
|
24
|
+
yieldStrengthMPa: 250,
|
|
25
|
+
elasticModulusGPa: 200,
|
|
26
|
+
safetyFactor: 1.5,
|
|
27
|
+
loadCasesCount: 1,
|
|
28
|
+
};
|
|
10
29
|
|
|
11
30
|
export function createStructuralAnalysisHandler(): TraitHandler<StructuralAnalysisConfig> {
|
|
12
|
-
return {
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
return {
|
|
32
|
+
name: 'structural_analysis',
|
|
33
|
+
defaultConfig,
|
|
34
|
+
onAttach(n: HSPlusNode, _c: StructuralAnalysisConfig, ctx: TraitContext) {
|
|
35
|
+
n.__structState = {
|
|
36
|
+
maxStressMPa: 0,
|
|
37
|
+
maxDeflectionMm: 0,
|
|
38
|
+
utilizationRatio: 0,
|
|
39
|
+
isPasssing: true,
|
|
40
|
+
};
|
|
41
|
+
ctx.emit?.('structural:ready');
|
|
42
|
+
},
|
|
43
|
+
onDetach(n: HSPlusNode, _c: StructuralAnalysisConfig, ctx: TraitContext) {
|
|
44
|
+
delete n.__structState;
|
|
45
|
+
ctx.emit?.('structural:removed');
|
|
46
|
+
},
|
|
15
47
|
onUpdate() {},
|
|
16
48
|
onEvent(n: HSPlusNode, c: StructuralAnalysisConfig, ctx: TraitContext, e: TraitEvent) {
|
|
17
|
-
const s = n.__structState as StructuralAnalysisState | undefined;
|
|
49
|
+
const s = n.__structState as StructuralAnalysisState | undefined;
|
|
50
|
+
if (!s) return;
|
|
18
51
|
if (e.type === 'structural:analyze') {
|
|
19
52
|
const loadKN = (e.payload?.loadKN as number) ?? 0;
|
|
20
53
|
const spanM = (e.payload?.spanM as number) ?? 1;
|
|
21
|
-
s.maxStressMPa = (loadKN * 1000 * spanM) / (0.001 * c.elasticModulusGPa * 1e9) * 1e6;
|
|
54
|
+
s.maxStressMPa = ((loadKN * 1000 * spanM) / (0.001 * c.elasticModulusGPa * 1e9)) * 1e6;
|
|
22
55
|
s.utilizationRatio = s.maxStressMPa / (c.yieldStrengthMPa / c.safetyFactor);
|
|
23
56
|
s.isPasssing = s.utilizationRatio <= 1.0;
|
|
24
|
-
ctx.emit?.('structural:result', {
|
|
57
|
+
ctx.emit?.('structural:result', {
|
|
58
|
+
stress: s.maxStressMPa,
|
|
59
|
+
utilization: s.utilizationRatio,
|
|
60
|
+
pass: s.isPasssing,
|
|
61
|
+
});
|
|
25
62
|
}
|
|
26
63
|
},
|
|
27
64
|
};
|
package/src/traits/types.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export interface HSPlusNode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export interface HSPlusNode {
|
|
2
|
+
id?: string;
|
|
3
|
+
properties?: Record<string, unknown>;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface TraitContext {
|
|
7
|
+
emit?: (event: string, payload?: unknown) => void;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface TraitEvent {
|
|
11
|
+
type: string;
|
|
12
|
+
payload?: Record<string, unknown>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface TraitHandler<T = unknown> {
|
|
16
|
+
name: string;
|
|
17
|
+
defaultConfig: T;
|
|
18
|
+
onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
19
|
+
onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
20
|
+
onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
|
|
21
|
+
onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
|
|
22
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
|
|
4
|
+
"include": ["src"]
|
|
5
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 HoloScript Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|