dsh-cad 0.13.2 → 0.14.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/LICENSE +201 -201
- package/README.md +3 -3
- package/README.zh-CN.md +1 -1
- package/lib/cad_connector/freecad-executor.js +209 -209
- package/lib/cad_connector/fusion360-executor.js +244 -244
- package/lib/client.js +172 -172
- package/lib/convert/step-worker.mjs +61 -61
- package/lib/demo-bracket.brep +667 -667
- package/lib/demo-flange.brep +708 -708
- package/lib/demo-shaft.brep +515 -515
- package/lib/modeling/modeling-worker.cjs +2 -2
- package/lib/modeling/occt-adapter.cjs +90 -3
- package/lib/modeling/occtts-adapter.cjs +80 -7
- package/lib/routes.js +5 -0
- package/lib/tools/cad-model.js +16 -5
- package/lib/types/modeling/client.d.ts +19 -1
- package/package.json +1 -1
|
@@ -166,8 +166,8 @@ async function applyOp(op) {
|
|
|
166
166
|
}
|
|
167
167
|
case 'loft': {
|
|
168
168
|
const bodyId = op.bodyId
|
|
169
|
-
const shape = adapter.makeLoft(op.sections, { solid: op.solid, ruled: op.ruled })
|
|
170
|
-
assertUsable(shape, 'loft', '
|
|
169
|
+
const shape = adapter.makeLoft(op.sections, { solid: op.solid, ruled: op.ruled, smooth: op.smooth })
|
|
170
|
+
assertUsable(shape, 'loft', '检查各截面是否闭合、点数是否合理;平滑截面要求截面点共面且不重复')
|
|
171
171
|
bodies.set(bodyId, { shape, name: op.name ?? bodyId })
|
|
172
172
|
return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
|
|
173
173
|
}
|
|
@@ -319,18 +319,105 @@ function createAdapter(occt) {
|
|
|
319
319
|
return face
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Closed Catmull-Rom polyline through the given flat [x,y,z, …] points,
|
|
324
|
+
* sampled at `samples` chords per span. This build has no curve→edge
|
|
325
|
+
* binding, so the occt.ts backend's periodic B-spline section is carried
|
|
326
|
+
* here as a dense closed polyline (the same approximation the bspline
|
|
327
|
+
* profile segment uses) — the loft still smooths across sections.
|
|
328
|
+
*/
|
|
329
|
+
function smoothLoop(points, samples = 16) {
|
|
330
|
+
if (!Array.isArray(points) || points.length < 9 || points.length % 3 !== 0) {
|
|
331
|
+
throw new Error('a section needs at least 3 [x,y,z] triplets (≥9 numbers)')
|
|
332
|
+
}
|
|
333
|
+
const n = points.length / 3
|
|
334
|
+
const at = (i) => {
|
|
335
|
+
const k = ((i % n) + n) % n
|
|
336
|
+
return [points[k * 3], points[k * 3 + 1], points[k * 3 + 2]]
|
|
337
|
+
}
|
|
338
|
+
const out = []
|
|
339
|
+
for (let i = 0; i < n; i++) {
|
|
340
|
+
const p0 = at(i - 1)
|
|
341
|
+
const p1 = at(i)
|
|
342
|
+
const p2 = at(i + 1)
|
|
343
|
+
const p3 = at(i + 2)
|
|
344
|
+
for (let s = 0; s < samples; s++) {
|
|
345
|
+
const t = s / samples
|
|
346
|
+
const t2 = t * t
|
|
347
|
+
const t3 = t2 * t
|
|
348
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
349
|
+
out.push(0.5 * (
|
|
350
|
+
2 * p1[axis]
|
|
351
|
+
+ (-p0[axis] + p2[axis]) * t
|
|
352
|
+
+ (2 * p0[axis] - 5 * p1[axis] + 4 * p2[axis] - p3[axis]) * t2
|
|
353
|
+
+ (-p0[axis] + 3 * p1[axis] - 3 * p2[axis] + p3[axis]) * t3
|
|
354
|
+
))
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return closedWire(out)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
|
362
|
+
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
363
|
+
const cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
|
364
|
+
const norm3 = (a) => {
|
|
365
|
+
const len = Math.hypot(a[0], a[1], a[2]) || 1
|
|
366
|
+
return [a[0] / len, a[1] / len, a[2] / len]
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Plane frame (origin, in-plane X, in-plane Y) for a planar curve section. */
|
|
370
|
+
function sectionFrame(section) {
|
|
371
|
+
const at = section.at ?? [0, 0, 0]
|
|
372
|
+
if (!Array.isArray(at) || at.length !== 3) throw new Error('section.at must be [x,y,z]')
|
|
373
|
+
const n = norm3(Array.isArray(section.normal) ? section.normal : [0, 0, 1])
|
|
374
|
+
const seed = Array.isArray(section.xAxis) ? section.xAxis : [1, 0, 0]
|
|
375
|
+
let u = sub3(seed, n.map((c) => c * dot3(seed, n)))
|
|
376
|
+
if (Math.hypot(u[0], u[1], u[2]) < 1e-9) {
|
|
377
|
+
const alt = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]
|
|
378
|
+
u = sub3(alt, n.map((c) => c * dot3(alt, n)))
|
|
379
|
+
}
|
|
380
|
+
u = norm3(u)
|
|
381
|
+
return [at, u, cross3(n, u)]
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* One loft section → a closed wire.
|
|
386
|
+
* [x,y,z, …] flat point loop (polygon, or smooth spline)
|
|
387
|
+
* { points: […], smooth?: bool } same, with a per-section override
|
|
388
|
+
* { through: [x,y,z, …] } smooth curve through the points
|
|
389
|
+
* { profile, at?, normal?, xAxis? } 2D curve profile placed in a plane
|
|
390
|
+
*/
|
|
391
|
+
function sectionWire(section, smoothDefault) {
|
|
392
|
+
if (Array.isArray(section)) return smoothDefault ? smoothLoop(section) : closedWire(section)
|
|
393
|
+
if (section === null || typeof section !== 'object') {
|
|
394
|
+
throw new Error('a loft section must be a flat [x,y,z,…] loop or a {points|through|profile, …} object')
|
|
395
|
+
}
|
|
396
|
+
if (section.profile !== undefined) {
|
|
397
|
+
const [o3, u3, v3] = sectionFrame(section)
|
|
398
|
+
return profileWire(section.profile, o3, u3, v3)
|
|
399
|
+
}
|
|
400
|
+
if (section.through !== undefined) return smoothLoop(section.through)
|
|
401
|
+
if (section.points !== undefined) {
|
|
402
|
+
return (section.smooth ?? smoothDefault) ? smoothLoop(section.points) : closedWire(section.points)
|
|
403
|
+
}
|
|
404
|
+
throw new Error('a section object needs `points`, `through` or `profile`')
|
|
405
|
+
}
|
|
406
|
+
|
|
322
407
|
/**
|
|
323
408
|
* Loft: skin a solid through successive closed sections (OCCT ThruSections).
|
|
324
|
-
*
|
|
325
|
-
* `
|
|
409
|
+
* Sections are flat [x,y,z, …] loops or section objects (see sectionWire);
|
|
410
|
+
* `smooth` fits a periodic B-spline through plain point loops on the occt.ts
|
|
411
|
+
* backend, `solid` caps the ends, `ruled` keeps the sides straight.
|
|
326
412
|
*/
|
|
327
413
|
function makeLoft(sections, options = {}) {
|
|
328
414
|
if (!Array.isArray(sections) || sections.length < 2) throw new Error('a loft needs at least 2 sections')
|
|
329
415
|
const solid = options.solid ?? true
|
|
330
416
|
const ruled = options.ruled ?? false
|
|
417
|
+
const smooth = options.smooth === true
|
|
331
418
|
// Verified spelling: this build binds only the 3-argument constructor.
|
|
332
419
|
const thru = new occt.BRepOffsetAPI_ThruSections(solid, ruled, 1e-6)
|
|
333
|
-
for (const section of sections) thru.AddWire(
|
|
420
|
+
for (const section of sections) thru.AddWire(sectionWire(section, smooth))
|
|
334
421
|
thru.Build()
|
|
335
422
|
if (!thru.IsDone()) throw new Error('loft failed (check that the sections are closed and non-degenerate)')
|
|
336
423
|
return thru.Shape()
|
|
@@ -174,9 +174,11 @@ function createOcctTsAdapter(mod) {
|
|
|
174
174
|
const parts = new mod.VectorShape()
|
|
175
175
|
try {
|
|
176
176
|
let cur = [profile.start[0], profile.start[1]]
|
|
177
|
+
const pushed = []
|
|
177
178
|
const push = (shape) => {
|
|
178
179
|
if (shape.isNull()) throw wrapError('segment edge', new Error('null edge'))
|
|
179
180
|
parts.push_back(shape)
|
|
181
|
+
pushed.push(shape)
|
|
180
182
|
}
|
|
181
183
|
for (const seg of segs) {
|
|
182
184
|
if (seg === null || typeof seg !== 'object') throw new Error('each profile segment must be an object')
|
|
@@ -222,6 +224,12 @@ function createOcctTsAdapter(mod) {
|
|
|
222
224
|
if (Math.hypot(cur[0] - profile.start[0], cur[1] - profile.start[1]) > 1e-9) {
|
|
223
225
|
push(mod.makeLine(cur[0], cur[1], 0, profile.start[0], profile.start[1], 0))
|
|
224
226
|
}
|
|
227
|
+
if (pushed.length === 1) {
|
|
228
|
+
// A single closed B-spline already IS a wire; makeWire needs ≥2 parts.
|
|
229
|
+
// (A lone edge cannot close a profile — say so instead of a kernel error.)
|
|
230
|
+
if (typeof pushed[0].shapeType === 'function' && pushed[0].shapeType() === 'Wire') return pushed[0]
|
|
231
|
+
throw new Error('a segment profile needs at least 2 segments (a single open edge cannot close a loop)')
|
|
232
|
+
}
|
|
225
233
|
const wire = mod.makeWire(parts)
|
|
226
234
|
if (wire.isNull()) throw wrapError('makeWire', new Error('null wire'))
|
|
227
235
|
return wire
|
|
@@ -241,17 +249,82 @@ function createOcctTsAdapter(mod) {
|
|
|
241
249
|
return solid
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
|
|
252
|
+
// ── loft sections: point loops, smooth curves, planar profiles ─────────────
|
|
253
|
+
/**
|
|
254
|
+
* Closed wire through a flat [x,y,z, …] loop: a polygon (the historical
|
|
255
|
+
* section) or — with `smooth` — a PERIODIC B-spline interpolating every
|
|
256
|
+
* point. The periodic spline has no start/end vertex, so a loft skinned over
|
|
257
|
+
* it carries no crease lines: the lateral face is a single smooth B-spline
|
|
258
|
+
* surface instead of one planar strip per polygon edge.
|
|
259
|
+
*/
|
|
260
|
+
function loopWire(xyz, smooth) {
|
|
261
|
+
if (!Array.isArray(xyz) || xyz.length < 9 || xyz.length % 3 !== 0) {
|
|
262
|
+
throw new Error('a section needs at least 3 [x,y,z] triplets (≥9 numbers)')
|
|
263
|
+
}
|
|
264
|
+
if (!smooth) return polygonWireXY(xyz)
|
|
265
|
+
const { ptr, free } = mallocF64(xyz)
|
|
266
|
+
try {
|
|
267
|
+
const wire = mod.makeBsplineThrough(ptr, xyz.length / 3, true, 1e-6)
|
|
268
|
+
if (wire.isNull()) throw wrapError('smooth section', new Error('periodic B-spline construction failed'))
|
|
269
|
+
return wire
|
|
270
|
+
} finally {
|
|
271
|
+
free()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Plane frame (origin, in-plane X, in-plane Y) for a planar curve section. */
|
|
276
|
+
function sectionFrame(section) {
|
|
277
|
+
const at = section.at ?? [0, 0, 0]
|
|
278
|
+
if (!Array.isArray(at) || at.length !== 3) throw new Error('section.at must be [x,y,z]')
|
|
279
|
+
const n = vec3.norm(Array.isArray(section.normal) ? section.normal : [0, 0, 1])
|
|
280
|
+
const seed = Array.isArray(section.xAxis) ? section.xAxis : [1, 0, 0]
|
|
281
|
+
const d = vec3.dot(seed, n)
|
|
282
|
+
let u = [seed[0] - n[0] * d, seed[1] - n[1] * d, seed[2] - n[2] * d]
|
|
283
|
+
if (Math.hypot(u[0], u[1], u[2]) < 1e-9) {
|
|
284
|
+
// xAxis parallel to the normal — fall back to a stable perpendicular.
|
|
285
|
+
const alt = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]
|
|
286
|
+
const d2 = vec3.dot(alt, n)
|
|
287
|
+
u = [alt[0] - n[0] * d2, alt[1] - n[1] * d2, alt[2] - n[2] * d2]
|
|
288
|
+
}
|
|
289
|
+
u = vec3.norm(u)
|
|
290
|
+
// v = n × u so that u × v = n — profileWire's (u,v) → plane mapping stays
|
|
291
|
+
// right-handed and profile +Y points along the plane normal cross X.
|
|
292
|
+
return [at, u, vec3.cross(n, u)]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* One loft section → a closed wire.
|
|
297
|
+
* [x,y,z, …] flat point loop (polygon, or smooth B-spline)
|
|
298
|
+
* { points: […], smooth?: bool } same, with a per-section override
|
|
299
|
+
* { through: [x,y,z, …] } periodic B-spline through the points
|
|
300
|
+
* { profile, at?, normal?, xAxis? } 2D curve profile placed in a plane
|
|
301
|
+
*/
|
|
302
|
+
function sectionWire(section, smoothDefault) {
|
|
303
|
+
if (Array.isArray(section)) return loopWire(section, smoothDefault)
|
|
304
|
+
if (section === null || typeof section !== 'object') {
|
|
305
|
+
throw new Error('a loft section must be a flat [x,y,z,…] loop or a {points|through|profile, …} object')
|
|
306
|
+
}
|
|
307
|
+
if (section.profile !== undefined) {
|
|
308
|
+
const [o3, u3, v3] = sectionFrame(section)
|
|
309
|
+
return profileWire(section.profile, o3, u3, v3)
|
|
310
|
+
}
|
|
311
|
+
if (section.through !== undefined) return loopWire(section.through, true)
|
|
312
|
+
if (section.points !== undefined) return loopWire(section.points, section.smooth ?? smoothDefault)
|
|
313
|
+
throw new Error('a section object needs `points`, `through` or `profile`')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Loft through closed 3D sections. `smooth: true` turns plain point loops
|
|
318
|
+
* into periodic B-splines, which is what makes a crease-free skin: OCCT
|
|
319
|
+
* skins a B-spline surface through continuous sections, while polygon
|
|
320
|
+
* sections leave a G0 crease along every polygon edge.
|
|
321
|
+
*/
|
|
245
322
|
function makeLoft(sections, options = {}) {
|
|
246
323
|
if (!Array.isArray(sections) || sections.length < 2) throw new Error('a loft needs at least 2 sections')
|
|
324
|
+
const smooth = options.smooth === true
|
|
247
325
|
const v = new mod.VectorShape()
|
|
248
326
|
try {
|
|
249
|
-
for (const section of sections)
|
|
250
|
-
if (!Array.isArray(section) || section.length < 9 || section.length % 3 !== 0) {
|
|
251
|
-
throw new Error('each section must be a flat [x,y,z,…] loop (≥9 numbers)')
|
|
252
|
-
}
|
|
253
|
-
v.push_back(polygonWireXY(section))
|
|
254
|
-
}
|
|
327
|
+
for (const section of sections) v.push_back(sectionWire(section, smooth))
|
|
255
328
|
const solid = mod.loft(v, options.ruled ?? false, options.solid ?? true)
|
|
256
329
|
if (solid.isNull()) throw wrapError('loft', new Error('null shape'))
|
|
257
330
|
if (Math.abs(volume(solid)) < 1e-9) {
|
package/lib/routes.js
CHANGED
|
@@ -166,6 +166,11 @@ export function registerDocsRoute(server, registry, binStore) {
|
|
|
166
166
|
name: doc.name,
|
|
167
167
|
bodies: doc.bodyCount,
|
|
168
168
|
updatedAt: doc.updatedAt,
|
|
169
|
+
// Op count: the panel's liveness signal. It polls this list and
|
|
170
|
+
// re-fetches the scene when the tracked document's version moves,
|
|
171
|
+
// so the viewport follows modeling ops even when the conversation
|
|
172
|
+
// meta pipeline is stale (host UI drift).
|
|
173
|
+
version: doc.opCount,
|
|
169
174
|
// Preview URL only when a published scene exists (memory or mirror).
|
|
170
175
|
...(await binStore.has(doc.id) ? { sceneUrl: `${BIN_ROUTE_PATH}/${doc.id}` } : {}),
|
|
171
176
|
})));
|
package/lib/tools/cad-model.js
CHANGED
|
@@ -792,16 +792,18 @@ export function createModelTools(deps) {
|
|
|
792
792
|
});
|
|
793
793
|
const cadLoft = defineTool({
|
|
794
794
|
name: 'cad_loft',
|
|
795
|
-
description: 'Loft (放样): skin a solid through successive closed sections
|
|
796
|
-
'
|
|
795
|
+
description: 'Loft (放样): skin a solid through successive closed sections (≥2), ordered along the loft. Each section is either a flat [x0,y0,z0, x1,y1,z1, …] point loop, or an object: {points:[…], smooth?:true}, {through:[…]} (closed B-spline interpolating the points), or {profile, at?, normal?, xAxis?} — a 2D curve profile (flat [x,y], {start,segments:[line|arc|bspline]}, or {circle:{center,radius}}) laid in its own plane. ' +
|
|
796
|
+
'For a CREASE-FREE surface pass `smooth: true` (or a curve section): a polygon-section loft is planar strips meeting at G0 creases, while a periodic B-spline / NURBS section skims ONE continuous B-spline lateral face. ' +
|
|
797
|
+
'Sections may differ in shape and point count (e.g. a square lofted to a hexagon); `ruled` keeps the sides straight instead of smoothing. ' +
|
|
797
798
|
'Each result passes a BRepCheck validity gate: a self-intersecting or degenerate loft is rejected with a clear message instead of storing an unusable body.',
|
|
798
799
|
parameters: {
|
|
799
800
|
sections: {
|
|
800
801
|
type: 'array',
|
|
801
802
|
required: true,
|
|
802
|
-
description: 'Ordered sections
|
|
803
|
-
items: { type: '
|
|
803
|
+
description: 'Ordered sections (≥2): each a flat [x,y,z,…] loop (≥9 numbers) or a section object {points|through|profile, at?, normal?, xAxis?, smooth?}.',
|
|
804
|
+
items: { type: 'json', description: 'One closed section loop or section object.' },
|
|
804
805
|
},
|
|
806
|
+
smooth: { type: 'boolean', description: 'Fit a periodic B-spline through each plain point loop — the crease-free "NURBS section" loft (default false).' },
|
|
805
807
|
ruled: { type: 'boolean', description: 'Straight (ruled) sides instead of a smoothed surface (default false).' },
|
|
806
808
|
solid: { type: 'boolean', description: 'Cap the ends into a solid (default true).' },
|
|
807
809
|
name: { type: 'string', description: 'Optional display name.' },
|
|
@@ -824,8 +826,17 @@ export function createModelTools(deps) {
|
|
|
824
826
|
await resolveDoc(exec);
|
|
825
827
|
if (!Array.isArray(args.sections) || args.sections.length < 2)
|
|
826
828
|
throw new Error('sections must list at least 2 closed loops');
|
|
829
|
+
for (const section of args.sections) {
|
|
830
|
+
if (Array.isArray(section)) {
|
|
831
|
+
if (section.length < 9 || section.length % 3 !== 0)
|
|
832
|
+
throw new Error('a point loop section needs ≥3 [x,y,z] triplets (≥9 numbers)');
|
|
833
|
+
}
|
|
834
|
+
else if (section === null || typeof section !== 'object') {
|
|
835
|
+
throw new Error('each section must be a flat [x,y,z,…] loop or a {points|through|profile, …} object');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
827
838
|
const bodyId = nextBodyId();
|
|
828
|
-
const op = { kind: 'loft', bodyId, sections: args.sections, solid: args.solid, ruled: args.ruled, name: args.name };
|
|
839
|
+
const op = { kind: 'loft', bodyId, sections: args.sections, solid: args.solid, ruled: args.ruled, smooth: args.smooth, name: args.name };
|
|
829
840
|
const result = await runModelOp(op);
|
|
830
841
|
return syncScene(op, result);
|
|
831
842
|
},
|
|
@@ -61,6 +61,23 @@ export interface DrawingViewSpec {
|
|
|
61
61
|
dir: [number, number, number];
|
|
62
62
|
xDir: [number, number, number];
|
|
63
63
|
}
|
|
64
|
+
/** One loft section: a flat world-space loop, or a curve/profile section. */
|
|
65
|
+
export type LoftSection = number[] | {
|
|
66
|
+
/** Flat [x,y,z, …] loop (polygon, or a smooth B-spline when `smooth`). */
|
|
67
|
+
points?: number[];
|
|
68
|
+
/** Flat [x,y,z, …] points interpolated by a closed B-spline. */
|
|
69
|
+
through?: number[];
|
|
70
|
+
/** 2D profile (flat [x,y,…], {start,segments}, or {circle}) in the section plane. */
|
|
71
|
+
profile?: unknown;
|
|
72
|
+
/** Plane origin for `profile` (default world origin). */
|
|
73
|
+
at?: [number, number, number];
|
|
74
|
+
/** Plane normal for `profile` (default +Z). */
|
|
75
|
+
normal?: [number, number, number];
|
|
76
|
+
/** In-plane X axis for `profile` (default world +X projected). */
|
|
77
|
+
xAxis?: [number, number, number];
|
|
78
|
+
/** Override the loft-level `smooth` flag for this section. */
|
|
79
|
+
smooth?: boolean;
|
|
80
|
+
};
|
|
64
81
|
export type ModelOp = {
|
|
65
82
|
kind: 'create_prim';
|
|
66
83
|
bodyId: string;
|
|
@@ -79,9 +96,10 @@ export type ModelOp = {
|
|
|
79
96
|
} | {
|
|
80
97
|
kind: 'loft';
|
|
81
98
|
bodyId: string;
|
|
82
|
-
sections:
|
|
99
|
+
sections: LoftSection[];
|
|
83
100
|
solid?: boolean;
|
|
84
101
|
ruled?: boolean;
|
|
102
|
+
smooth?: boolean;
|
|
85
103
|
name?: string;
|
|
86
104
|
} | {
|
|
87
105
|
kind: 'revolve';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cad",
|
|
3
3
|
"description": "CAD visualization plugin for DeepSeek Harness: cad_view / cad_info tools with an embedded 3D (STL/OBJ/STEP/IGES) and 2D (DXF/SVG) viewer card in the Web UI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "LAU-MARS",
|