@things-factory/figure-service 10.1.147 → 10.1.149
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/dist-server/routers/figure-mesh-router.d.ts +14 -0
- package/dist-server/routers/figure-mesh-router.js +54 -0
- package/dist-server/routers/figure-mesh-router.js.map +1 -0
- package/dist-server/routes.js +3 -0
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/figure/figure-glb.d.ts +62 -0
- package/dist-server/service/figure/figure-glb.js +229 -0
- package/dist-server/service/figure/figure-glb.js.map +1 -0
- package/dist-server/service/figure/figure-mesh-store.d.ts +30 -0
- package/dist-server/service/figure/figure-mesh-store.js +80 -0
- package/dist-server/service/figure/figure-mesh-store.js.map +1 -0
- package/dist-server/service/figure/figure-mutation.d.ts +8 -0
- package/dist-server/service/figure/figure-mutation.js +59 -0
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-package-export.js +15 -0
- package/dist-server/service/figure/figure-package-export.js.map +1 -1
- package/dist-server/service/figure/figure-query.d.ts +2 -0
- package/dist-server/service/figure/figure-query.js +15 -0
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +5 -0
- package/dist-server/service/figure/figure-type.js +16 -1
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/server/routers/figure-mesh-router.ts +59 -0
- package/server/routes.ts +3 -0
- package/server/service/figure/figure-glb.test.ts +112 -0
- package/server/service/figure/figure-glb.ts +251 -0
- package/server/service/figure/figure-mesh-db.test.ts +244 -0
- package/server/service/figure/figure-mesh-store.ts +91 -0
- package/server/service/figure/figure-mutation.ts +52 -0
- package/server/service/figure/figure-package-export.ts +15 -0
- package/server/service/figure/figure-query.ts +9 -0
- package/server/service/figure/figure-type.ts +10 -0
- package/translations/en.json +10 -1
- package/translations/es.json +10 -1
- package/translations/ja.json +10 -1
- package/translations/ko.json +10 -1
- package/translations/ms.json +10 -1
- package/translations/zh.json +10 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ThumbnailFiles } from '../service/figure/figure-thumbnail-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* A mesh part's file, by its hash (ADR-0129 decision 8): what the modeller and a board draw a mesh part from, on the
|
|
4
|
+
* server that authored it. A board server serves its own copy at `/figure-library-file/:hash`.
|
|
5
|
+
*
|
|
6
|
+
* The same door as a figure query, and the same scope: this domain's files and its parent's, whose figures it reads.
|
|
7
|
+
* The content is its hash, so it never changes: cached privately for as long as a browser keeps anything.
|
|
8
|
+
*/
|
|
9
|
+
export declare const figureMeshRouter: any;
|
|
10
|
+
/** The file of a mesh part the caller may read. Exported for tests, which give their own `files`. */
|
|
11
|
+
export declare function sendFigureMesh(context: any, next: () => Promise<any>, hash: string, domain: {
|
|
12
|
+
id: string;
|
|
13
|
+
parentId?: string;
|
|
14
|
+
}, files?: ThumbnailFiles): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.figureMeshRouter = void 0;
|
|
4
|
+
exports.sendFigureMesh = sendFigureMesh;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
|
|
7
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
8
|
+
const shell_1 = require("@things-factory/shell");
|
|
9
|
+
const figure_mesh_store_js_1 = require("../service/figure/figure-mesh-store.js");
|
|
10
|
+
const figure_glb_js_1 = require("../service/figure/figure-glb.js");
|
|
11
|
+
const figure_thumbnail_store_js_1 = require("../service/figure/figure-thumbnail-store.js");
|
|
12
|
+
/**
|
|
13
|
+
* A mesh part's file, by its hash (ADR-0129 decision 8): what the modeller and a board draw a mesh part from, on the
|
|
14
|
+
* server that authored it. A board server serves its own copy at `/figure-library-file/:hash`.
|
|
15
|
+
*
|
|
16
|
+
* The same door as a figure query, and the same scope: this domain's files and its parent's, whose figures it reads.
|
|
17
|
+
* The content is its hash, so it never changes: cached privately for as long as a browser keeps anything.
|
|
18
|
+
*/
|
|
19
|
+
exports.figureMeshRouter = new koa_router_1.default();
|
|
20
|
+
exports.figureMeshRouter.get('/figure-mesh/:hash', async (context, next) => {
|
|
21
|
+
const { domain, user, unsafeIP, prohibitedPrivileges } = context.state;
|
|
22
|
+
const allowed = await (0, auth_base_1.checkPermission)({ category: 'figure', privilege: 'query', owner: true }, user, domain, unsafeIP, prohibitedPrivileges);
|
|
23
|
+
if (!allowed) {
|
|
24
|
+
context.status = 403;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await sendFigureMesh(context, next, context.params.hash, domain);
|
|
28
|
+
});
|
|
29
|
+
/** The file of a mesh part the caller may read. Exported for tests, which give their own `files`. */
|
|
30
|
+
async function sendFigureMesh(context, next, hash, domain, files = (0, figure_thumbnail_store_js_1.thumbnailFiles)()) {
|
|
31
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
32
|
+
context.status = 404;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const kept = await (0, figure_mesh_store_js_1.meshAttachmentOf)({ getRepository: shell_1.getRepository }, [domain.id, domain.parentId].filter(Boolean), hash);
|
|
36
|
+
if (!kept) {
|
|
37
|
+
context.status = 404;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const ours = () => {
|
|
41
|
+
context.type = figure_glb_js_1.GLB_MIMETYPE;
|
|
42
|
+
context.set('Cache-Control', 'private, max-age=31536000, immutable');
|
|
43
|
+
context.set('ETag', `"${hash}"`);
|
|
44
|
+
};
|
|
45
|
+
ours();
|
|
46
|
+
context.status = 200;
|
|
47
|
+
if (context.fresh) {
|
|
48
|
+
context.status = 304;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await files.send(context, kept.path, next);
|
|
52
|
+
ours();
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=figure-mesh-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-mesh-router.js","sourceRoot":"","sources":["../../server/routers/figure-mesh-router.ts"],"names":[],"mappings":";;;AA6BA,wCA6BC;;AA1DD,oEAA+B;AAE/B,yDAA2D;AAC3D,iDAAqD;AAErD,iFAAyE;AACzE,mEAA8D;AAC9D,2FAAiG;AAEjG;;;;;;GAMG;AACU,QAAA,gBAAgB,GAAG,IAAI,oBAAM,EAAE,CAAA;AAE5C,wBAAgB,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACjE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACtE,MAAM,OAAO,GAAG,MAAM,IAAA,2BAAe,EAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAA;IAC5I,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AAClE,CAAC,CAAC,CAAA;AAEF,qGAAqG;AAC9F,KAAK,UAAU,cAAc,CAClC,OAAY,EACZ,IAAwB,EACxB,IAAY,EACZ,MAAyC,EACzC,QAAwB,IAAA,0CAAc,GAAE;IAExC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,IAAA,uCAAgB,EAAC,EAAE,aAAa,EAAb,qBAAa,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,EAAE,IAAI,CAAC,CAAA;IACtH,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,OAAO,CAAC,IAAI,GAAG,4BAAY,CAAA;QAC3B,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,sCAAsC,CAAC,CAAA;QACpE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,GAAG,CAAC,CAAA;IAClC,CAAC,CAAA;IACD,IAAI,EAAE,CAAA;IACN,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;IACpB,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAK,EAAE,IAAI,CAAC,CAAA;IAC3C,IAAI,EAAE,CAAA;AACR,CAAC","sourcesContent":["import Router from 'koa-router'\n\nimport { checkPermission } from '@things-factory/auth-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { meshAttachmentOf } from '../service/figure/figure-mesh-store.js'\nimport { GLB_MIMETYPE } from '../service/figure/figure-glb.js'\nimport { thumbnailFiles, type ThumbnailFiles } from '../service/figure/figure-thumbnail-store.js'\n\n/**\n * A mesh part's file, by its hash (ADR-0129 decision 8): what the modeller and a board draw a mesh part from, on the\n * server that authored it. A board server serves its own copy at `/figure-library-file/:hash`.\n *\n * The same door as a figure query, and the same scope: this domain's files and its parent's, whose figures it reads.\n * The content is its hash, so it never changes: cached privately for as long as a browser keeps anything.\n */\nexport const figureMeshRouter = new Router()\n\nfigureMeshRouter.get('/figure-mesh/:hash', async (context, next) => {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n const allowed = await checkPermission({ category: 'figure', privilege: 'query', owner: true }, user, domain, unsafeIP, prohibitedPrivileges)\n if (!allowed) {\n context.status = 403\n return\n }\n await sendFigureMesh(context, next, context.params.hash, domain)\n})\n\n/** The file of a mesh part the caller may read. Exported for tests, which give their own `files`. */\nexport async function sendFigureMesh(\n context: any,\n next: () => Promise<any>,\n hash: string,\n domain: { id: string; parentId?: string },\n files: ThumbnailFiles = thumbnailFiles()\n): Promise<void> {\n if (!/^[0-9a-f]{64}$/.test(hash)) {\n context.status = 404\n return\n }\n const kept = await meshAttachmentOf({ getRepository }, [domain.id, domain.parentId].filter(Boolean) as string[], hash)\n if (!kept) {\n context.status = 404\n return\n }\n const ours = () => {\n context.type = GLB_MIMETYPE\n context.set('Cache-Control', 'private, max-age=31536000, immutable')\n context.set('ETag', `\"${hash}\"`)\n }\n ours()\n context.status = 200\n if (context.fresh) {\n context.status = 304\n return\n }\n await files.send(context, kept.path!, next)\n ours()\n}\n"]}
|
package/dist-server/routes.js
CHANGED
|
@@ -4,6 +4,7 @@ const env_1 = require("@things-factory/env");
|
|
|
4
4
|
const shell_1 = require("@things-factory/shell");
|
|
5
5
|
const figure_thumbnail_router_js_1 = require("./routers/figure-thumbnail-router.js");
|
|
6
6
|
const figure_package_router_js_1 = require("./routers/figure-package-router.js");
|
|
7
|
+
const figure_mesh_router_js_1 = require("./routers/figure-mesh-router.js");
|
|
7
8
|
/**
|
|
8
9
|
* 도메인 경로가 주소에 들어가는 배치인가.
|
|
9
10
|
*
|
|
@@ -15,10 +16,12 @@ process.on('bootstrap-module-domain-private-route', (app, domainPrivateRouter) =
|
|
|
15
16
|
if (isPathBaseDomain) {
|
|
16
17
|
domainPrivateRouter.use(`/${(0, shell_1.getRoutePrefixForDomainType)()}/:domain`, figure_thumbnail_router_js_1.figureThumbnailRouter.routes(), figure_thumbnail_router_js_1.figureThumbnailRouter.allowedMethods());
|
|
17
18
|
domainPrivateRouter.use(`/${(0, shell_1.getRoutePrefixForDomainType)()}/:domain`, figure_package_router_js_1.figurePackageRouter.routes(), figure_package_router_js_1.figurePackageRouter.allowedMethods());
|
|
19
|
+
domainPrivateRouter.use(`/${(0, shell_1.getRoutePrefixForDomainType)()}/:domain`, figure_mesh_router_js_1.figureMeshRouter.routes(), figure_mesh_router_js_1.figureMeshRouter.allowedMethods());
|
|
18
20
|
}
|
|
19
21
|
else {
|
|
20
22
|
domainPrivateRouter.use('', figure_thumbnail_router_js_1.figureThumbnailRouter.routes(), figure_thumbnail_router_js_1.figureThumbnailRouter.allowedMethods());
|
|
21
23
|
domainPrivateRouter.use('', figure_package_router_js_1.figurePackageRouter.routes(), figure_package_router_js_1.figurePackageRouter.allowedMethods());
|
|
24
|
+
domainPrivateRouter.use('', figure_mesh_router_js_1.figureMeshRouter.routes(), figure_mesh_router_js_1.figureMeshRouter.allowedMethods());
|
|
22
25
|
}
|
|
23
26
|
});
|
|
24
27
|
//# sourceMappingURL=routes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA,6CAA4C;AAC5C,iDAAmE;AAEnE,qFAA4E;AAC5E,iFAAwE;
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA,6CAA4C;AAC5C,iDAAmE;AAEnE,qFAA4E;AAC5E,iFAAwE;AACxE,2EAAkE;AAElE;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG,CAAC,YAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;AAEjD,OAAO,CAAC,EAAE,CAAC,uCAA8C,EAAE,CAAC,GAAG,EAAE,mBAAmB,EAAE,EAAE;IACtF,IAAI,gBAAgB,EAAE,CAAC;QACrB,mBAAmB,CAAC,GAAG,CACrB,IAAI,IAAA,mCAA2B,GAAE,UAAU,EAC3C,kDAAqB,CAAC,MAAM,EAAE,EAC9B,kDAAqB,CAAC,cAAc,EAAE,CACvC,CAAA;QACD,mBAAmB,CAAC,GAAG,CAAC,IAAI,IAAA,mCAA2B,GAAE,UAAU,EAAE,8CAAmB,CAAC,MAAM,EAAE,EAAE,8CAAmB,CAAC,cAAc,EAAE,CAAC,CAAA;QACxI,mBAAmB,CAAC,GAAG,CAAC,IAAI,IAAA,mCAA2B,GAAE,UAAU,EAAE,wCAAgB,CAAC,MAAM,EAAE,EAAE,wCAAgB,CAAC,cAAc,EAAE,CAAC,CAAA;IACpI,CAAC;SAAM,CAAC;QACN,mBAAmB,CAAC,GAAG,CAAC,EAAE,EAAE,kDAAqB,CAAC,MAAM,EAAE,EAAE,kDAAqB,CAAC,cAAc,EAAE,CAAC,CAAA;QACnG,mBAAmB,CAAC,GAAG,CAAC,EAAE,EAAE,8CAAmB,CAAC,MAAM,EAAE,EAAE,8CAAmB,CAAC,cAAc,EAAE,CAAC,CAAA;QAC/F,mBAAmB,CAAC,GAAG,CAAC,EAAE,EAAE,wCAAgB,CAAC,MAAM,EAAE,EAAE,wCAAgB,CAAC,cAAc,EAAE,CAAC,CAAA;IAC3F,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["import { config } from '@things-factory/env'\nimport { getRoutePrefixForDomainType } from '@things-factory/shell'\n\nimport { figureThumbnailRouter } from './routers/figure-thumbnail-router.js'\nimport { figurePackageRouter } from './routers/figure-package-router.js'\nimport { figureMeshRouter } from './routers/figure-mesh-router.js'\n\n/**\n * 도메인 경로가 주소에 들어가는 배치인가.\n *\n * `subdomain` 설정이 있으면 도메인은 호스트 이름으로 갈리므로 경로에 도메인이 없다. 없으면\n * `/domain/{subdomain}/...` 로 갈린다 — board-service 가 같은 판단을 같은 방식으로 한다.\n */\nconst isPathBaseDomain = !config.get('subdomain')\n\nprocess.on('bootstrap-module-domain-private-route' as any, (app, domainPrivateRouter) => {\n if (isPathBaseDomain) {\n domainPrivateRouter.use(\n `/${getRoutePrefixForDomainType()}/:domain`,\n figureThumbnailRouter.routes(),\n figureThumbnailRouter.allowedMethods()\n )\n domainPrivateRouter.use(`/${getRoutePrefixForDomainType()}/:domain`, figurePackageRouter.routes(), figurePackageRouter.allowedMethods())\n domainPrivateRouter.use(`/${getRoutePrefixForDomainType()}/:domain`, figureMeshRouter.routes(), figureMeshRouter.allowedMethods())\n } else {\n domainPrivateRouter.use('', figureThumbnailRouter.routes(), figureThumbnailRouter.allowedMethods())\n domainPrivateRouter.use('', figurePackageRouter.routes(), figurePackageRouter.allowedMethods())\n domainPrivateRouter.use('', figureMeshRouter.routes(), figureMeshRouter.allowedMethods())\n }\n})\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare const GLB_MIMETYPE = "model/gltf-binary";
|
|
2
|
+
/** The largest file kept, in bytes. A board downloads it for every figure that draws it. */
|
|
3
|
+
export declare const GLB_MAX_BYTES: number;
|
|
4
|
+
export type Box = {
|
|
5
|
+
min: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
z: number;
|
|
9
|
+
};
|
|
10
|
+
max: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
z: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export interface GlbNode {
|
|
17
|
+
/** Its place in the file's node list: what a mesh part names it by. */
|
|
18
|
+
index: number;
|
|
19
|
+
name: string;
|
|
20
|
+
parent: number | null;
|
|
21
|
+
/** The node's subtree, in its own frame, in mm; none when it holds no mesh. */
|
|
22
|
+
bounds: Box | null;
|
|
23
|
+
triangles: number;
|
|
24
|
+
primitives: number;
|
|
25
|
+
/** Where the node stands in the file's root frame: what a part made from it is placed at. */
|
|
26
|
+
position: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
z: number;
|
|
30
|
+
};
|
|
31
|
+
rotation: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
z: number;
|
|
35
|
+
};
|
|
36
|
+
/** Why it cannot be a part, when it cannot. */
|
|
37
|
+
refused?: 'no-mesh' | 'sheared';
|
|
38
|
+
}
|
|
39
|
+
export interface GlbSummary {
|
|
40
|
+
/** The whole file in its root frame, in mm. */
|
|
41
|
+
bounds: Box;
|
|
42
|
+
triangles: number;
|
|
43
|
+
primitives: number;
|
|
44
|
+
unit: 'm';
|
|
45
|
+
nodes: GlbNode[];
|
|
46
|
+
extensions: string[];
|
|
47
|
+
}
|
|
48
|
+
export declare class GlbRefusal extends Error {
|
|
49
|
+
readonly code: 'GLB_NOT_BINARY' | 'GLB_VERSION' | 'GLB_UNREADABLE' | 'GLB_EXTERNAL' | 'GLB_EXTENSION' | 'GLB_NO_MESH' | 'GLB_TOO_LARGE';
|
|
50
|
+
/** What a screen says it with: the extensions it needs, the most bytes kept. */
|
|
51
|
+
readonly params: Record<string, string | number>;
|
|
52
|
+
constructor(code: 'GLB_NOT_BINARY' | 'GLB_VERSION' | 'GLB_UNREADABLE' | 'GLB_EXTERNAL' | 'GLB_EXTENSION' | 'GLB_NO_MESH' | 'GLB_TOO_LARGE', message: string,
|
|
53
|
+
/** What a screen says it with: the extensions it needs, the most bytes kept. */
|
|
54
|
+
params?: Record<string, string | number>);
|
|
55
|
+
}
|
|
56
|
+
/** A rotation as degrees about X, Y and Z in the order `rigid@1` turns them (Rx · Ry · Rz). */
|
|
57
|
+
export declare function eulerOfRotation(r: number[][]): {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
z: number;
|
|
61
|
+
};
|
|
62
|
+
export declare function readGlb(bytes: Uint8Array): GlbSummary;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* What a GLB file holds, read on upload (ADR-0129 decision 6): whether it is a glTF 2.0 binary the board can draw, and
|
|
4
|
+
* the numbers a mesh part carries -- its bounds in mm, its triangles, its primitives -- for the whole file and for each
|
|
5
|
+
* node of it. The kernel never reads the file (figure-model `mesh@1`, contract §13); these numbers come from here.
|
|
6
|
+
*
|
|
7
|
+
* Nothing is decoded but the JSON chunk. glTF requires every POSITION accessor to carry its min and max, so the bounds
|
|
8
|
+
* are those corners carried through the node transforms; a compressed buffer (Meshopt) keeps them too.
|
|
9
|
+
*
|
|
10
|
+
* A node's frame is its place in the file: its world translation and rotation, in the file's root frame. A part made
|
|
11
|
+
* from a node stands there (its position and rotation), turns about the node's origin, and is drawn with the node's
|
|
12
|
+
* world scale kept in the drawing. A node whose world transform shears or mirrors has no such frame and is not offered.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.GlbRefusal = exports.GLB_MAX_BYTES = exports.GLB_MIMETYPE = void 0;
|
|
16
|
+
exports.eulerOfRotation = eulerOfRotation;
|
|
17
|
+
exports.readGlb = readGlb;
|
|
18
|
+
const figure_model_1 = require("@hatiolab/figure-model");
|
|
19
|
+
/* The package contract's rule (figure-model): the same type and limit a board server holds an import to. */
|
|
20
|
+
exports.GLB_MIMETYPE = figure_model_1.FIGURE_MESH_CONTENT_TYPE;
|
|
21
|
+
/** The largest file kept, in bytes. A board downloads it for every figure that draws it. */
|
|
22
|
+
exports.GLB_MAX_BYTES = figure_model_1.FIGURE_MESH_MAX_BYTES;
|
|
23
|
+
/** The extensions a board can draw: Meshopt's decoder ships with the loader. Draco and KTX2 wait (ADR-0129 Q4). */
|
|
24
|
+
const DRAWABLE_EXTENSIONS = new Set(['EXT_meshopt_compression', 'KHR_mesh_quantization', 'KHR_materials_emissive_strength', 'KHR_texture_transform']);
|
|
25
|
+
class GlbRefusal extends Error {
|
|
26
|
+
constructor(code, message,
|
|
27
|
+
/** What a screen says it with: the extensions it needs, the most bytes kept. */
|
|
28
|
+
params = {}) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.params = params;
|
|
32
|
+
this.name = 'GlbRefusal';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.GlbRefusal = GlbRefusal;
|
|
36
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
37
|
+
const mul = (a, b) => {
|
|
38
|
+
const out = new Array(16).fill(0);
|
|
39
|
+
for (let c = 0; c < 4; c++)
|
|
40
|
+
for (let r = 0; r < 4; r++)
|
|
41
|
+
for (let k = 0; k < 4; k++)
|
|
42
|
+
out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
45
|
+
const apply = (m, p) => [
|
|
46
|
+
m[0] * p[0] + m[4] * p[1] + m[8] * p[2] + m[12],
|
|
47
|
+
m[1] * p[0] + m[5] * p[1] + m[9] * p[2] + m[13],
|
|
48
|
+
m[2] * p[0] + m[6] * p[1] + m[10] * p[2] + m[14]
|
|
49
|
+
];
|
|
50
|
+
function local(node) {
|
|
51
|
+
if (Array.isArray(node.matrix) && node.matrix.length === 16)
|
|
52
|
+
return node.matrix.map(Number);
|
|
53
|
+
const [tx, ty, tz] = node.translation ?? [0, 0, 0];
|
|
54
|
+
const [qx, qy, qz, qw] = node.rotation ?? [0, 0, 0, 1];
|
|
55
|
+
const [sx, sy, sz] = node.scale ?? [1, 1, 1];
|
|
56
|
+
const r = [
|
|
57
|
+
1 - 2 * (qy * qy + qz * qz), 2 * (qx * qy + qz * qw), 2 * (qx * qz - qy * qw),
|
|
58
|
+
2 * (qx * qy - qz * qw), 1 - 2 * (qx * qx + qz * qz), 2 * (qy * qz + qx * qw),
|
|
59
|
+
2 * (qx * qz + qy * qw), 2 * (qy * qz - qx * qw), 1 - 2 * (qx * qx + qy * qy)
|
|
60
|
+
];
|
|
61
|
+
return [r[0] * sx, r[1] * sx, r[2] * sx, 0, r[3] * sy, r[4] * sy, r[5] * sy, 0, r[6] * sz, r[7] * sz, r[8] * sz, 0, tx, ty, tz, 1];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A world matrix as translation and rotation, the scale left in the drawing. Undefined when its axes are not square to
|
|
65
|
+
* each other (a shear) or it mirrors: there is no frame to stand a part in.
|
|
66
|
+
*/
|
|
67
|
+
function frameOf(m) {
|
|
68
|
+
const cols = [0, 1, 2].map(c => [m[c * 4], m[c * 4 + 1], m[c * 4 + 2]]);
|
|
69
|
+
const len = cols.map(c => Math.hypot(...c));
|
|
70
|
+
if (len.some(l => !(l > 1e-12)))
|
|
71
|
+
return undefined;
|
|
72
|
+
const unit = cols.map((c, i) => c.map(v => v / len[i]));
|
|
73
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
74
|
+
if (Math.abs(dot(unit[0], unit[1])) > 1e-6 || Math.abs(dot(unit[0], unit[2])) > 1e-6 || Math.abs(dot(unit[1], unit[2])) > 1e-6)
|
|
75
|
+
return undefined;
|
|
76
|
+
const [a, b, c] = unit;
|
|
77
|
+
const det = a[0] * (b[1] * c[2] - b[2] * c[1]) - b[0] * (a[1] * c[2] - a[2] * c[1]) + c[0] * (a[1] * b[2] - a[2] * b[1]);
|
|
78
|
+
if (det < 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
/* rotation rows: R[r][c] = unit[c][r] */
|
|
81
|
+
const rotation = [0, 1, 2].map(r => [0, 1, 2].map(col => unit[col][r]));
|
|
82
|
+
const translation = [m[12], m[13], m[14]];
|
|
83
|
+
/* The inverse of the rigid part: Rᵀ and −Rᵀt. */
|
|
84
|
+
const inv = [...IDENTITY];
|
|
85
|
+
for (let r = 0; r < 3; r++)
|
|
86
|
+
for (let col = 0; col < 3; col++)
|
|
87
|
+
inv[col * 4 + r] = rotation[col][r];
|
|
88
|
+
for (let r = 0; r < 3; r++)
|
|
89
|
+
inv[12 + r] = -(rotation[0][r] * translation[0] + rotation[1][r] * translation[1] + rotation[2][r] * translation[2]);
|
|
90
|
+
return { rotation, translation, inverse: inv };
|
|
91
|
+
}
|
|
92
|
+
/** A rotation as degrees about X, Y and Z in the order `rigid@1` turns them (Rx · Ry · Rz). */
|
|
93
|
+
function eulerOfRotation(r) {
|
|
94
|
+
const deg = (a) => {
|
|
95
|
+
const d = Math.round(((a * 180) / Math.PI) * 1e6) / 1e6;
|
|
96
|
+
return d === 0 ? 0 : d;
|
|
97
|
+
};
|
|
98
|
+
const sy = Math.max(-1, Math.min(1, r[0][2]));
|
|
99
|
+
const y = Math.asin(sy);
|
|
100
|
+
if (Math.abs(sy) < 1 - 1e-9)
|
|
101
|
+
return { x: deg(Math.atan2(-r[1][2], r[2][2])), y: deg(y), z: deg(Math.atan2(-r[0][1], r[0][0])) };
|
|
102
|
+
/* About Y by a quarter turn: X and Z turn about one axis and only their sum counts; Z is taken as 0. */
|
|
103
|
+
return { x: deg(Math.atan2(r[2][1], r[1][1])), y: deg(y), z: 0 };
|
|
104
|
+
}
|
|
105
|
+
const TO_MM = 1000;
|
|
106
|
+
const empty = () => ({ min: { x: Infinity, y: Infinity, z: Infinity }, max: { x: -Infinity, y: -Infinity, z: -Infinity } });
|
|
107
|
+
const grow = (box, p) => {
|
|
108
|
+
box.min = { x: Math.min(box.min.x, p[0]), y: Math.min(box.min.y, p[1]), z: Math.min(box.min.z, p[2]) };
|
|
109
|
+
box.max = { x: Math.max(box.max.x, p[0]), y: Math.max(box.max.y, p[1]), z: Math.max(box.max.z, p[2]) };
|
|
110
|
+
};
|
|
111
|
+
const round = (v) => {
|
|
112
|
+
const r = Math.round(v * 1e6) / 1e6;
|
|
113
|
+
return r === 0 ? 0 : r;
|
|
114
|
+
};
|
|
115
|
+
const inMm = (box) => ({
|
|
116
|
+
min: { x: round(box.min.x * TO_MM), y: round(box.min.y * TO_MM), z: round(box.min.z * TO_MM) },
|
|
117
|
+
max: { x: round(box.max.x * TO_MM), y: round(box.max.y * TO_MM), z: round(box.max.z * TO_MM) }
|
|
118
|
+
});
|
|
119
|
+
/** Reads a GLB's JSON chunk, and refuses one a board cannot draw as it is. */
|
|
120
|
+
function jsonOf(bytes) {
|
|
121
|
+
if (bytes.length > exports.GLB_MAX_BYTES)
|
|
122
|
+
throw new GlbRefusal('GLB_TOO_LARGE', `the file is ${bytes.length} bytes; the most kept is ${exports.GLB_MAX_BYTES}`, { max: Math.round(exports.GLB_MAX_BYTES / 1024 / 1024) });
|
|
123
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
124
|
+
if (bytes.length < 20 || view.getUint32(0, true) !== 0x46546c67)
|
|
125
|
+
throw new GlbRefusal('GLB_NOT_BINARY', 'not a binary glTF (.glb) file');
|
|
126
|
+
if (view.getUint32(4, true) !== 2)
|
|
127
|
+
throw new GlbRefusal('GLB_VERSION', `glTF ${view.getUint32(4, true)}; only glTF 2.0 is read`);
|
|
128
|
+
if (view.getUint32(8, true) !== bytes.length)
|
|
129
|
+
throw new GlbRefusal('GLB_UNREADABLE', 'the file is cut short or has bytes past its end');
|
|
130
|
+
const length = view.getUint32(12, true);
|
|
131
|
+
if (view.getUint32(16, true) !== 0x4e4f534a || 20 + length > bytes.length)
|
|
132
|
+
throw new GlbRefusal('GLB_UNREADABLE', 'its first chunk is not its JSON');
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + length)));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new GlbRefusal('GLB_UNREADABLE', 'its JSON chunk is not JSON');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function readGlb(bytes) {
|
|
141
|
+
const gltf = jsonOf(bytes);
|
|
142
|
+
if (String(gltf?.asset?.version ?? '').split('.')[0] !== '2')
|
|
143
|
+
throw new GlbRefusal('GLB_VERSION', `glTF ${gltf?.asset?.version}; only glTF 2.0 is read`);
|
|
144
|
+
const required = Array.isArray(gltf.extensionsRequired) ? gltf.extensionsRequired : [];
|
|
145
|
+
const undrawable = required.filter(e => !DRAWABLE_EXTENSIONS.has(e));
|
|
146
|
+
if (undrawable.length)
|
|
147
|
+
throw new GlbRefusal('GLB_EXTENSION', `a board cannot draw it yet: it needs ${undrawable.join(', ')}`, { extensions: undrawable.join(', ') });
|
|
148
|
+
/* Self-contained: one buffer, the file's own; no file beside it. */
|
|
149
|
+
if ((gltf.buffers ?? []).some((b) => b?.uri !== undefined))
|
|
150
|
+
throw new GlbRefusal('GLB_EXTERNAL', 'it reads a buffer from another file');
|
|
151
|
+
if ((gltf.images ?? []).some((i) => i?.uri !== undefined && !String(i.uri).startsWith('data:')))
|
|
152
|
+
throw new GlbRefusal('GLB_EXTERNAL', 'it reads a picture from another file');
|
|
153
|
+
const nodes = Array.isArray(gltf.nodes) ? gltf.nodes : [];
|
|
154
|
+
const meshes = Array.isArray(gltf.meshes) ? gltf.meshes : [];
|
|
155
|
+
const accessors = Array.isArray(gltf.accessors) ? gltf.accessors : [];
|
|
156
|
+
const parent = new Array(nodes.length).fill(null);
|
|
157
|
+
nodes.forEach((n, i) => (Array.isArray(n?.children) ? n.children : []).forEach((c) => (parent[c] = i)));
|
|
158
|
+
/* World matrices, from the roots of the scene shown (or every root, when none is named). */
|
|
159
|
+
const world = new Array(nodes.length);
|
|
160
|
+
const scene = gltf.scenes?.[gltf.scene ?? 0];
|
|
161
|
+
const roots = Array.isArray(scene?.nodes) ? scene.nodes : nodes.map((_, i) => i).filter(i => parent[i] === null);
|
|
162
|
+
const visit = (i, above) => {
|
|
163
|
+
if (world[i])
|
|
164
|
+
throw new GlbRefusal('GLB_UNREADABLE', `node ${i} is reached twice: the nodes are not a tree`);
|
|
165
|
+
world[i] = mul(above, local(nodes[i]));
|
|
166
|
+
for (const c of nodes[i]?.children ?? [])
|
|
167
|
+
visit(c, world[i]);
|
|
168
|
+
};
|
|
169
|
+
roots.forEach(i => visit(i, IDENTITY));
|
|
170
|
+
/* What one primitive costs and where it reaches, in its mesh's own space. */
|
|
171
|
+
const primitive = (p) => {
|
|
172
|
+
const position = accessors[p?.attributes?.POSITION];
|
|
173
|
+
if (!position || !Array.isArray(position.min) || !Array.isArray(position.max))
|
|
174
|
+
throw new GlbRefusal('GLB_UNREADABLE', 'a primitive has no POSITION with its min and max');
|
|
175
|
+
const count = p.indices !== undefined ? Number(accessors[p.indices]?.count) : Number(position.count);
|
|
176
|
+
const mode = p.mode ?? 4;
|
|
177
|
+
const triangles = mode === 4 ? Math.floor(count / 3) : mode === 5 || mode === 6 ? Math.max(count - 2, 0) : 0;
|
|
178
|
+
const [x0, y0, z0] = position.min.map(Number);
|
|
179
|
+
const [x1, y1, z1] = position.max.map(Number);
|
|
180
|
+
const corners = [x0, x1].flatMap(x => [y0, y1].flatMap(y => [z0, z1].map(z => [x, y, z])));
|
|
181
|
+
return { triangles, corners };
|
|
182
|
+
};
|
|
183
|
+
const drawn = (i) => (nodes[i]?.mesh !== undefined ? meshes[nodes[i].mesh]?.primitives ?? [] : []);
|
|
184
|
+
const subtree = (i) => [i, ...(nodes[i]?.children ?? []).flatMap((c) => subtree(c))];
|
|
185
|
+
const whole = empty();
|
|
186
|
+
let triangles = 0, primitives = 0;
|
|
187
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
188
|
+
if (!world[i])
|
|
189
|
+
continue;
|
|
190
|
+
for (const p of drawn(i)) {
|
|
191
|
+
const one = primitive(p);
|
|
192
|
+
triangles += one.triangles;
|
|
193
|
+
primitives++;
|
|
194
|
+
for (const c of one.corners)
|
|
195
|
+
grow(whole, apply(world[i], c));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!primitives)
|
|
199
|
+
throw new GlbRefusal('GLB_NO_MESH', 'it holds no mesh to draw');
|
|
200
|
+
const listed = nodes.map((n, i) => {
|
|
201
|
+
const frame = world[i] ? frameOf(world[i]) : undefined;
|
|
202
|
+
const below = subtree(i).filter(k => world[k]);
|
|
203
|
+
const box = empty();
|
|
204
|
+
let t = 0, p = 0;
|
|
205
|
+
for (const k of below)
|
|
206
|
+
for (const prim of drawn(k)) {
|
|
207
|
+
const one = primitive(prim);
|
|
208
|
+
t += one.triangles;
|
|
209
|
+
p++;
|
|
210
|
+
if (frame)
|
|
211
|
+
for (const c of one.corners)
|
|
212
|
+
grow(box, apply(mul(frame.inverse, world[k]), c));
|
|
213
|
+
}
|
|
214
|
+
const position = frame ? { x: round(frame.translation[0] * TO_MM), y: round(frame.translation[1] * TO_MM), z: round(frame.translation[2] * TO_MM) } : { x: 0, y: 0, z: 0 };
|
|
215
|
+
return {
|
|
216
|
+
index: i,
|
|
217
|
+
name: typeof n?.name === 'string' ? n.name : `node ${i}`,
|
|
218
|
+
parent: parent[i],
|
|
219
|
+
bounds: p && frame ? inMm(box) : null,
|
|
220
|
+
triangles: t,
|
|
221
|
+
primitives: p,
|
|
222
|
+
position,
|
|
223
|
+
rotation: frame ? eulerOfRotation(frame.rotation) : { x: 0, y: 0, z: 0 },
|
|
224
|
+
...(!p ? { refused: 'no-mesh' } : !frame ? { refused: 'sheared' } : {})
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return { bounds: inMm(whole), triangles, primitives, unit: 'm', nodes: listed, extensions: Array.isArray(gltf.extensionsUsed) ? gltf.extensionsUsed : [] };
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=figure-glb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-glb.js","sourceRoot":"","sources":["../../../server/service/figure/figure-glb.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a GLB file holds, read on upload (ADR-0129 decision 6): whether it is a glTF 2.0 binary the board can draw, and\n * the numbers a mesh part carries -- its bounds in mm, its triangles, its primitives -- for the whole file and for each\n * node of it. The kernel never reads the file (figure-model `mesh@1`, contract §13); these numbers come from here.\n *\n * Nothing is decoded but the JSON chunk. glTF requires every POSITION accessor to carry its min and max, so the bounds\n * are those corners carried through the node transforms; a compressed buffer (Meshopt) keeps them too.\n *\n * A node's frame is its place in the file: its world translation and rotation, in the file's root frame. A part made\n * from a node stands there (its position and rotation), turns about the node's origin, and is drawn with the node's\n * world scale kept in the drawing. A node whose world transform shears or mirrors has no such frame and is not offered.\n */\n\nimport { FIGURE_MESH_CONTENT_TYPE, FIGURE_MESH_MAX_BYTES } from '@hatiolab/figure-model'\n\n/* The package contract's rule (figure-model): the same type and limit a board server holds an import to. */\nexport const GLB_MIMETYPE = FIGURE_MESH_CONTENT_TYPE\n/** The largest file kept, in bytes. A board downloads it for every figure that draws it. */\nexport const GLB_MAX_BYTES = FIGURE_MESH_MAX_BYTES\n\n/** The extensions a board can draw: Meshopt's decoder ships with the loader. Draco and KTX2 wait (ADR-0129 Q4). */\nconst DRAWABLE_EXTENSIONS = new Set(['EXT_meshopt_compression', 'KHR_mesh_quantization', 'KHR_materials_emissive_strength', 'KHR_texture_transform'])\n\nexport type Box = { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } }\n\nexport interface GlbNode {\n /** Its place in the file's node list: what a mesh part names it by. */\n index: number\n name: string\n parent: number | null\n /** The node's subtree, in its own frame, in mm; none when it holds no mesh. */\n bounds: Box | null\n triangles: number\n primitives: number\n /** Where the node stands in the file's root frame: what a part made from it is placed at. */\n position: { x: number; y: number; z: number }\n rotation: { x: number; y: number; z: number }\n /** Why it cannot be a part, when it cannot. */\n refused?: 'no-mesh' | 'sheared'\n}\n\nexport interface GlbSummary {\n /** The whole file in its root frame, in mm. */\n bounds: Box\n triangles: number\n primitives: number\n unit: 'm'\n nodes: GlbNode[]\n extensions: string[]\n}\n\nexport class GlbRefusal extends Error {\n constructor(\n readonly code:\n | 'GLB_NOT_BINARY'\n | 'GLB_VERSION'\n | 'GLB_UNREADABLE'\n | 'GLB_EXTERNAL'\n | 'GLB_EXTENSION'\n | 'GLB_NO_MESH'\n | 'GLB_TOO_LARGE',\n message: string,\n /** What a screen says it with: the extensions it needs, the most bytes kept. */\n readonly params: Record<string, string | number> = {}\n ) {\n super(message)\n this.name = 'GlbRefusal'\n }\n}\n\ntype M4 = number[] // column-major, as glTF writes it\nconst IDENTITY: M4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\nconst mul = (a: M4, b: M4): M4 => {\n const out = new Array(16).fill(0)\n for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r]! * b[c * 4 + k]!\n return out\n}\nconst apply = (m: M4, p: [number, number, number]): [number, number, number] => [\n m[0]! * p[0] + m[4]! * p[1] + m[8]! * p[2] + m[12]!,\n m[1]! * p[0] + m[5]! * p[1] + m[9]! * p[2] + m[13]!,\n m[2]! * p[0] + m[6]! * p[1] + m[10]! * p[2] + m[14]!\n]\nfunction local(node: any): M4 {\n if (Array.isArray(node.matrix) && node.matrix.length === 16) return node.matrix.map(Number)\n const [tx, ty, tz] = node.translation ?? [0, 0, 0]\n const [qx, qy, qz, qw] = node.rotation ?? [0, 0, 0, 1]\n const [sx, sy, sz] = node.scale ?? [1, 1, 1]\n const r = [\n 1 - 2 * (qy * qy + qz * qz), 2 * (qx * qy + qz * qw), 2 * (qx * qz - qy * qw),\n 2 * (qx * qy - qz * qw), 1 - 2 * (qx * qx + qz * qz), 2 * (qy * qz + qx * qw),\n 2 * (qx * qz + qy * qw), 2 * (qy * qz - qx * qw), 1 - 2 * (qx * qx + qy * qy)\n ]\n return [r[0]! * sx, r[1]! * sx, r[2]! * sx, 0, r[3]! * sy, r[4]! * sy, r[5]! * sy, 0, r[6]! * sz, r[7]! * sz, r[8]! * sz, 0, tx, ty, tz, 1]\n}\n\n/**\n * A world matrix as translation and rotation, the scale left in the drawing. Undefined when its axes are not square to\n * each other (a shear) or it mirrors: there is no frame to stand a part in.\n */\nfunction frameOf(m: M4): { rotation: number[][]; translation: [number, number, number]; inverse: M4 } | undefined {\n const cols = [0, 1, 2].map(c => [m[c * 4]!, m[c * 4 + 1]!, m[c * 4 + 2]!])\n const len = cols.map(c => Math.hypot(...c))\n if (len.some(l => !(l > 1e-12))) return undefined\n const unit = cols.map((c, i) => c.map(v => v / len[i]!))\n const dot = (a: number[], b: number[]) => a[0]! * b[0]! + a[1]! * b[1]! + a[2]! * b[2]!\n if (Math.abs(dot(unit[0]!, unit[1]!)) > 1e-6 || Math.abs(dot(unit[0]!, unit[2]!)) > 1e-6 || Math.abs(dot(unit[1]!, unit[2]!)) > 1e-6) return undefined\n const [a, b, c] = unit as [number[], number[], number[]]\n const det = a[0]! * (b[1]! * c[2]! - b[2]! * c[1]!) - b[0]! * (a[1]! * c[2]! - a[2]! * c[1]!) + c[0]! * (a[1]! * b[2]! - a[2]! * b[1]!)\n if (det < 0) return undefined\n /* rotation rows: R[r][c] = unit[c][r] */\n const rotation = [0, 1, 2].map(r => [0, 1, 2].map(col => unit[col]![r]!))\n const translation: [number, number, number] = [m[12]!, m[13]!, m[14]!]\n /* The inverse of the rigid part: Rᵀ and −Rᵀt. */\n const inv: M4 = [...IDENTITY]\n for (let r = 0; r < 3; r++) for (let col = 0; col < 3; col++) inv[col * 4 + r] = rotation[col]![r]!\n for (let r = 0; r < 3; r++) inv[12 + r] = -(rotation[0]![r]! * translation[0] + rotation[1]![r]! * translation[1] + rotation[2]![r]! * translation[2])\n return { rotation, translation, inverse: inv }\n}\n\n/** A rotation as degrees about X, Y and Z in the order `rigid@1` turns them (Rx · Ry · Rz). */\nexport function eulerOfRotation(r: number[][]): { x: number; y: number; z: number } {\n const deg = (a: number) => {\n const d = Math.round(((a * 180) / Math.PI) * 1e6) / 1e6\n return d === 0 ? 0 : d\n }\n const sy = Math.max(-1, Math.min(1, r[0]![2]!))\n const y = Math.asin(sy)\n if (Math.abs(sy) < 1 - 1e-9) return { x: deg(Math.atan2(-r[1]![2]!, r[2]![2]!)), y: deg(y), z: deg(Math.atan2(-r[0]![1]!, r[0]![0]!)) }\n /* About Y by a quarter turn: X and Z turn about one axis and only their sum counts; Z is taken as 0. */\n return { x: deg(Math.atan2(r[2]![1]!, r[1]![1]!)), y: deg(y), z: 0 }\n}\n\nconst TO_MM = 1000\nconst empty = (): Box => ({ min: { x: Infinity, y: Infinity, z: Infinity }, max: { x: -Infinity, y: -Infinity, z: -Infinity } })\nconst grow = (box: Box, p: [number, number, number]) => {\n box.min = { x: Math.min(box.min.x, p[0]), y: Math.min(box.min.y, p[1]), z: Math.min(box.min.z, p[2]) }\n box.max = { x: Math.max(box.max.x, p[0]), y: Math.max(box.max.y, p[1]), z: Math.max(box.max.z, p[2]) }\n}\nconst round = (v: number) => {\n const r = Math.round(v * 1e6) / 1e6\n return r === 0 ? 0 : r\n}\nconst inMm = (box: Box): Box => ({\n min: { x: round(box.min.x * TO_MM), y: round(box.min.y * TO_MM), z: round(box.min.z * TO_MM) },\n max: { x: round(box.max.x * TO_MM), y: round(box.max.y * TO_MM), z: round(box.max.z * TO_MM) }\n})\n\n/** Reads a GLB's JSON chunk, and refuses one a board cannot draw as it is. */\nfunction jsonOf(bytes: Uint8Array): any {\n if (bytes.length > GLB_MAX_BYTES) throw new GlbRefusal('GLB_TOO_LARGE', `the file is ${bytes.length} bytes; the most kept is ${GLB_MAX_BYTES}`, { max: Math.round(GLB_MAX_BYTES / 1024 / 1024) })\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n if (bytes.length < 20 || view.getUint32(0, true) !== 0x46546c67) throw new GlbRefusal('GLB_NOT_BINARY', 'not a binary glTF (.glb) file')\n if (view.getUint32(4, true) !== 2) throw new GlbRefusal('GLB_VERSION', `glTF ${view.getUint32(4, true)}; only glTF 2.0 is read`)\n if (view.getUint32(8, true) !== bytes.length) throw new GlbRefusal('GLB_UNREADABLE', 'the file is cut short or has bytes past its end')\n const length = view.getUint32(12, true)\n if (view.getUint32(16, true) !== 0x4e4f534a || 20 + length > bytes.length) throw new GlbRefusal('GLB_UNREADABLE', 'its first chunk is not its JSON')\n try {\n return JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + length)))\n } catch {\n throw new GlbRefusal('GLB_UNREADABLE', 'its JSON chunk is not JSON')\n }\n}\n\nexport function readGlb(bytes: Uint8Array): GlbSummary {\n const gltf = jsonOf(bytes)\n if (String(gltf?.asset?.version ?? '').split('.')[0] !== '2') throw new GlbRefusal('GLB_VERSION', `glTF ${gltf?.asset?.version}; only glTF 2.0 is read`)\n const required: string[] = Array.isArray(gltf.extensionsRequired) ? gltf.extensionsRequired : []\n const undrawable = required.filter(e => !DRAWABLE_EXTENSIONS.has(e))\n if (undrawable.length) throw new GlbRefusal('GLB_EXTENSION', `a board cannot draw it yet: it needs ${undrawable.join(', ')}`, { extensions: undrawable.join(', ') })\n /* Self-contained: one buffer, the file's own; no file beside it. */\n if ((gltf.buffers ?? []).some((b: any) => b?.uri !== undefined)) throw new GlbRefusal('GLB_EXTERNAL', 'it reads a buffer from another file')\n if ((gltf.images ?? []).some((i: any) => i?.uri !== undefined && !String(i.uri).startsWith('data:')))\n throw new GlbRefusal('GLB_EXTERNAL', 'it reads a picture from another file')\n\n const nodes: any[] = Array.isArray(gltf.nodes) ? gltf.nodes : []\n const meshes: any[] = Array.isArray(gltf.meshes) ? gltf.meshes : []\n const accessors: any[] = Array.isArray(gltf.accessors) ? gltf.accessors : []\n const parent = new Array<number | null>(nodes.length).fill(null)\n nodes.forEach((n, i) => (Array.isArray(n?.children) ? n.children : []).forEach((c: number) => (parent[c] = i)))\n\n /* World matrices, from the roots of the scene shown (or every root, when none is named). */\n const world = new Array<M4 | undefined>(nodes.length)\n const scene = gltf.scenes?.[gltf.scene ?? 0]\n const roots: number[] = Array.isArray(scene?.nodes) ? scene.nodes : nodes.map((_, i) => i).filter(i => parent[i] === null)\n const visit = (i: number, above: M4) => {\n if (world[i]) throw new GlbRefusal('GLB_UNREADABLE', `node ${i} is reached twice: the nodes are not a tree`)\n world[i] = mul(above, local(nodes[i]))\n for (const c of nodes[i]?.children ?? []) visit(c, world[i]!)\n }\n roots.forEach(i => visit(i, IDENTITY))\n\n /* What one primitive costs and where it reaches, in its mesh's own space. */\n const primitive = (p: any) => {\n const position = accessors[p?.attributes?.POSITION]\n if (!position || !Array.isArray(position.min) || !Array.isArray(position.max))\n throw new GlbRefusal('GLB_UNREADABLE', 'a primitive has no POSITION with its min and max')\n const count = p.indices !== undefined ? Number(accessors[p.indices]?.count) : Number(position.count)\n const mode = p.mode ?? 4\n const triangles = mode === 4 ? Math.floor(count / 3) : mode === 5 || mode === 6 ? Math.max(count - 2, 0) : 0\n const [x0, y0, z0] = position.min.map(Number)\n const [x1, y1, z1] = position.max.map(Number)\n const corners: Array<[number, number, number]> = [x0, x1].flatMap(x => [y0, y1].flatMap(y => [z0, z1].map(z => [x, y, z] as [number, number, number])))\n return { triangles, corners }\n }\n\n const drawn = (i: number) => (nodes[i]?.mesh !== undefined ? meshes[nodes[i].mesh]?.primitives ?? [] : [])\n const subtree = (i: number): number[] => [i, ...(nodes[i]?.children ?? []).flatMap((c: number) => subtree(c))]\n\n const whole = empty()\n let triangles = 0,\n primitives = 0\n for (let i = 0; i < nodes.length; i++) {\n if (!world[i]) continue\n for (const p of drawn(i)) {\n const one = primitive(p)\n triangles += one.triangles\n primitives++\n for (const c of one.corners) grow(whole, apply(world[i]!, c))\n }\n }\n if (!primitives) throw new GlbRefusal('GLB_NO_MESH', 'it holds no mesh to draw')\n\n const listed: GlbNode[] = nodes.map((n, i) => {\n const frame = world[i] ? frameOf(world[i]!) : undefined\n const below = subtree(i).filter(k => world[k])\n const box = empty()\n let t = 0,\n p = 0\n for (const k of below)\n for (const prim of drawn(k)) {\n const one = primitive(prim)\n t += one.triangles\n p++\n if (frame) for (const c of one.corners) grow(box, apply(mul(frame.inverse, world[k]!), c))\n }\n const position = frame ? { x: round(frame.translation[0] * TO_MM), y: round(frame.translation[1] * TO_MM), z: round(frame.translation[2] * TO_MM) } : { x: 0, y: 0, z: 0 }\n return {\n index: i,\n name: typeof n?.name === 'string' ? n.name : `node ${i}`,\n parent: parent[i]!,\n bounds: p && frame ? inMm(box) : null,\n triangles: t,\n primitives: p,\n position,\n rotation: frame ? eulerOfRotation(frame.rotation) : { x: 0, y: 0, z: 0 },\n ...(!p ? { refused: 'no-mesh' as const } : !frame ? { refused: 'sheared' as const } : {})\n }\n })\n\n return { bounds: inMm(whole), triangles, primitives, unit: 'm', nodes: listed, extensions: Array.isArray(gltf.extensionsUsed) ? gltf.extensionsUsed : [] }\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Attachment } from '@things-factory/attachment-base';
|
|
2
|
+
import { type GlbSummary } from './figure-glb.js';
|
|
3
|
+
import { type ThumbnailFiles } from './figure-thumbnail-store.js';
|
|
4
|
+
export declare const MESH_REF_TYPE = "figure-mesh";
|
|
5
|
+
type Tx = {
|
|
6
|
+
getRepository: (target: any) => any;
|
|
7
|
+
};
|
|
8
|
+
/** The attachment holding this file in one of these domains -- the asker's first, then its parent's -- or undefined. */
|
|
9
|
+
export declare function meshAttachmentOf(tx: Tx, domainIds: readonly string[], hash: string): Promise<Attachment | undefined>;
|
|
10
|
+
/** Keeps a GLB and gives its hash and what it holds. Refused, by the reader's code, when a board cannot draw it. */
|
|
11
|
+
export declare function keepMesh(tx: Tx, domain: {
|
|
12
|
+
id: string;
|
|
13
|
+
}, user: unknown, bytes: Buffer, files?: ThumbnailFiles): Promise<{
|
|
14
|
+
hash: string;
|
|
15
|
+
summary: GlbSummary;
|
|
16
|
+
}>;
|
|
17
|
+
/** What a kept file holds, read again from its bytes: the modeller asks when it reopens a figure. */
|
|
18
|
+
export declare function meshSummaryOf(tx: Tx, domain: {
|
|
19
|
+
id: string;
|
|
20
|
+
parentId?: string | null;
|
|
21
|
+
}, hash: string, files?: ThumbnailFiles): Promise<GlbSummary | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* A source may name only files this domain holds (or its parent, whose figures it reads): a figure that names a file
|
|
24
|
+
* nobody kept cannot be drawn anywhere, and is refused when it is saved, not when a board finds out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireMeshFiles(tx: Tx, domain: {
|
|
27
|
+
id: string;
|
|
28
|
+
parentId?: string | null;
|
|
29
|
+
}, source: string): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MESH_REF_TYPE = void 0;
|
|
4
|
+
exports.meshAttachmentOf = meshAttachmentOf;
|
|
5
|
+
exports.keepMesh = keepMesh;
|
|
6
|
+
exports.meshSummaryOf = meshSummaryOf;
|
|
7
|
+
exports.requireMeshFiles = requireMeshFiles;
|
|
8
|
+
const typeorm_1 = require("typeorm");
|
|
9
|
+
const attachment_base_1 = require("@things-factory/attachment-base");
|
|
10
|
+
const figure_model_1 = require("@hatiolab/figure-model");
|
|
11
|
+
const shell_1 = require("@things-factory/shell");
|
|
12
|
+
const figure_glb_js_1 = require("./figure-glb.js");
|
|
13
|
+
const figure_thumbnail_store_js_1 = require("./figure-thumbnail-store.js");
|
|
14
|
+
/*
|
|
15
|
+
* A mesh part's file (ADR-0129 decision 6), kept as a thumbnail is (ADR-0104 decision 6): by attachment-base's storage,
|
|
16
|
+
* one attachment per (domain, content), refType `figure-mesh`, refBy the SHA-256 of the bytes. A figure's source names
|
|
17
|
+
* it by that hash and nothing else; the bytes are served only through `/figure-mesh/:hash`, behind the figure query
|
|
18
|
+
* permission.
|
|
19
|
+
*
|
|
20
|
+
* A file is read before it is kept (`readGlb`): one a board cannot draw is refused by name, never kept under a hash.
|
|
21
|
+
* The file is written before the row that names it, as a picture is: a file cannot be rolled back.
|
|
22
|
+
*/
|
|
23
|
+
exports.MESH_REF_TYPE = 'figure-mesh';
|
|
24
|
+
/** The attachment holding this file in one of these domains -- the asker's first, then its parent's -- or undefined. */
|
|
25
|
+
async function meshAttachmentOf(tx, domainIds, hash) {
|
|
26
|
+
const found = await tx.getRepository(attachment_base_1.Attachment).find({
|
|
27
|
+
where: { domain: { id: (0, typeorm_1.In)([...domainIds]) }, refType: exports.MESH_REF_TYPE, refBy: hash },
|
|
28
|
+
relations: { domain: true }
|
|
29
|
+
});
|
|
30
|
+
return domainIds.map(id => found.find(a => a.domain?.id === id)).find(Boolean);
|
|
31
|
+
}
|
|
32
|
+
/** Keeps a GLB and gives its hash and what it holds. Refused, by the reader's code, when a board cannot draw it. */
|
|
33
|
+
async function keepMesh(tx, domain, user, bytes, files = (0, figure_thumbnail_store_js_1.thumbnailFiles)()) {
|
|
34
|
+
const summary = (0, figure_glb_js_1.readGlb)(bytes);
|
|
35
|
+
const hash = (0, figure_thumbnail_store_js_1.sha256Of)(bytes);
|
|
36
|
+
if (await meshAttachmentOf(tx, [domain.id], hash))
|
|
37
|
+
return { hash, summary };
|
|
38
|
+
const name = `${hash}.glb`;
|
|
39
|
+
/* The file first: it cannot be rolled back. The row that names it comes after, in the caller's transaction. */
|
|
40
|
+
const put = await files.put(bytes, figure_glb_js_1.GLB_MIMETYPE, name, { state: { domain, user, tx } });
|
|
41
|
+
await tx.getRepository(attachment_base_1.Attachment).save({
|
|
42
|
+
domain,
|
|
43
|
+
creator: user,
|
|
44
|
+
updater: user,
|
|
45
|
+
id: put.id,
|
|
46
|
+
name,
|
|
47
|
+
mimetype: figure_glb_js_1.GLB_MIMETYPE,
|
|
48
|
+
encoding: 'binary',
|
|
49
|
+
refType: exports.MESH_REF_TYPE,
|
|
50
|
+
refBy: hash,
|
|
51
|
+
category: 'model',
|
|
52
|
+
size: String(put.size),
|
|
53
|
+
path: put.path,
|
|
54
|
+
...(put.contents ? { contents: put.contents } : {})
|
|
55
|
+
});
|
|
56
|
+
return { hash, summary };
|
|
57
|
+
}
|
|
58
|
+
/** What a kept file holds, read again from its bytes: the modeller asks when it reopens a figure. */
|
|
59
|
+
async function meshSummaryOf(tx, domain, hash, files = (0, figure_thumbnail_store_js_1.thumbnailFiles)()) {
|
|
60
|
+
const kept = await meshAttachmentOf(tx, [domain.id, domain.parentId].filter(Boolean), hash);
|
|
61
|
+
return kept ? (0, figure_glb_js_1.readGlb)(await files.read(kept.path)) : undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A source may name only files this domain holds (or its parent, whose figures it reads): a figure that names a file
|
|
65
|
+
* nobody kept cannot be drawn anywhere, and is refused when it is saved, not when a board finds out.
|
|
66
|
+
*/
|
|
67
|
+
async function requireMeshFiles(tx, domain, source) {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(source);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return; /* the format check refuses it, with its own words */
|
|
74
|
+
}
|
|
75
|
+
const ids = [domain.id, domain.parentId].filter(Boolean);
|
|
76
|
+
for (const hash of (0, figure_model_1.figureFilesOf)(parsed))
|
|
77
|
+
if (!(await meshAttachmentOf(tx, ids, hash)))
|
|
78
|
+
throw new shell_1.Refusal('figure-mesh-missing', { hash }, 'the figure names a mesh file this domain does not hold: {hash}. Upload the file again.');
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=figure-mesh-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-mesh-store.js","sourceRoot":"","sources":["../../../server/service/figure/figure-mesh-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { Attachment } from '@things-factory/attachment-base'\nimport { figureFilesOf } from '@hatiolab/figure-model'\nimport { Refusal } from '@things-factory/shell'\n\nimport { GLB_MIMETYPE, readGlb, type GlbSummary } from './figure-glb.js'\nimport { sha256Of, thumbnailFiles, type ThumbnailFiles } from './figure-thumbnail-store.js'\n\n/*\n * A mesh part's file (ADR-0129 decision 6), kept as a thumbnail is (ADR-0104 decision 6): by attachment-base's storage,\n * one attachment per (domain, content), refType `figure-mesh`, refBy the SHA-256 of the bytes. A figure's source names\n * it by that hash and nothing else; the bytes are served only through `/figure-mesh/:hash`, behind the figure query\n * permission.\n *\n * A file is read before it is kept (`readGlb`): one a board cannot draw is refused by name, never kept under a hash.\n * The file is written before the row that names it, as a picture is: a file cannot be rolled back.\n */\n\nexport const MESH_REF_TYPE = 'figure-mesh'\n\ntype Tx = { getRepository: (target: any) => any }\n\n/** The attachment holding this file in one of these domains -- the asker's first, then its parent's -- or undefined. */\nexport async function meshAttachmentOf(tx: Tx, domainIds: readonly string[], hash: string): Promise<Attachment | undefined> {\n const found: Attachment[] = await tx.getRepository(Attachment).find({\n where: { domain: { id: In([...domainIds]) }, refType: MESH_REF_TYPE, refBy: hash },\n relations: { domain: true }\n })\n return domainIds.map(id => found.find(a => a.domain?.id === id)).find(Boolean)\n}\n\n/** Keeps a GLB and gives its hash and what it holds. Refused, by the reader's code, when a board cannot draw it. */\nexport async function keepMesh(\n tx: Tx,\n domain: { id: string },\n user: unknown,\n bytes: Buffer,\n files: ThumbnailFiles = thumbnailFiles()\n): Promise<{ hash: string; summary: GlbSummary }> {\n const summary = readGlb(bytes)\n const hash = sha256Of(bytes)\n if (await meshAttachmentOf(tx, [domain.id], hash)) return { hash, summary }\n const name = `${hash}.glb`\n /* The file first: it cannot be rolled back. The row that names it comes after, in the caller's transaction. */\n const put = await files.put(bytes, GLB_MIMETYPE, name, { state: { domain, user, tx } })\n await tx.getRepository(Attachment).save({\n domain,\n creator: user,\n updater: user,\n id: put.id,\n name,\n mimetype: GLB_MIMETYPE,\n encoding: 'binary',\n refType: MESH_REF_TYPE,\n refBy: hash,\n category: 'model',\n size: String(put.size),\n path: put.path,\n ...(put.contents ? { contents: put.contents } : {})\n })\n return { hash, summary }\n}\n\n/** What a kept file holds, read again from its bytes: the modeller asks when it reopens a figure. */\nexport async function meshSummaryOf(\n tx: Tx,\n domain: { id: string; parentId?: string | null },\n hash: string,\n files: ThumbnailFiles = thumbnailFiles()\n): Promise<GlbSummary | undefined> {\n const kept = await meshAttachmentOf(tx, [domain.id, domain.parentId].filter(Boolean) as string[], hash)\n return kept ? readGlb(await files.read(kept.path!)) : undefined\n}\n\n/**\n * A source may name only files this domain holds (or its parent, whose figures it reads): a figure that names a file\n * nobody kept cannot be drawn anywhere, and is refused when it is saved, not when a board finds out.\n */\nexport async function requireMeshFiles(tx: Tx, domain: { id: string; parentId?: string | null }, source: string): Promise<void> {\n let parsed: unknown\n try {\n parsed = JSON.parse(source)\n } catch {\n return /* the format check refuses it, with its own words */\n }\n const ids = [domain.id, domain.parentId].filter(Boolean) as string[]\n for (const hash of figureFilesOf(parsed))\n if (!(await meshAttachmentOf(tx, ids, hash)))\n throw new Refusal('figure-mesh-missing', { hash }, 'the figure names a mesh file this domain does not hold: {hash}. Upload the file again.')\n}\n"]}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Figure } from './figure.js';
|
|
2
2
|
import { FigurePatch, FigureRevertResult, FigureSaveResult, NewFigure } from './figure-type.js';
|
|
3
3
|
import { FigureProposal, ProposeRequest } from './figure-propose-type.js';
|
|
4
|
+
import { FigureMeshUpload } from './figure-type.js';
|
|
5
|
+
import type { FileUpload } from 'graphql-upload/GraphQLUpload.js';
|
|
4
6
|
/** Display names by language: a language code to a name of 1-128 characters. Refused whole, with the bad entry named. */
|
|
5
7
|
export declare function assertNames(names: unknown): asserts names is Record<string, string>;
|
|
6
8
|
export declare class FigureMutation {
|
|
7
9
|
saveFigure(source: string, context: ResolverContext, id?: string, expectedSource?: string, creationId?: string, thumbnail?: string, type?: string, proposalKeys?: string[]): Promise<Figure>;
|
|
8
10
|
private writeSource;
|
|
11
|
+
/**
|
|
12
|
+
* A GLB for a mesh part (ADR-0129 decision 6): read, then kept by its hash. Answers the hash and what the file holds --
|
|
13
|
+
* its bounds in mm, its triangles and primitives, and its nodes -- which the modeller turns into a part. A file a board
|
|
14
|
+
* cannot draw is refused by name and not kept.
|
|
15
|
+
*/
|
|
16
|
+
uploadFigureMesh(file: FileUpload, context: ResolverContext): Promise<FigureMeshUpload>;
|
|
9
17
|
createFigure(figure: NewFigure, context: ResolverContext): Promise<FigureSaveResult>;
|
|
10
18
|
updateFigure(id: string, patch: FigurePatch, context: ResolverContext): Promise<FigureSaveResult>;
|
|
11
19
|
/**
|