@meshy-ai/cli 0.2.0 → 0.3.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/.env.example +8 -2
- package/README.md +342 -108
- package/dist/client/endpoints/animation-catalog.d.ts +37 -0
- package/dist/client/endpoints/animation-catalog.d.ts.map +1 -0
- package/dist/client/endpoints/animation-catalog.js +55 -0
- package/dist/client/endpoints/animation-catalog.js.map +1 -0
- package/dist/client/endpoints/balance.d.ts +5 -0
- package/dist/client/endpoints/balance.d.ts.map +1 -1
- package/dist/client/endpoints/balance.js +5 -1
- package/dist/client/endpoints/balance.js.map +1 -1
- package/dist/client/endpoints/base.d.ts +41 -7
- package/dist/client/endpoints/base.d.ts.map +1 -1
- package/dist/client/endpoints/base.js +61 -40
- package/dist/client/endpoints/base.js.map +1 -1
- package/dist/client/endpoints/showcases.d.ts +31 -0
- package/dist/client/endpoints/showcases.d.ts.map +1 -0
- package/dist/client/endpoints/showcases.js +49 -0
- package/dist/client/endpoints/showcases.js.map +1 -0
- package/dist/client/errors.d.ts +1 -0
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +1 -1
- package/dist/client/errors.js.map +1 -1
- package/dist/client/index.d.ts +47 -38
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +89 -120
- package/dist/client/index.js.map +1 -1
- package/dist/client/resource-registry.d.ts +97 -0
- package/dist/client/resource-registry.d.ts.map +1 -0
- package/dist/client/resource-registry.js +157 -0
- package/dist/client/resource-registry.js.map +1 -0
- package/dist/client/transport.d.ts +99 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/transport.js +320 -0
- package/dist/client/transport.js.map +1 -0
- package/dist/client/types.d.ts +17 -6
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +28 -6
- package/dist/client/types.js.map +1 -1
- package/dist/cmd/animation-catalog.d.ts +26 -0
- package/dist/cmd/animation-catalog.d.ts.map +1 -0
- package/dist/cmd/animation-catalog.js +80 -0
- package/dist/cmd/animation-catalog.js.map +1 -0
- package/dist/cmd/api.d.ts +3 -2
- package/dist/cmd/api.d.ts.map +1 -1
- package/dist/cmd/api.js +22 -8
- package/dist/cmd/api.js.map +1 -1
- package/dist/cmd/auth.d.ts.map +1 -1
- package/dist/cmd/auth.js +5 -0
- package/dist/cmd/auth.js.map +1 -1
- package/dist/cmd/balance.d.ts.map +1 -1
- package/dist/cmd/balance.js +10 -6
- package/dist/cmd/balance.js.map +1 -1
- package/dist/cmd/creative-lab.d.ts +26 -0
- package/dist/cmd/creative-lab.d.ts.map +1 -0
- package/dist/cmd/creative-lab.js +256 -0
- package/dist/cmd/creative-lab.js.map +1 -0
- package/dist/cmd/delete.d.ts.map +1 -1
- package/dist/cmd/delete.js +6 -7
- package/dist/cmd/delete.js.map +1 -1
- package/dist/cmd/doctor.d.ts +23 -0
- package/dist/cmd/doctor.d.ts.map +1 -0
- package/dist/cmd/doctor.js +61 -0
- package/dist/cmd/doctor.js.map +1 -0
- package/dist/cmd/download.d.ts +17 -0
- package/dist/cmd/download.d.ts.map +1 -0
- package/dist/cmd/download.js +436 -0
- package/dist/cmd/download.js.map +1 -0
- package/dist/cmd/inspect.d.ts +63 -0
- package/dist/cmd/inspect.d.ts.map +1 -0
- package/dist/cmd/inspect.js +179 -0
- package/dist/cmd/inspect.js.map +1 -0
- package/dist/cmd/make.d.ts +13 -3
- package/dist/cmd/make.d.ts.map +1 -1
- package/dist/cmd/make.js +240 -36
- package/dist/cmd/make.js.map +1 -1
- package/dist/cmd/mesh.d.ts +13 -0
- package/dist/cmd/mesh.d.ts.map +1 -0
- package/dist/cmd/mesh.js +67 -0
- package/dist/cmd/mesh.js.map +1 -0
- package/dist/cmd/project.d.ts +16 -0
- package/dist/cmd/project.d.ts.map +1 -0
- package/dist/cmd/project.js +136 -0
- package/dist/cmd/project.js.map +1 -0
- package/dist/cmd/resources.d.ts +5 -0
- package/dist/cmd/resources.d.ts.map +1 -1
- package/dist/cmd/resources.js +38 -10
- package/dist/cmd/resources.js.map +1 -1
- package/dist/cmd/showcases.d.ts +10 -0
- package/dist/cmd/showcases.d.ts.map +1 -0
- package/dist/cmd/showcases.js +63 -0
- package/dist/cmd/showcases.js.map +1 -0
- package/dist/cmd/slicer.d.ts +12 -0
- package/dist/cmd/slicer.d.ts.map +1 -0
- package/dist/cmd/slicer.js +46 -0
- package/dist/cmd/slicer.js.map +1 -0
- package/dist/cmd/uv-unwrap.d.ts +16 -0
- package/dist/cmd/uv-unwrap.d.ts.map +1 -0
- package/dist/cmd/uv-unwrap.js +52 -0
- package/dist/cmd/uv-unwrap.js.map +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/artifacts.d.ts +97 -0
- package/dist/internal/artifacts.d.ts.map +1 -0
- package/dist/internal/artifacts.js +280 -0
- package/dist/internal/artifacts.js.map +1 -0
- package/dist/internal/atomic-file.d.ts +39 -0
- package/dist/internal/atomic-file.d.ts.map +1 -0
- package/dist/internal/atomic-file.js +160 -0
- package/dist/internal/atomic-file.js.map +1 -0
- package/dist/internal/command-helpers.d.ts +46 -0
- package/dist/internal/command-helpers.d.ts.map +1 -0
- package/dist/internal/command-helpers.js +61 -0
- package/dist/internal/command-helpers.js.map +1 -0
- package/dist/internal/config.d.ts +48 -5
- package/dist/internal/config.d.ts.map +1 -1
- package/dist/internal/config.js +95 -6
- package/dist/internal/config.js.map +1 -1
- package/dist/internal/context.d.ts +25 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/context.js +37 -0
- package/dist/internal/context.js.map +1 -0
- package/dist/internal/credentials.d.ts +11 -0
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +2 -0
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/doctor.d.ts +94 -0
- package/dist/internal/doctor.d.ts.map +1 -0
- package/dist/internal/doctor.js +280 -0
- package/dist/internal/doctor.js.map +1 -0
- package/dist/internal/download.d.ts +147 -13
- package/dist/internal/download.d.ts.map +1 -1
- package/dist/internal/download.js +735 -142
- package/dist/internal/download.js.map +1 -1
- package/dist/internal/env-file.d.ts +29 -0
- package/dist/internal/env-file.d.ts.map +1 -0
- package/dist/internal/env-file.js +95 -0
- package/dist/internal/env-file.js.map +1 -0
- package/dist/internal/errors.d.ts +74 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +173 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/file-input.d.ts +52 -12
- package/dist/internal/file-input.d.ts.map +1 -1
- package/dist/internal/file-input.js +162 -33
- package/dist/internal/file-input.js.map +1 -1
- package/dist/internal/global-options.d.ts +6 -4
- package/dist/internal/global-options.d.ts.map +1 -1
- package/dist/internal/global-options.js +19 -4
- package/dist/internal/global-options.js.map +1 -1
- package/dist/internal/inspect.d.ts +67 -0
- package/dist/internal/inspect.d.ts.map +1 -0
- package/dist/internal/inspect.js +111 -0
- package/dist/internal/inspect.js.map +1 -0
- package/dist/internal/lock.d.ts +19 -0
- package/dist/internal/lock.d.ts.map +1 -0
- package/dist/internal/lock.js +66 -0
- package/dist/internal/lock.js.map +1 -0
- package/dist/internal/material-links.d.ts +115 -0
- package/dist/internal/material-links.d.ts.map +1 -0
- package/dist/internal/material-links.js +577 -0
- package/dist/internal/material-links.js.map +1 -0
- package/dist/internal/obj-transform.d.ts +115 -0
- package/dist/internal/obj-transform.d.ts.map +1 -0
- package/dist/internal/obj-transform.js +649 -0
- package/dist/internal/obj-transform.js.map +1 -0
- package/dist/internal/operation-store.d.ts +123 -0
- package/dist/internal/operation-store.d.ts.map +1 -0
- package/dist/internal/operation-store.js +229 -0
- package/dist/internal/operation-store.js.map +1 -0
- package/dist/internal/output.d.ts +20 -2
- package/dist/internal/output.d.ts.map +1 -1
- package/dist/internal/output.js +32 -4
- package/dist/internal/output.js.map +1 -1
- package/dist/internal/paths.d.ts +89 -0
- package/dist/internal/paths.d.ts.map +1 -0
- package/dist/internal/paths.js +168 -0
- package/dist/internal/paths.js.map +1 -0
- package/dist/internal/payload.d.ts +15 -0
- package/dist/internal/payload.d.ts.map +1 -1
- package/dist/internal/payload.js +50 -3
- package/dist/internal/payload.js.map +1 -1
- package/dist/internal/poll.d.ts +30 -4
- package/dist/internal/poll.d.ts.map +1 -1
- package/dist/internal/poll.js +99 -16
- package/dist/internal/poll.js.map +1 -1
- package/dist/internal/project-store.d.ts +197 -0
- package/dist/internal/project-store.d.ts.map +1 -0
- package/dist/internal/project-store.js +467 -0
- package/dist/internal/project-store.js.map +1 -0
- package/dist/internal/result.d.ts +50 -0
- package/dist/internal/result.d.ts.map +1 -0
- package/dist/internal/result.js +41 -0
- package/dist/internal/result.js.map +1 -0
- package/dist/internal/runtime.d.ts +35 -5
- package/dist/internal/runtime.d.ts.map +1 -1
- package/dist/internal/runtime.js +68 -18
- package/dist/internal/runtime.js.map +1 -1
- package/dist/internal/slicers.d.ts +117 -0
- package/dist/internal/slicers.d.ts.map +1 -0
- package/dist/internal/slicers.js +314 -0
- package/dist/internal/slicers.js.map +1 -0
- package/dist/internal/stream.d.ts +58 -0
- package/dist/internal/stream.d.ts.map +1 -0
- package/dist/internal/stream.js +280 -0
- package/dist/internal/stream.js.map +1 -0
- package/dist/internal/task-command.d.ts +128 -11
- package/dist/internal/task-command.d.ts.map +1 -1
- package/dist/internal/task-command.js +991 -111
- package/dist/internal/task-command.js.map +1 -1
- package/dist/internal/task-view.d.ts +56 -0
- package/dist/internal/task-view.d.ts.map +1 -0
- package/dist/internal/task-view.js +95 -0
- package/dist/internal/task-view.js.map +1 -0
- package/dist/internal/update-notifier.d.ts +5 -1
- package/dist/internal/update-notifier.d.ts.map +1 -1
- package/dist/internal/update-notifier.js +7 -3
- package/dist/internal/update-notifier.js.map +1 -1
- package/dist/root.d.ts +10 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +75 -10
- package/dist/root.js.map +1 -1
- package/package.json +3 -2
- package/skills/meshy-cli/SKILL.md +140 -59
- package/dist/client/endpoints/analyze-printability.d.ts +0 -5
- package/dist/client/endpoints/analyze-printability.d.ts.map +0 -1
- package/dist/client/endpoints/analyze-printability.js +0 -7
- package/dist/client/endpoints/analyze-printability.js.map +0 -1
- package/dist/client/endpoints/animate.d.ts +0 -6
- package/dist/client/endpoints/animate.d.ts.map +0 -1
- package/dist/client/endpoints/animate.js +0 -8
- package/dist/client/endpoints/animate.js.map +0 -1
- package/dist/client/endpoints/convert.d.ts +0 -5
- package/dist/client/endpoints/convert.d.ts.map +0 -1
- package/dist/client/endpoints/convert.js +0 -7
- package/dist/client/endpoints/convert.js.map +0 -1
- package/dist/client/endpoints/image-to-3d.d.ts +0 -5
- package/dist/client/endpoints/image-to-3d.d.ts.map +0 -1
- package/dist/client/endpoints/image-to-3d.js +0 -7
- package/dist/client/endpoints/image-to-3d.js.map +0 -1
- package/dist/client/endpoints/image-to-image.d.ts +0 -5
- package/dist/client/endpoints/image-to-image.d.ts.map +0 -1
- package/dist/client/endpoints/image-to-image.js +0 -7
- package/dist/client/endpoints/image-to-image.js.map +0 -1
- package/dist/client/endpoints/multi-color-print.d.ts +0 -5
- package/dist/client/endpoints/multi-color-print.d.ts.map +0 -1
- package/dist/client/endpoints/multi-color-print.js +0 -7
- package/dist/client/endpoints/multi-color-print.js.map +0 -1
- package/dist/client/endpoints/multi-image-to-3d.d.ts +0 -5
- package/dist/client/endpoints/multi-image-to-3d.d.ts.map +0 -1
- package/dist/client/endpoints/multi-image-to-3d.js +0 -7
- package/dist/client/endpoints/multi-image-to-3d.js.map +0 -1
- package/dist/client/endpoints/remesh.d.ts +0 -5
- package/dist/client/endpoints/remesh.d.ts.map +0 -1
- package/dist/client/endpoints/remesh.js +0 -7
- package/dist/client/endpoints/remesh.js.map +0 -1
- package/dist/client/endpoints/repair-printability.d.ts +0 -5
- package/dist/client/endpoints/repair-printability.d.ts.map +0 -1
- package/dist/client/endpoints/repair-printability.js +0 -7
- package/dist/client/endpoints/repair-printability.js.map +0 -1
- package/dist/client/endpoints/resize.d.ts +0 -5
- package/dist/client/endpoints/resize.d.ts.map +0 -1
- package/dist/client/endpoints/resize.js +0 -7
- package/dist/client/endpoints/resize.js.map +0 -1
- package/dist/client/endpoints/retexture.d.ts +0 -5
- package/dist/client/endpoints/retexture.d.ts.map +0 -1
- package/dist/client/endpoints/retexture.js +0 -7
- package/dist/client/endpoints/retexture.js.map +0 -1
- package/dist/client/endpoints/rigging.d.ts +0 -6
- package/dist/client/endpoints/rigging.d.ts.map +0 -1
- package/dist/client/endpoints/rigging.js +0 -8
- package/dist/client/endpoints/rigging.js.map +0 -1
- package/dist/client/endpoints/text-to-3d.d.ts +0 -6
- package/dist/client/endpoints/text-to-3d.d.ts.map +0 -1
- package/dist/client/endpoints/text-to-3d.js +0 -8
- package/dist/client/endpoints/text-to-3d.js.map +0 -1
- package/dist/client/endpoints/text-to-image.d.ts +0 -5
- package/dist/client/endpoints/text-to-image.d.ts.map +0 -1
- package/dist/client/endpoints/text-to-image.js +0 -7
- package/dist/client/endpoints/text-to-image.js.map +0 -1
- package/dist/client/endpoints/text-to-motion.d.ts +0 -6
- package/dist/client/endpoints/text-to-motion.d.ts.map +0 -1
- package/dist/client/endpoints/text-to-motion.js +0 -8
- package/dist/client/endpoints/text-to-motion.js.map +0 -1
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OBJ print preparation — the legacy `fix_obj.py` transform as a streaming module.
|
|
3
|
+
*
|
|
4
|
+
* Meshy exports Y-up OBJ files in arbitrary units; slicers want Z-up
|
|
5
|
+
* millimetres with the model standing on the build plate. The transform is
|
|
6
|
+
*
|
|
7
|
+
* R(x, y, z) = (x, -z, y) Y-up → Z-up; determinant +1, so face winding is kept
|
|
8
|
+
* v' = s·R(v) + (tx, ty, tz) s = height_mm / (zmax − zmin) of the rotated box,
|
|
9
|
+
* XY centred on the origin, minZ on the plate (0)
|
|
10
|
+
* n' = R(n) normals only rotate — no scale, no translation
|
|
11
|
+
*
|
|
12
|
+
* Every other line (vt, f, o, g, s, usemtl, mtllib, comments, blanks) is copied
|
|
13
|
+
* verbatim, so topology, UVs and material bindings cannot drift. Extra fields on
|
|
14
|
+
* a `v` line (vertex colours, w) are kept verbatim after the three coordinates.
|
|
15
|
+
*
|
|
16
|
+
* Two streaming passes keep memory constant regardless of file size: pass 1 folds
|
|
17
|
+
* every vertex into a bounding box, pass 2 rewrites lines through a write stream
|
|
18
|
+
* into a temp file in the target directory. The temp file is published only after
|
|
19
|
+
* the whole rewrite succeeded — nothing is written when validation fails, an
|
|
20
|
+
* existing output is never overwritten, and the input is replaced only with
|
|
21
|
+
* `inPlace` (temp file + rename on the same filesystem).
|
|
22
|
+
*
|
|
23
|
+
* Material dependencies (`mtllib`, and `map_*` textures inside those MTLs) are
|
|
24
|
+
* resolved relative to the input and only inside the input's directory tree.
|
|
25
|
+
* When the output lands in another directory they are copied alongside it; a
|
|
26
|
+
* missing or escaping dependency is a validation error there unless the caller
|
|
27
|
+
* asked for `geometryOnly`, because a "success" with a silently broken material
|
|
28
|
+
* is worse than a refusal. Every copy target is proven to lie inside the write
|
|
29
|
+
* root (`root`, i.e. the --workspace, else the output directory) on its *real*
|
|
30
|
+
* path before any directory is created or byte copied, and again right before
|
|
31
|
+
* publication — a symlinked `materials/` inside the target cannot redirect a
|
|
32
|
+
* copy outside. No env, no credentials, no network.
|
|
33
|
+
*/
|
|
34
|
+
import { chmodSync, closeSync, createReadStream, createWriteStream, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, } from "node:fs";
|
|
35
|
+
import { once } from "node:events";
|
|
36
|
+
import { basename, dirname, extname, join, posix, relative, resolve as resolvePath, win32 } from "node:path";
|
|
37
|
+
import { finished } from "node:stream/promises";
|
|
38
|
+
import { StringDecoder } from "node:string_decoder";
|
|
39
|
+
import { copyFilePublished, publishTempFile, tempPathFor } from "./atomic-file.js";
|
|
40
|
+
import { CliError, UsageError } from "./errors.js";
|
|
41
|
+
import { freezeRoot, isInside, realpathLenient, resolveWithinRoot } from "./paths.js";
|
|
42
|
+
import { warning } from "./result.js";
|
|
43
|
+
export const OBJ_ROTATION = "(x,y,z)->(x,-z,y)";
|
|
44
|
+
export const DEFAULT_HEIGHT_MM = 75;
|
|
45
|
+
/** Same engineering ceiling as endpoint-contracts.json `limits.obj_bytes`. */
|
|
46
|
+
export const DEFAULT_MAX_OBJ_BYTES = 2 * 1024 * 1024 * 1024;
|
|
47
|
+
/** A rotated height at or below this is treated as degenerate (flat model). */
|
|
48
|
+
export const MIN_MODEL_HEIGHT = 1e-6;
|
|
49
|
+
const READ_CHUNK_BYTES = 256 * 1024;
|
|
50
|
+
/** A "line" longer than this is not a text OBJ; refusing keeps the carry buffer bounded. */
|
|
51
|
+
const MAX_LINE_CHARS = 16 * 1024 * 1024;
|
|
52
|
+
const WS = /\s+/;
|
|
53
|
+
const NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
54
|
+
const TEXTURE_KEY_RE = /^(?:map_[A-Za-z0-9_]+|bump|disp|decal|refl|norm)$/i;
|
|
55
|
+
const URL_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
56
|
+
export function rotateYUpToZUp(v) {
|
|
57
|
+
return [v[0], -v[2], v[1]];
|
|
58
|
+
}
|
|
59
|
+
/** Default output beside the input: `<stem>.print.obj`. */
|
|
60
|
+
export function defaultOutputPath(input) {
|
|
61
|
+
const ext = extname(input);
|
|
62
|
+
const stem = ext ? basename(input, ext) : basename(input);
|
|
63
|
+
return join(dirname(input), `${stem}.print.obj`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Six fixed decimals (error ≤ 5e-7, inside the 1e-5 mm oracle tolerance) with
|
|
67
|
+
* trailing zeros trimmed so a unit box does not become a wall of `.000000`.
|
|
68
|
+
* `-0` never appears: a slicer gains nothing from a signed zero.
|
|
69
|
+
*/
|
|
70
|
+
export function formatObjNumber(n) {
|
|
71
|
+
if (n === 0)
|
|
72
|
+
return "0";
|
|
73
|
+
let s = n.toFixed(6);
|
|
74
|
+
if (s.includes("."))
|
|
75
|
+
s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
76
|
+
return s === "-0" ? "0" : s;
|
|
77
|
+
}
|
|
78
|
+
function validation(message, extra = {}) {
|
|
79
|
+
return new CliError({ code: "validation", message, hint: extra.hint, details: extra.details });
|
|
80
|
+
}
|
|
81
|
+
function localIo(message, cause) {
|
|
82
|
+
return new CliError({ code: "local_io", message, cause });
|
|
83
|
+
}
|
|
84
|
+
function errno(err) {
|
|
85
|
+
return err?.code;
|
|
86
|
+
}
|
|
87
|
+
function removeQuietly(path) {
|
|
88
|
+
try {
|
|
89
|
+
unlinkSync(path);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* already gone */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function noNegZero(n) {
|
|
96
|
+
return n === 0 ? 0 : n;
|
|
97
|
+
}
|
|
98
|
+
function parseCoord(token) {
|
|
99
|
+
if (token === undefined || !NUMBER_RE.test(token))
|
|
100
|
+
return null;
|
|
101
|
+
const n = Number(token);
|
|
102
|
+
return Number.isFinite(n) ? n : null;
|
|
103
|
+
}
|
|
104
|
+
/** First whitespace-delimited token (the OBJ keyword) of an already-trimmed line. */
|
|
105
|
+
function keywordOf(t) {
|
|
106
|
+
const m = WS.exec(t);
|
|
107
|
+
return m ? t.slice(0, m.index) : t;
|
|
108
|
+
}
|
|
109
|
+
function parseVec3(tokens, lineNo, kind) {
|
|
110
|
+
if (tokens.length < 4) {
|
|
111
|
+
throw validation(`${kind} line ${lineNo} has ${tokens.length - 1} coordinate(s); expected 3`);
|
|
112
|
+
}
|
|
113
|
+
const out = [];
|
|
114
|
+
for (let i = 1; i <= 3; i++) {
|
|
115
|
+
const n = parseCoord(tokens[i]);
|
|
116
|
+
if (n === null) {
|
|
117
|
+
throw validation(`non-finite or unparseable coordinate '${tokens[i] ?? ""}' in ${kind} line ${lineNo}`);
|
|
118
|
+
}
|
|
119
|
+
out.push(n);
|
|
120
|
+
}
|
|
121
|
+
return [out[0], out[1], out[2]];
|
|
122
|
+
}
|
|
123
|
+
/** Negate a numeric token textually so the input's precision survives the rotation. */
|
|
124
|
+
function negateToken(token) {
|
|
125
|
+
const n = Number(token);
|
|
126
|
+
if (n === 0)
|
|
127
|
+
return "0";
|
|
128
|
+
if (token.startsWith("-"))
|
|
129
|
+
return token.slice(1);
|
|
130
|
+
if (token.startsWith("+"))
|
|
131
|
+
return `-${token.slice(1)}`;
|
|
132
|
+
return `-${token}`;
|
|
133
|
+
}
|
|
134
|
+
function emptyBbox() {
|
|
135
|
+
return { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity], seen: false };
|
|
136
|
+
}
|
|
137
|
+
function fold(b, v) {
|
|
138
|
+
for (let i = 0; i < 3; i++) {
|
|
139
|
+
const c = v[i];
|
|
140
|
+
if (c < b.min[i])
|
|
141
|
+
b.min[i] = c;
|
|
142
|
+
if (c > b.max[i])
|
|
143
|
+
b.max[i] = c;
|
|
144
|
+
}
|
|
145
|
+
b.seen = true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Stream `path` line by line. Each line's own terminator ("\n", "\r\n", or ""
|
|
149
|
+
* for a final unterminated line) is handed to the sink so pass 2 can preserve
|
|
150
|
+
* the input's style. UTF-8 is decoded across chunk boundaries. `afterChunk`
|
|
151
|
+
* runs after every read chunk so a writer can apply backpressure.
|
|
152
|
+
*/
|
|
153
|
+
async function forEachLine(path, onLine, afterChunk) {
|
|
154
|
+
const stream = createReadStream(path, { highWaterMark: READ_CHUNK_BYTES });
|
|
155
|
+
const decoder = new StringDecoder("utf8");
|
|
156
|
+
let carry = "";
|
|
157
|
+
let lineNo = 0;
|
|
158
|
+
try {
|
|
159
|
+
for await (const chunk of stream) {
|
|
160
|
+
carry += decoder.write(chunk);
|
|
161
|
+
let start = 0;
|
|
162
|
+
let nl;
|
|
163
|
+
while ((nl = carry.indexOf("\n", start)) !== -1) {
|
|
164
|
+
let end = nl;
|
|
165
|
+
let eol = "\n";
|
|
166
|
+
if (end > start && carry.charCodeAt(end - 1) === 13) {
|
|
167
|
+
end -= 1;
|
|
168
|
+
eol = "\r\n";
|
|
169
|
+
}
|
|
170
|
+
lineNo += 1;
|
|
171
|
+
onLine(carry.slice(start, end), eol, lineNo);
|
|
172
|
+
start = nl + 1;
|
|
173
|
+
}
|
|
174
|
+
carry = start > 0 ? carry.slice(start) : carry;
|
|
175
|
+
if (carry.length > MAX_LINE_CHARS) {
|
|
176
|
+
throw validation(`line ${lineNo + 1} exceeds ${MAX_LINE_CHARS} characters; not a text OBJ file`);
|
|
177
|
+
}
|
|
178
|
+
if (afterChunk)
|
|
179
|
+
await afterChunk();
|
|
180
|
+
}
|
|
181
|
+
carry += decoder.end();
|
|
182
|
+
if (carry.length > 0) {
|
|
183
|
+
lineNo += 1;
|
|
184
|
+
onLine(carry, "", lineNo);
|
|
185
|
+
if (afterChunk)
|
|
186
|
+
await afterChunk();
|
|
187
|
+
}
|
|
188
|
+
return lineNo;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
stream.destroy();
|
|
192
|
+
if (err instanceof CliError || err instanceof UsageError)
|
|
193
|
+
throw err;
|
|
194
|
+
throw localIo(`failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Pass 1: bounding boxes, counts and material references. Constant memory. */
|
|
198
|
+
async function scanObj(input) {
|
|
199
|
+
const scan = { raw: emptyBbox(), rotated: emptyBbox(), vertices: 0, normals: 0, uvs: 0, faces: 0, lines: 0, mtllib: [] };
|
|
200
|
+
const seenMtl = new Set();
|
|
201
|
+
scan.lines = await forEachLine(input, (text, _eol, lineNo) => {
|
|
202
|
+
const t = text.trim();
|
|
203
|
+
if (t.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
switch (keywordOf(t)) {
|
|
206
|
+
case "v": {
|
|
207
|
+
const v = parseVec3(t.split(WS), lineNo, "v");
|
|
208
|
+
fold(scan.raw, v);
|
|
209
|
+
fold(scan.rotated, rotateYUpToZUp(v));
|
|
210
|
+
scan.vertices += 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "vn":
|
|
214
|
+
parseVec3(t.split(WS), lineNo, "vn");
|
|
215
|
+
scan.normals += 1;
|
|
216
|
+
break;
|
|
217
|
+
case "vt":
|
|
218
|
+
scan.uvs += 1;
|
|
219
|
+
break;
|
|
220
|
+
case "f":
|
|
221
|
+
scan.faces += 1;
|
|
222
|
+
break;
|
|
223
|
+
case "mtllib": {
|
|
224
|
+
const ref = t.slice("mtllib".length).trim();
|
|
225
|
+
if (ref && !seenMtl.has(ref)) {
|
|
226
|
+
seenMtl.add(ref);
|
|
227
|
+
scan.mtllib.push(ref);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}, null);
|
|
235
|
+
return scan;
|
|
236
|
+
}
|
|
237
|
+
function rewriteLine(text, lineNo, tf) {
|
|
238
|
+
const t = text.trim();
|
|
239
|
+
if (t.length === 0)
|
|
240
|
+
return text;
|
|
241
|
+
const key = keywordOf(t);
|
|
242
|
+
if (key === "v") {
|
|
243
|
+
const tokens = t.split(WS);
|
|
244
|
+
const [rx, ry, rz] = rotateYUpToZUp(parseVec3(tokens, lineNo, "v"));
|
|
245
|
+
const parts = [
|
|
246
|
+
"v",
|
|
247
|
+
formatObjNumber(rx * tf.scale + tf.translation[0]),
|
|
248
|
+
formatObjNumber(ry * tf.scale + tf.translation[1]),
|
|
249
|
+
formatObjNumber(rz * tf.scale + tf.translation[2]),
|
|
250
|
+
];
|
|
251
|
+
for (let i = 4; i < tokens.length; i++)
|
|
252
|
+
parts.push(tokens[i]);
|
|
253
|
+
return parts.join(" ");
|
|
254
|
+
}
|
|
255
|
+
if (key === "vn") {
|
|
256
|
+
const tokens = t.split(WS);
|
|
257
|
+
parseVec3(tokens, lineNo, "vn");
|
|
258
|
+
// (nx, ny, nz) → (nx, -nz, ny), reordering the original tokens so no precision is lost.
|
|
259
|
+
const parts = ["vn", tokens[1], negateToken(tokens[3]), tokens[2]];
|
|
260
|
+
for (let i = 4; i < tokens.length; i++)
|
|
261
|
+
parts.push(tokens[i]);
|
|
262
|
+
return parts.join(" ");
|
|
263
|
+
}
|
|
264
|
+
return text;
|
|
265
|
+
}
|
|
266
|
+
/** Pass 2: rewrite into `tmp`, streaming with backpressure. */
|
|
267
|
+
async function rewriteObj(input, tmp, tf) {
|
|
268
|
+
const out = createWriteStream(tmp, { flags: "wx" });
|
|
269
|
+
let writeError = null;
|
|
270
|
+
out.on("error", (err) => {
|
|
271
|
+
writeError = err;
|
|
272
|
+
});
|
|
273
|
+
let buf = "";
|
|
274
|
+
const flush = async () => {
|
|
275
|
+
if (writeError)
|
|
276
|
+
throw writeError;
|
|
277
|
+
if (buf.length === 0)
|
|
278
|
+
return;
|
|
279
|
+
const chunk = buf;
|
|
280
|
+
buf = "";
|
|
281
|
+
if (!out.write(chunk))
|
|
282
|
+
await once(out, "drain");
|
|
283
|
+
if (writeError)
|
|
284
|
+
throw writeError;
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
await forEachLine(input, (text, eol, lineNo) => {
|
|
288
|
+
buf += rewriteLine(text, lineNo, tf) + eol;
|
|
289
|
+
}, flush);
|
|
290
|
+
await flush();
|
|
291
|
+
out.end();
|
|
292
|
+
await finished(out);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
out.destroy();
|
|
296
|
+
if (err instanceof CliError || err instanceof UsageError)
|
|
297
|
+
throw err;
|
|
298
|
+
throw localIo(`failed to write ${tmp}: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Resolve a material reference relative to `baseDir`, admitting only regular
|
|
303
|
+
* files whose real path stays inside `rootDir`. URLs, absolute paths (POSIX or
|
|
304
|
+
* Windows) and anything escaping the tree are never read.
|
|
305
|
+
*/
|
|
306
|
+
function locateDependency(ref, baseDir, rootReal) {
|
|
307
|
+
if (!ref || URL_RE.test(ref) || /^data:/i.test(ref))
|
|
308
|
+
return { kind: "outside" };
|
|
309
|
+
const candidates = [ref];
|
|
310
|
+
if (ref.includes("\\"))
|
|
311
|
+
candidates.push(ref.replaceAll("\\", "/"));
|
|
312
|
+
let sawMissing = false;
|
|
313
|
+
for (const candidate of candidates) {
|
|
314
|
+
if (posix.isAbsolute(candidate) || win32.isAbsolute(candidate))
|
|
315
|
+
return { kind: "outside" };
|
|
316
|
+
const abs = resolvePath(baseDir, candidate);
|
|
317
|
+
let real;
|
|
318
|
+
try {
|
|
319
|
+
real = realpathLenient(abs);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
sawMissing = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!isInside(rootReal, real))
|
|
326
|
+
return { kind: "outside" };
|
|
327
|
+
let st = null;
|
|
328
|
+
try {
|
|
329
|
+
st = statSync(real);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (errno(err) !== "ENOENT" && errno(err) !== "ENOTDIR")
|
|
333
|
+
throw localIo(`cannot access ${abs}: ${err.message}`, err);
|
|
334
|
+
}
|
|
335
|
+
if (st?.isFile())
|
|
336
|
+
return { kind: "found", path: abs, real };
|
|
337
|
+
sawMissing = true;
|
|
338
|
+
}
|
|
339
|
+
return sawMissing ? { kind: "missing" } : { kind: "outside" };
|
|
340
|
+
}
|
|
341
|
+
/** Texture references (`map_Kd [-options] file`) inside an MTL. */
|
|
342
|
+
export function textureReferencesInMtl(text) {
|
|
343
|
+
const refs = [];
|
|
344
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
345
|
+
const line = rawLine.trim();
|
|
346
|
+
if (!line || line.startsWith("#"))
|
|
347
|
+
continue;
|
|
348
|
+
const key = keywordOf(line);
|
|
349
|
+
if (!TEXTURE_KEY_RE.test(key))
|
|
350
|
+
continue;
|
|
351
|
+
const rest = line.slice(key.length).trim();
|
|
352
|
+
if (!rest)
|
|
353
|
+
continue;
|
|
354
|
+
// Options (`-s 1 1 1`, `-bm 0.5`) precede the filename; without options the
|
|
355
|
+
// whole remainder is the name so spaces in filenames survive.
|
|
356
|
+
const ref = rest.startsWith("-") ? rest.split(WS).at(-1) : rest;
|
|
357
|
+
if (!refs.includes(ref))
|
|
358
|
+
refs.push(ref);
|
|
359
|
+
}
|
|
360
|
+
return refs;
|
|
361
|
+
}
|
|
362
|
+
function planMaterials(refs, inputDir, outputDir, wantCopies) {
|
|
363
|
+
const plan = { copies: [], missing: [], warnings: [] };
|
|
364
|
+
const inputReal = realpathLenient(inputDir);
|
|
365
|
+
const targets = new Set();
|
|
366
|
+
const note = (ref, located, origin) => {
|
|
367
|
+
plan.missing.push(ref);
|
|
368
|
+
if (located.kind === "outside") {
|
|
369
|
+
plan.warnings.push(warning("material_dependency_outside_input_dir", `${origin} references '${ref}', which is a URL, an absolute path or escapes the input's directory; it was not read or copied`));
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
plan.warnings.push(warning("material_dependency_missing", `${origin} references '${ref}', which does not exist next to the input`));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const schedule = (ref, real) => {
|
|
376
|
+
if (!wantCopies)
|
|
377
|
+
return;
|
|
378
|
+
const target = join(outputDir, relative(inputReal, real));
|
|
379
|
+
if (targets.has(target))
|
|
380
|
+
return;
|
|
381
|
+
targets.add(target);
|
|
382
|
+
plan.copies.push({ ref, source: real, target, real: target });
|
|
383
|
+
};
|
|
384
|
+
for (const written of refs) {
|
|
385
|
+
// `mtllib a.mtl b.mtl` lists several files, but names with spaces exist in
|
|
386
|
+
// the wild: try the whole remainder first, split only when it is not a file.
|
|
387
|
+
let names = [written];
|
|
388
|
+
if (WS.test(written) && locateDependency(written, inputDir, inputReal).kind !== "found") {
|
|
389
|
+
names = written.split(WS);
|
|
390
|
+
}
|
|
391
|
+
for (const name of names) {
|
|
392
|
+
const located = locateDependency(name, inputDir, inputReal);
|
|
393
|
+
if (located.kind !== "found") {
|
|
394
|
+
note(name, located, "mtllib");
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
schedule(name, located.real);
|
|
398
|
+
let mtlText;
|
|
399
|
+
try {
|
|
400
|
+
mtlText = readFileSync(located.real, "utf8");
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
throw localIo(`failed to read material file ${located.path}: ${err.message}`, err);
|
|
404
|
+
}
|
|
405
|
+
const mtlDir = dirname(located.real);
|
|
406
|
+
for (const texRef of textureReferencesInMtl(mtlText)) {
|
|
407
|
+
const tex = locateDependency(texRef, mtlDir, inputReal);
|
|
408
|
+
if (tex.kind !== "found") {
|
|
409
|
+
note(texRef, tex, `${name}`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
schedule(texRef, tex.real);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return plan;
|
|
417
|
+
}
|
|
418
|
+
function sameContent(a, b) {
|
|
419
|
+
const sa = statSync(a);
|
|
420
|
+
const sb = statSync(b);
|
|
421
|
+
if (sa.size !== sb.size)
|
|
422
|
+
return false;
|
|
423
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
424
|
+
}
|
|
425
|
+
/** Copy a dependency without overwriting; an identical file already there counts as present. */
|
|
426
|
+
function copyDependency(source, target) {
|
|
427
|
+
let existing = null;
|
|
428
|
+
try {
|
|
429
|
+
existing = lstatSync(target);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
if (errno(err) !== "ENOENT" && errno(err) !== "ENOTDIR")
|
|
433
|
+
throw localIo(`cannot access ${target}: ${err.message}`, err);
|
|
434
|
+
}
|
|
435
|
+
if (existing) {
|
|
436
|
+
if (existing.isFile() && sameContent(source, target))
|
|
437
|
+
return "present";
|
|
438
|
+
throw localIo(`refusing to overwrite ${target}: a different file already exists there (needed for material dependency ${basename(source)})`);
|
|
439
|
+
}
|
|
440
|
+
copyFilePublished(source, target);
|
|
441
|
+
return "copied";
|
|
442
|
+
}
|
|
443
|
+
function statInput(input) {
|
|
444
|
+
let lst;
|
|
445
|
+
try {
|
|
446
|
+
lst = lstatSync(input);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
if (errno(err) === "ENOENT" || errno(err) === "ENOTDIR") {
|
|
450
|
+
throw new CliError({ code: "not_found", message: `input file not found: ${input}` });
|
|
451
|
+
}
|
|
452
|
+
throw localIo(`cannot access ${input}: ${err.message}`, err);
|
|
453
|
+
}
|
|
454
|
+
let st;
|
|
455
|
+
try {
|
|
456
|
+
st = statSync(input);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
if (errno(err) === "ENOENT" || errno(err) === "ENOTDIR") {
|
|
460
|
+
throw new CliError({ code: "not_found", message: `input file not found: ${input} (dangling symbolic link)` });
|
|
461
|
+
}
|
|
462
|
+
throw localIo(`cannot access ${input}: ${err.message}`, err);
|
|
463
|
+
}
|
|
464
|
+
if (!st.isFile())
|
|
465
|
+
throw validation(`input is not a regular file: ${input}`);
|
|
466
|
+
return { stat: st, lstat: lst };
|
|
467
|
+
}
|
|
468
|
+
function isDirectory(path) {
|
|
469
|
+
try {
|
|
470
|
+
return statSync(path).isDirectory();
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function exists(path) {
|
|
477
|
+
try {
|
|
478
|
+
lstatSync(path);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Rotate, scale and ground an OBJ for printing. See the module comment for the
|
|
487
|
+
* maths and the file contract. Throws CliError (`validation` / `local_io` /
|
|
488
|
+
* `not_found`) or UsageError; on any failure no output file exists.
|
|
489
|
+
*/
|
|
490
|
+
export async function prepareObjForPrint(inputPath, opts = {}) {
|
|
491
|
+
const heightMm = opts.heightMm ?? DEFAULT_HEIGHT_MM;
|
|
492
|
+
if (typeof heightMm !== "number" || !Number.isFinite(heightMm) || heightMm <= 0) {
|
|
493
|
+
throw validation(`--height-mm must be a finite number greater than 0 (got ${String(heightMm)})`);
|
|
494
|
+
}
|
|
495
|
+
const inPlace = opts.inPlace === true;
|
|
496
|
+
const geometryOnly = opts.geometryOnly === true;
|
|
497
|
+
if (opts.outputPath !== undefined && inPlace) {
|
|
498
|
+
throw new UsageError("--output/-o and --in-place are mutually exclusive: pick a new file or replace the input, not both");
|
|
499
|
+
}
|
|
500
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_OBJ_BYTES;
|
|
501
|
+
const input = resolvePath(inputPath);
|
|
502
|
+
const { stat, lstat } = statInput(input);
|
|
503
|
+
if (stat.size === 0)
|
|
504
|
+
throw validation(`input is empty: ${input}`);
|
|
505
|
+
if (stat.size > maxBytes) {
|
|
506
|
+
throw localIo(`input is ${stat.size} bytes, above the ${maxBytes}-byte limit for local OBJ processing: ${input}`);
|
|
507
|
+
}
|
|
508
|
+
if (inPlace && lstat.isSymbolicLink()) {
|
|
509
|
+
throw localIo(`refusing to replace ${input} in place: it is a symbolic link`);
|
|
510
|
+
}
|
|
511
|
+
const inputDir = dirname(input);
|
|
512
|
+
let output;
|
|
513
|
+
if (inPlace) {
|
|
514
|
+
output = input;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
output = opts.outputPath !== undefined ? resolvePath(opts.outputPath) : defaultOutputPath(input);
|
|
518
|
+
if (isDirectory(output))
|
|
519
|
+
output = join(output, basename(defaultOutputPath(input)));
|
|
520
|
+
if (exists(output)) {
|
|
521
|
+
throw new CliError({
|
|
522
|
+
code: "local_io",
|
|
523
|
+
message: `refusing to overwrite existing file: ${output} (choose another --output path, or pass --in-place to replace the input itself)`,
|
|
524
|
+
recovery: { action: "choose_path", automatic: false },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const outputDir = dirname(output);
|
|
529
|
+
// Pass 1 — validate and measure. Nothing has been written yet.
|
|
530
|
+
const scan = await scanObj(input);
|
|
531
|
+
if (scan.vertices === 0)
|
|
532
|
+
throw validation(`no vertex (v) lines found in ${input}`);
|
|
533
|
+
const rot = scan.rotated;
|
|
534
|
+
const height = rot.max[2] - rot.min[2];
|
|
535
|
+
if (!(height > MIN_MODEL_HEIGHT)) {
|
|
536
|
+
throw validation(`degenerate model: rotated height is ${height} (≤ ${MIN_MODEL_HEIGHT}); every vertex shares the same up-axis value, so no scale can be derived`);
|
|
537
|
+
}
|
|
538
|
+
const scale = heightMm / height;
|
|
539
|
+
const translation = [
|
|
540
|
+
noNegZero((-(rot.min[0] + rot.max[0]) / 2) * scale),
|
|
541
|
+
noNegZero((-(rot.min[1] + rot.max[1]) / 2) * scale),
|
|
542
|
+
noNegZero(-rot.min[2] * scale),
|
|
543
|
+
];
|
|
544
|
+
const tf = { scale, translation };
|
|
545
|
+
// Materials — decided before any write so a broken binding is refused up front.
|
|
546
|
+
const crossDir = !inPlace && realpathLenient(outputDir) !== realpathLenient(inputDir);
|
|
547
|
+
const wantCopies = crossDir && !geometryOnly;
|
|
548
|
+
const plan = planMaterials(scan.mtllib, inputDir, outputDir, wantCopies);
|
|
549
|
+
const warnings = [...plan.warnings];
|
|
550
|
+
// Every path written by this run must resolve inside the write root: the
|
|
551
|
+
// output itself and each dependency copy, checked on real paths before any
|
|
552
|
+
// directory is created. A `materials/` symlink pointing elsewhere fails here.
|
|
553
|
+
const writeRoot = opts.root === undefined ? freezeRoot(outputDir, { label: "output directory" }) : typeof opts.root === "string" ? freezeRoot(opts.root, { label: "--workspace" }) : opts.root;
|
|
554
|
+
if (!inPlace)
|
|
555
|
+
resolveWithinRoot(output, writeRoot, { label: "output" });
|
|
556
|
+
for (const copy of plan.copies) {
|
|
557
|
+
copy.real = resolveWithinRoot(copy.target, writeRoot, { label: `material dependency target for '${copy.ref}'` }).path;
|
|
558
|
+
}
|
|
559
|
+
if (wantCopies && plan.missing.length > 0) {
|
|
560
|
+
throw validation(`material dependencies of ${input} cannot be carried to ${outputDir}: ${plan.missing.join(", ")} — the output would reference files that are not there. ` +
|
|
561
|
+
"Pass --geometry-only to write only the geometry, or keep the output in the input's directory", { hint: "pass --geometry-only, or write the output next to the input" });
|
|
562
|
+
}
|
|
563
|
+
if (crossDir && geometryOnly && scan.mtllib.length > 0) {
|
|
564
|
+
warnings.push(warning("material_dependencies_not_copied", `--geometry-only: ${scan.mtllib.join(", ")} referenced by the output were not copied to ${outputDir}`));
|
|
565
|
+
}
|
|
566
|
+
// Pass 2 — rewrite into a temp file beside the target, then publish.
|
|
567
|
+
if (!inPlace) {
|
|
568
|
+
try {
|
|
569
|
+
mkdirSync(outputDir, { recursive: true });
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
throw localIo(`cannot create output directory ${outputDir}: ${err.message}`, err);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const tmp = tempPathFor(output);
|
|
576
|
+
const copied = [];
|
|
577
|
+
try {
|
|
578
|
+
await rewriteObj(input, tmp, tf);
|
|
579
|
+
if (inPlace) {
|
|
580
|
+
const fd = openSync(tmp, "r");
|
|
581
|
+
try {
|
|
582
|
+
fsyncSync(fd);
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
closeSync(fd);
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
chmodSync(tmp, stat.mode & 0o777);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
/* keep default mode */
|
|
592
|
+
}
|
|
593
|
+
renameSync(tmp, output);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
for (const copy of plan.copies) {
|
|
597
|
+
// Re-proven at publication time: the tree may have changed since planning.
|
|
598
|
+
copy.real = resolveWithinRoot(copy.target, writeRoot, { label: `material dependency target for '${copy.ref}'` }).path;
|
|
599
|
+
if (copyDependency(copy.source, copy.real) === "copied") {
|
|
600
|
+
copied.push(copy.target);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
warnings.push(warning("material_dependency_already_present", `${copy.target} already exists with identical content; not copied again`));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
resolveWithinRoot(output, writeRoot, { label: "output" });
|
|
607
|
+
publishTempFile(tmp, output);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
removeQuietly(tmp);
|
|
612
|
+
if (err instanceof CliError || err instanceof UsageError)
|
|
613
|
+
throw err;
|
|
614
|
+
throw localIo(`failed to write ${output}: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
615
|
+
}
|
|
616
|
+
const afterBbox = {
|
|
617
|
+
min: [
|
|
618
|
+
noNegZero(rot.min[0] * scale + translation[0]),
|
|
619
|
+
noNegZero(rot.min[1] * scale + translation[1]),
|
|
620
|
+
noNegZero(rot.min[2] * scale + translation[2]),
|
|
621
|
+
],
|
|
622
|
+
max: [
|
|
623
|
+
noNegZero(rot.max[0] * scale + translation[0]),
|
|
624
|
+
noNegZero(rot.max[1] * scale + translation[1]),
|
|
625
|
+
noNegZero(rot.max[2] * scale + translation[2]),
|
|
626
|
+
],
|
|
627
|
+
};
|
|
628
|
+
return {
|
|
629
|
+
input,
|
|
630
|
+
output,
|
|
631
|
+
height_mm: heightMm,
|
|
632
|
+
scale,
|
|
633
|
+
rotation: OBJ_ROTATION,
|
|
634
|
+
translation,
|
|
635
|
+
before_bbox: { min: [...scan.raw.min], max: [...scan.raw.max] },
|
|
636
|
+
after_bbox: afterBbox,
|
|
637
|
+
counts: {
|
|
638
|
+
vertices: scan.vertices,
|
|
639
|
+
normals: scan.normals,
|
|
640
|
+
uvs: scan.uvs,
|
|
641
|
+
faces: scan.faces,
|
|
642
|
+
lines_total: scan.lines,
|
|
643
|
+
},
|
|
644
|
+
material: { mtllib: scan.mtllib, copied, missing: plan.missing },
|
|
645
|
+
in_place: inPlace,
|
|
646
|
+
warnings,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
//# sourceMappingURL=obj-transform.js.map
|