topotopo 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -12
- package/dist/cli.js +98 -23
- package/dist/graph/engine.js +4 -1
- package/dist/graph/params.d.ts +7 -1
- package/dist/graph/params.js +21 -8
- package/dist/graph/types.d.ts +11 -2
- package/dist/host/export_scene.py +200 -0
- package/dist/host/node.d.ts +8 -0
- package/dist/host/node.js +114 -0
- package/dist/host/types.d.ts +5 -0
- package/dist/inspect.js +5 -2
- package/dist/io/common.d.ts +6 -0
- package/dist/io/export.js +7 -4
- package/dist/io/gltf.d.ts +1 -1
- package/dist/io/gltf.js +167 -32
- package/dist/io/heightmap.d.ts +1 -1
- package/dist/io/import.d.ts +2 -2
- package/dist/io/import.js +10 -3
- package/dist/io/png-decode.d.ts +5 -2
- package/dist/io/png-decode.js +10 -3
- package/dist/io/textures.d.ts +31 -2
- package/dist/io/textures.js +66 -18
- package/dist/io/unreal.js +22 -5
- package/dist/mcp/docs.d.ts +9 -2
- package/dist/mcp/docs.js +113 -2
- package/dist/mcp/inputs.d.ts +28 -0
- package/dist/mcp/inputs.js +377 -0
- package/dist/mcp/server.js +4 -1
- package/dist/mcp/tools.js +32 -18
- package/dist/nodes/code.js +2 -0
- package/dist/nodes/copy.js +14 -4
- package/dist/nodes/flow.js +136 -35
- package/dist/nodes/heightfield.js +67 -11
- package/dist/nodes/texture.d.ts +1 -1
- package/dist/nodes/texture.js +61 -5
- package/dist/render/options.js +2 -1
- package/dist/render/raster.d.ts +2 -0
- package/dist/render/raster.js +3 -3
- package/dist/render/texview.d.ts +9 -1
- package/dist/render/texview.js +43 -2
- package/dist/viewer/app/api.js +64 -0
- package/dist/viewer/app/camera.js +230 -0
- package/dist/viewer/app/grid.js +217 -0
- package/dist/viewer/app/guides.js +402 -0
- package/dist/viewer/app/keymap.js +187 -0
- package/dist/viewer/app/layers.js +225 -0
- package/dist/viewer/app/main.js +590 -0
- package/dist/viewer/app/markers.js +206 -0
- package/dist/viewer/app/mesh.js +172 -0
- package/dist/viewer/app/outliner.js +184 -0
- package/dist/viewer/app/props.js +432 -0
- package/dist/viewer/app/scene.js +529 -0
- package/dist/viewer/app/select.js +142 -0
- package/dist/viewer/app/selection.js +79 -0
- package/dist/viewer/app/style.css +172 -0
- package/dist/viewer/app/undo.js +49 -0
- package/dist/viewer/app/units.js +51 -0
- package/dist/viewer/app/vars.js +64 -0
- package/dist/viewer/index.html +53 -184
- package/dist/viewer/server.d.ts +42 -1
- package/dist/viewer/server.js +432 -17
- package/dist/workspace.d.ts +52 -2
- package/dist/workspace.js +95 -3
- package/package.json +8 -3
- package/shelf/brick_wall.json +1 -1
- package/shelf/wall_segment.json +2 -2
package/dist/io/gltf.js
CHANGED
|
@@ -17,8 +17,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
17
17
|
// or the smallest integer type that holds an int attribute). The original names and types are kept in mesh
|
|
18
18
|
// extras (topotopo.attributes), so imports restore them exactly. POINTS always carry pscale as `_PSCALE`.
|
|
19
19
|
// Materials: the prim "material" attribute names a material of the detail table "materials" (core/materials.ts).
|
|
20
|
+
// Colour space: TopoTopo colours (Cd, material base/emissive colours) are display values; glTF defines
|
|
21
|
+
// baseColorFactor, emissiveFactor and COLOR_0 as linear, so they are converted on export and back on import.
|
|
20
22
|
// Vertex colours win: a primitive with Cd gets baseColorFactor white × COLOR_0, the material supplies the PBR
|
|
21
23
|
// scalars; the original material is kept in material extras (topotopo.material) for lossless re-import.
|
|
24
|
+
// A primitive whose Cd is one constant colour writes it as the baseColorFactor instead of COLOR_0 (the same
|
|
25
|
+
// colour under glTF's factor × COLOR_0, and visible in engines whose default materials ignore vertex colours,
|
|
26
|
+
// such as Unreal's Interchange); primitive extras (topotopo.Cd) restore it as point Cd on import.
|
|
22
27
|
// Options: quantize (KHR_mesh_quantization), compress 'meshopt' (EXT_meshopt_compression, implies quantize),
|
|
23
28
|
// compress 'draco' (KHR_draco_mesh_compression, only when the optional draco3dgltf package is installed).
|
|
24
29
|
// Packed instances (instancing 'packed', the default): one mesh + node per packed source with EXT_mesh_gpu_instancing
|
|
@@ -54,7 +59,7 @@ import { resolveVertexNormals, triangulate } from '../core/topology.js';
|
|
|
54
59
|
import { DEFAULT_MATERIAL, mergeMaterialTables, normalizeMaterial, primMaterials, texturePaths, writeMaterials } from '../core/materials.js';
|
|
55
60
|
import { m4apply, m4applyDir, m4fromQuat, m4mul, m4normalMatrix, m4scale, m4translate } from '../core/math.js';
|
|
56
61
|
import { INSTANCE_ATTRIB_SKIP, anchorAttribs, anchorValue, baseOf, decomposeXform, isolatedMask, packedSourceNames, pieces, xf, xfDir, xfLinear } from './common.js';
|
|
57
|
-
import { DEFAULT_TRANSFER, expandPacked, flattenSource, packedFromMatrices, stripPacked } from '../core/packed.js';
|
|
62
|
+
import { DEFAULT_TRANSFER, expandPacked, flattenSource, packedFromMatrices, stripPacked, xform3 } from '../core/packed.js';
|
|
58
63
|
import { hasSplinesDeep, splinesToPolylinesDeep } from '../core/splines.js';
|
|
59
64
|
import { imageFromPNG, imageTextureData } from './textures.js';
|
|
60
65
|
import { decodePNG } from './png-decode.js';
|
|
@@ -270,8 +275,8 @@ function textureFor(st, path) {
|
|
|
270
275
|
st.textures.set(path, t);
|
|
271
276
|
return t;
|
|
272
277
|
}
|
|
273
|
-
function getMaterial(st, name, vertexColors, blend) {
|
|
274
|
-
const key = `${name}\u0000${vertexColors ? 1 : 0}${blend ? 1 : 0}`;
|
|
278
|
+
function getMaterial(st, name, vertexColors, blend, tint) {
|
|
279
|
+
const key = `${name}\u0000${vertexColors ? 1 : 0}${blend ? 1 : 0}${tint ? `\u0000${tint.join(',')}` : ''}`;
|
|
275
280
|
let m = st.cache.get(key);
|
|
276
281
|
if (m)
|
|
277
282
|
return m;
|
|
@@ -286,9 +291,10 @@ function getMaterial(st, name, vertexColors, blend) {
|
|
|
286
291
|
st.warn?.(`glTF: material "${name}" has an orm and a roughness map; the orm map's green channel is used`);
|
|
287
292
|
// a map replaces its factor: white base colour under a base colour map, roughness 1 under a roughness map,
|
|
288
293
|
// metallic 1 under an ORM map
|
|
289
|
-
|
|
294
|
+
// a folded constant vertex colour multiplies a base colour map exactly as COLOR_0 would have
|
|
295
|
+
const rgb = tint ?? (vertexColors || baseTex ? [1, 1, 1] : mat.baseColor);
|
|
290
296
|
m = st.doc.createMaterial(isDefault ? 'default' : name)
|
|
291
|
-
.setBaseColorFactor([rgb[0], rgb[1], rgb[2], mat.alpha])
|
|
297
|
+
.setBaseColorFactor([toLinear(rgb[0]), toLinear(rgb[1]), toLinear(rgb[2]), mat.alpha]) // glTF factors are linear
|
|
292
298
|
.setMetallicFactor(ormTex ? 1 : mat.metallic)
|
|
293
299
|
.setRoughnessFactor(roughTex ? 1 : mat.roughness)
|
|
294
300
|
.setDoubleSided(mat.doubleSided);
|
|
@@ -302,12 +308,12 @@ function getMaterial(st, name, vertexColors, blend) {
|
|
|
302
308
|
m.setOcclusionTexture(ormTex);
|
|
303
309
|
const emax = Math.max(...mat.emissive);
|
|
304
310
|
if (emax > 1) {
|
|
305
|
-
m.setEmissiveFactor([mat.emissive[0] / emax, mat.emissive[1] / emax, mat.emissive[2] / emax]);
|
|
311
|
+
m.setEmissiveFactor([toLinear(mat.emissive[0] / emax), toLinear(mat.emissive[1] / emax), toLinear(mat.emissive[2] / emax)]);
|
|
306
312
|
const ext = st.doc.createExtension(KHRMaterialsEmissiveStrength);
|
|
307
313
|
m.setExtension('KHR_materials_emissive_strength', ext.createEmissiveStrength().setEmissiveStrength(emax));
|
|
308
314
|
}
|
|
309
315
|
else
|
|
310
|
-
m.setEmissiveFactor([mat.emissive[0], mat.emissive[1], mat.emissive[2]]);
|
|
316
|
+
m.setEmissiveFactor([toLinear(mat.emissive[0]), toLinear(mat.emissive[1]), toLinear(mat.emissive[2])]);
|
|
311
317
|
if (mat.alpha < 1 || blend)
|
|
312
318
|
m.setAlphaMode('BLEND');
|
|
313
319
|
if (mat.unlit)
|
|
@@ -316,6 +322,17 @@ function getMaterial(st, name, vertexColors, blend) {
|
|
|
316
322
|
st.cache.set(key, m);
|
|
317
323
|
return m;
|
|
318
324
|
}
|
|
325
|
+
/** Display (sRGB-encoded) value to linear and back: glTF's factors and COLOR_0 are linear, TopoTopo colours are display
|
|
326
|
+
* values (docs/DESIGN.md, colour). */
|
|
327
|
+
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
328
|
+
const toDisplay = (c) => (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
329
|
+
/** True when every element of a colour stream has the same rgb. */
|
|
330
|
+
const constantRGB = (d, size) => {
|
|
331
|
+
for (let i = size; i < d.length; i += size)
|
|
332
|
+
if (d[i] !== d[0] || d[i + 1] !== d[1] || d[i + 2] !== d[2])
|
|
333
|
+
return false;
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
319
336
|
/** True when every rgb component of a colour stream is 1 (Houdini's default Cd). */
|
|
320
337
|
const rgbAllOnes = (d, size) => {
|
|
321
338
|
for (let i = 0; i < d.length; i++)
|
|
@@ -382,18 +399,27 @@ export function gltfDocument(g, o = {}) {
|
|
|
382
399
|
const finishColor = (out, mat) => {
|
|
383
400
|
// vertex colours win where they are set; all-white Cd lets the material colour through
|
|
384
401
|
const ci = color ? out.channels.indexOf(color) : -1;
|
|
385
|
-
let vc = false, blend = false;
|
|
402
|
+
let vc = false, blend = false, tint;
|
|
386
403
|
if (ci >= 0) {
|
|
387
404
|
const ch = out.channels[ci], d = out.data[ci];
|
|
388
405
|
const rgbSet = !!geo.findAttrib('Cd') && !rgbAllOnes(d, ch.size);
|
|
389
406
|
blend = ch.size === 4 && d.some((x, i) => i % 4 === 3 && x < 1);
|
|
390
407
|
vc = rgbSet;
|
|
391
|
-
|
|
408
|
+
// one colour over the whole primitive: the factor carries it (identical under glTF's factor × COLOR_0), so
|
|
409
|
+
// engines that ignore vertex colours in their default materials (Unreal Interchange) still show it
|
|
410
|
+
if (rgbSet && !blend && d.length >= ch.size && constantRGB(d, ch.size)) {
|
|
411
|
+
tint = [d[0], d[1], d[2]].map((x) => Math.max(0, Math.min(1, x)));
|
|
412
|
+
out.foldedColor = tint;
|
|
413
|
+
vc = false;
|
|
414
|
+
}
|
|
415
|
+
else if (rgbSet && o.materialColorWins && mat && st.table[mat])
|
|
416
|
+
vc = false; // keeps COLOR_0, factor = material colour
|
|
417
|
+
if ((!rgbSet || tint) && !blend) {
|
|
392
418
|
out.channels.splice(ci, 1);
|
|
393
419
|
out.data.splice(ci, 1);
|
|
394
420
|
}
|
|
395
421
|
}
|
|
396
|
-
out.material = getMaterial(st, mat, vc, blend);
|
|
422
|
+
out.material = getMaterial(st, mat, vc, blend, tint);
|
|
397
423
|
if (out.material.getBaseColorTexture() || out.material.getNormalTexture() || out.material.getMetallicRoughnessTexture()) {
|
|
398
424
|
if (!out.channels.some((c) => c.semantic === 'TEXCOORD_0'))
|
|
399
425
|
noUV++;
|
|
@@ -699,6 +725,8 @@ function writeMesh(doc, buffer, scene, name, outs, info, quantize) {
|
|
|
699
725
|
const acc = (arr, size, normalized = false) => doc.createAccessor().setArray(arr).setType(TYPES[size - 1]).setBuffer(buffer).setNormalized(normalized);
|
|
700
726
|
for (const out of outs) {
|
|
701
727
|
const prim = doc.createPrimitive().setMode(out.mode);
|
|
728
|
+
if (out.foldedColor)
|
|
729
|
+
prim.setExtras({ topotopo: { Cd: out.foldedColor } });
|
|
702
730
|
out.channels.forEach((ch, ci) => {
|
|
703
731
|
const d = out.data[ci];
|
|
704
732
|
let a;
|
|
@@ -715,10 +743,11 @@ function writeMesh(doc, buffer, scene, name, outs, info, quantize) {
|
|
|
715
743
|
a = acc(Uint16Array.from(d, (x) => Math.round(x * 65535)), 2, true);
|
|
716
744
|
}
|
|
717
745
|
else if (ch.semantic === 'COLOR_0' && quantize && d.every((x) => x >= 0 && x <= 1)) {
|
|
718
|
-
|
|
746
|
+
// COLOR_0 is linear: rgb from display values, alpha as it is
|
|
747
|
+
a = acc(Uint16Array.from(d, (x, i) => Math.round((i % ch.size < 3 ? toLinear(x) : x) * 65535)), ch.size, true);
|
|
719
748
|
}
|
|
720
749
|
else if (ch.semantic === 'COLOR_0') {
|
|
721
|
-
a = acc(Float32Array.from(d, (x) => Math.max(0, Math.min(1, x))), ch.size);
|
|
750
|
+
a = acc(Float32Array.from(d, (x, i) => { const c = Math.max(0, Math.min(1, x)); return i % ch.size < 3 ? toLinear(c) : c; }), ch.size);
|
|
722
751
|
}
|
|
723
752
|
else if (ch.int) {
|
|
724
753
|
let mn = Infinity, mx = -Infinity;
|
|
@@ -882,9 +911,10 @@ function materialEntry(m, base = '', index = () => 0) {
|
|
|
882
911
|
return normalizeMaterial(ex);
|
|
883
912
|
const bc = m.getBaseColorFactor(), em = m.getEmissiveFactor();
|
|
884
913
|
const strength = m.getExtension('KHR_materials_emissive_strength')?.getEmissiveStrength() ?? 1;
|
|
914
|
+
// glTF factors are linear; the material table holds display values
|
|
885
915
|
return normalizeMaterial({
|
|
886
|
-
baseColor: [bc[0], bc[1], bc[2]], metallic: m.getMetallicFactor(), roughness: m.getRoughnessFactor(),
|
|
887
|
-
emissive: [em[0] * strength, em[1] * strength, em[2] * strength], alpha: bc[3],
|
|
916
|
+
baseColor: [toDisplay(bc[0]), toDisplay(bc[1]), toDisplay(bc[2])], metallic: m.getMetallicFactor(), roughness: m.getRoughnessFactor(),
|
|
917
|
+
emissive: [toDisplay(em[0]) * strength, toDisplay(em[1]) * strength, toDisplay(em[2]) * strength], alpha: bc[3],
|
|
888
918
|
doubleSided: m.getDoubleSided(), unlit: !!m.getExtension('KHR_materials_unlit'),
|
|
889
919
|
textures: {
|
|
890
920
|
baseColor: texturePathOf(m.getBaseColorTexture(), base, index),
|
|
@@ -970,7 +1000,8 @@ function primitiveGeometry(prim, attrInfo, warn) {
|
|
|
970
1000
|
else if (sem === 'TEXCOORD_0' || sem === 'TEXCOORD_1')
|
|
971
1001
|
setPt(sem === 'TEXCOORD_0' ? 'uv' : 'uv2', a, 3, 'float', (s, i, out) => { out[i * 3] = s[i * es]; out[i * 3 + 1] = 1 - s[i * es + 1]; out[i * 3 + 2] = 0; });
|
|
972
1002
|
else if (sem === 'COLOR_0') {
|
|
973
|
-
|
|
1003
|
+
// COLOR_0 is linear; Cd holds display values
|
|
1004
|
+
setPt('Cd', a, 3, 'float', (s, i, out) => { out[i * 3] = toDisplay(s[i * es]); out[i * 3 + 1] = toDisplay(s[i * es + 1]); out[i * 3 + 2] = toDisplay(s[i * es + 2]); });
|
|
974
1005
|
if (es === 4)
|
|
975
1006
|
setPt('Alpha', a, 1, 'float', (s, i, out) => { out[i] = s[i * 4 + 3]; });
|
|
976
1007
|
}
|
|
@@ -981,6 +1012,18 @@ function primitiveGeometry(prim, attrInfo, warn) {
|
|
|
981
1012
|
}
|
|
982
1013
|
// TANGENT, JOINTS_n, WEIGHTS_n, TEXCOORD_2+, COLOR_1+ are not imported
|
|
983
1014
|
}
|
|
1015
|
+
// a constant Cd that the exporter folded into the material factor comes back as the point colour it was
|
|
1016
|
+
const folded = prim.getExtras()?.topotopo?.Cd;
|
|
1017
|
+
if (!prim.getAttribute('COLOR_0') && Array.isArray(folded) && folded.length === 3 && folded.every((x) => typeof x === 'number')) {
|
|
1018
|
+
const cd = new Attribute('float', 3, new Float64Array(n * 3), defaultValueFor('Cd', 3, 'float'));
|
|
1019
|
+
const d = cd.data;
|
|
1020
|
+
for (let i = 0; i < n; i++) {
|
|
1021
|
+
d[i * 3] = folded[0];
|
|
1022
|
+
d[i * 3 + 1] = folded[1];
|
|
1023
|
+
d[i * 3 + 2] = folded[2];
|
|
1024
|
+
}
|
|
1025
|
+
g.points.set('Cd', cd);
|
|
1026
|
+
}
|
|
984
1027
|
return g;
|
|
985
1028
|
}
|
|
986
1029
|
function transformGeo(local, m) {
|
|
@@ -1015,7 +1058,35 @@ function instanceMatrices(inst) {
|
|
|
1015
1058
|
}
|
|
1016
1059
|
return out;
|
|
1017
1060
|
}
|
|
1018
|
-
|
|
1061
|
+
const RESERVED_EXTRAS = new Set('P N uv uv2 Cd Alpha material name path orient scale pscale'.split(' '));
|
|
1062
|
+
function constantAttribute(table, name, type, size, value) {
|
|
1063
|
+
const a = table.add(name, type, size);
|
|
1064
|
+
for (let i = 0; i < table.count; i++)
|
|
1065
|
+
a.set(i, value);
|
|
1066
|
+
}
|
|
1067
|
+
/** Object extras only; topotopo's private round-trip metadata keeps its existing readers. */
|
|
1068
|
+
function objectExtras(table, extras, collisions) {
|
|
1069
|
+
// Explicit key order keeps schemas stable even when producers serialize extras in a different order.
|
|
1070
|
+
for (const key of Object.keys(extras).sort()) {
|
|
1071
|
+
if (key === 'topotopo')
|
|
1072
|
+
continue;
|
|
1073
|
+
if (RESERVED_EXTRAS.has(key)) {
|
|
1074
|
+
collisions.add(key);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const value = extras[key];
|
|
1078
|
+
if (typeof value === 'string')
|
|
1079
|
+
constantAttribute(table, key, 'string', 1, value);
|
|
1080
|
+
else if (typeof value === 'boolean')
|
|
1081
|
+
constantAttribute(table, key, 'int', 1, value ? 1 : 0);
|
|
1082
|
+
else if (typeof value === 'number' && Number.isFinite(value))
|
|
1083
|
+
constantAttribute(table, key, Number.isInteger(value) ? 'int' : 'float', 1, value);
|
|
1084
|
+
else if (Array.isArray(value) && value.length >= 2 && value.length <= 4 && value.every((v) => typeof v === 'number' && Number.isFinite(v))) {
|
|
1085
|
+
constantAttribute(table, key, 'float', value.length, value);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/** Geometry from the default (or first) scene, traversed in scene order with world transforms. */
|
|
1019
1090
|
export function documentToGeometry(doc, warn = () => { }, instancing = 'packed', base = '') {
|
|
1020
1091
|
const root = doc.getRoot();
|
|
1021
1092
|
const texIndex = (t) => root.listTextures().indexOf(t);
|
|
@@ -1040,24 +1111,62 @@ export function documentToGeometry(doc, warn = () => { }, instancing = 'packed',
|
|
|
1040
1111
|
const parts = [];
|
|
1041
1112
|
const localCache = new Map();
|
|
1042
1113
|
const sourceCache = new Map();
|
|
1114
|
+
const sharedSources = new Map();
|
|
1115
|
+
const nodes = [];
|
|
1116
|
+
const scene = root.getDefaultScene() ?? root.listScenes()[0] ?? null;
|
|
1117
|
+
if (scene)
|
|
1118
|
+
scene.traverse((n) => { nodes.push(n); });
|
|
1119
|
+
else
|
|
1120
|
+
for (const n of root.listNodes())
|
|
1121
|
+
if (!n.getParentNode())
|
|
1122
|
+
n.traverse((n) => { nodes.push(n); });
|
|
1123
|
+
const meshRefs = new Map();
|
|
1124
|
+
for (const node of nodes) {
|
|
1125
|
+
const mesh = node.getMesh();
|
|
1126
|
+
if (mesh && !node.getExtension('EXT_mesh_gpu_instancing') && !node.getCamera() && !node.getExtension('KHR_lights_punctual'))
|
|
1127
|
+
meshRefs.set(mesh, (meshRefs.get(mesh) ?? 0) + 1);
|
|
1128
|
+
}
|
|
1129
|
+
const collisions = new Set();
|
|
1130
|
+
let skipped = 0;
|
|
1043
1131
|
let instanced = false;
|
|
1044
1132
|
const visitNode = (node) => {
|
|
1045
|
-
|
|
1046
|
-
|
|
1133
|
+
if (node.getCamera() || node.getExtension('KHR_lights_punctual')) {
|
|
1134
|
+
skipped++;
|
|
1047
1135
|
return;
|
|
1136
|
+
}
|
|
1137
|
+
const mesh = node.getMesh();
|
|
1048
1138
|
const world = Float64Array.from(node.getWorldMatrix());
|
|
1139
|
+
const ancestors = [];
|
|
1140
|
+
for (let p = node.getParentNode(); p; p = p.getParentNode())
|
|
1141
|
+
ancestors.push(p.getName());
|
|
1142
|
+
const path = ancestors.reverse().join('/');
|
|
1143
|
+
const extras = { ...mesh?.getExtras(), ...node.getExtras() };
|
|
1144
|
+
if (!mesh) {
|
|
1145
|
+
const g = new Geometry({ positions: [world[12], world[13], world[14]] });
|
|
1146
|
+
const { q, s } = decomposeXform(xform3(world));
|
|
1147
|
+
constantAttribute(g.points, 'name', 'string', 1, node.getName());
|
|
1148
|
+
constantAttribute(g.points, 'path', 'string', 1, path);
|
|
1149
|
+
constantAttribute(g.points, 'orient', 'float', 4, q);
|
|
1150
|
+
constantAttribute(g.points, 'scale', 'float', 3, s);
|
|
1151
|
+
constantAttribute(g.points, 'pscale', 'float', 1, 1);
|
|
1152
|
+
objectExtras(g.points, extras, collisions);
|
|
1153
|
+
parts.push(g);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1049
1156
|
const inst = node.getExtension('EXT_mesh_gpu_instancing');
|
|
1050
1157
|
const mats = inst ? instanceMatrices(inst).map((m) => m4mul(world, m)) : [world];
|
|
1051
1158
|
const info = (mesh.getExtras()?.topotopo?.attributes ?? {});
|
|
1052
1159
|
const label = node.getName() || mesh.getName();
|
|
1053
|
-
|
|
1054
|
-
|
|
1160
|
+
const shared = !inst && (meshRefs.get(mesh) ?? 0) > 1;
|
|
1161
|
+
if ((inst || shared) && instancing === 'packed') {
|
|
1162
|
+
// Shared-node sources omit name: the packed prim owns it, so unpack's normal transfer keeps each label.
|
|
1163
|
+
// Existing EXT sources retain their labels for round trips and source naming in instance-stream exports.
|
|
1055
1164
|
let byLabel = sourceCache.get(mesh);
|
|
1056
1165
|
if (!byLabel) {
|
|
1057
1166
|
byLabel = new Map();
|
|
1058
1167
|
sourceCache.set(mesh, byLabel);
|
|
1059
1168
|
}
|
|
1060
|
-
let src = byLabel.get(label);
|
|
1169
|
+
let src = shared ? sharedSources.get(mesh) : byLabel.get(label);
|
|
1061
1170
|
if (!src) {
|
|
1062
1171
|
const ps = [];
|
|
1063
1172
|
for (const prim of mesh.listPrimitives()) {
|
|
@@ -1070,7 +1179,7 @@ export function documentToGeometry(doc, warn = () => { }, instancing = 'packed',
|
|
|
1070
1179
|
continue;
|
|
1071
1180
|
const g = local.clone();
|
|
1072
1181
|
const mname = matName(prim.getMaterial());
|
|
1073
|
-
if (label) {
|
|
1182
|
+
if (!shared && label) {
|
|
1074
1183
|
const a = g.prims.add('name', 'string', 1);
|
|
1075
1184
|
for (let k = 0; k < g.numPrims; k++)
|
|
1076
1185
|
a.set(k, label);
|
|
@@ -1083,10 +1192,23 @@ export function documentToGeometry(doc, warn = () => { }, instancing = 'packed',
|
|
|
1083
1192
|
ps.push(g);
|
|
1084
1193
|
}
|
|
1085
1194
|
src = Geometry.merge(ps);
|
|
1086
|
-
|
|
1195
|
+
if (shared)
|
|
1196
|
+
sharedSources.set(mesh, src);
|
|
1197
|
+
else
|
|
1198
|
+
byLabel.set(label, src);
|
|
1087
1199
|
}
|
|
1088
1200
|
const part = packedFromMatrices(src, mats);
|
|
1089
|
-
|
|
1201
|
+
constantAttribute(part.prims, 'name', 'string', 1, label);
|
|
1202
|
+
constantAttribute(part.prims, 'path', 'string', 1, path);
|
|
1203
|
+
objectExtras(part.prims, extras, collisions);
|
|
1204
|
+
if (inst)
|
|
1205
|
+
applyInstanceAttribs(part, inst, (node.getExtras()?.topotopo?.instanceAttributes ?? {}), warn);
|
|
1206
|
+
// Native instance metadata can carry these in point class; expose the effective label on the prim too.
|
|
1207
|
+
for (const key of ['name', 'path']) {
|
|
1208
|
+
const a = part.points.get(key);
|
|
1209
|
+
if (a?.type === 'string')
|
|
1210
|
+
part.prims.set(key, a.clone());
|
|
1211
|
+
}
|
|
1090
1212
|
parts.push(part);
|
|
1091
1213
|
return;
|
|
1092
1214
|
}
|
|
@@ -1113,19 +1235,30 @@ export function documentToGeometry(doc, warn = () => { }, instancing = 'packed',
|
|
|
1113
1235
|
for (let k = 0; k < g.numPrims; k++)
|
|
1114
1236
|
a.set(k, mname);
|
|
1115
1237
|
}
|
|
1238
|
+
constantAttribute(g.prims, 'path', 'string', 1, path);
|
|
1239
|
+
objectExtras(g.prims, extras, collisions);
|
|
1116
1240
|
parts.push(g);
|
|
1117
1241
|
}
|
|
1118
1242
|
}
|
|
1119
1243
|
};
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
n.traverse(visitNode);
|
|
1244
|
+
for (const node of nodes)
|
|
1245
|
+
visitNode(node);
|
|
1246
|
+
if (skipped)
|
|
1247
|
+
warn(`skipped ${skipped} cameras/lights`);
|
|
1248
|
+
if (collisions.size)
|
|
1249
|
+
warn(`glTF extras skipped reserved attributes: ${[...collisions].sort().join(', ')}`);
|
|
1127
1250
|
if (instanced)
|
|
1128
1251
|
warn('EXT_mesh_gpu_instancing instances were expanded into geometry');
|
|
1252
|
+
// Existing unpack/export rules prefer point attributes over prim attributes. Empty-point schemas must not
|
|
1253
|
+
// hide an instance's values with merge defaults. Mirror only colliding keys onto its anchor, before the normal
|
|
1254
|
+
// Geometry.merge handles type promotion/defaults. Explicit native instance point metadata still wins.
|
|
1255
|
+
const pointKeys = new Set(parts.flatMap((g) => g.points.names()));
|
|
1256
|
+
for (const part of parts)
|
|
1257
|
+
if (part.hasPacked()) {
|
|
1258
|
+
for (const [key, a] of part.prims.map)
|
|
1259
|
+
if (pointKeys.has(key) && !part.points.has(key))
|
|
1260
|
+
part.points.set(key, a.clone());
|
|
1261
|
+
}
|
|
1129
1262
|
const out = Geometry.merge(parts);
|
|
1130
1263
|
if (Object.keys(table).length)
|
|
1131
1264
|
writeMaterials(out, table);
|
|
@@ -1143,7 +1276,9 @@ export function documentToGeometry(doc, warn = () => { }, instancing = 'packed',
|
|
|
1143
1276
|
if (name === null || images.has(name) || !bytes?.length || imageMime(bytes) !== 'image/png')
|
|
1144
1277
|
continue;
|
|
1145
1278
|
try {
|
|
1146
|
-
|
|
1279
|
+
// one layer per texture: a grey + alpha map stays RGBA, so a re-export writes its alpha
|
|
1280
|
+
const png = decodePNG(bytes);
|
|
1281
|
+
images.set(name, imageFromPNG(png, name, { layers: png.channels === 1 ? 'grey' : 'rgba' }));
|
|
1147
1282
|
}
|
|
1148
1283
|
catch (e) {
|
|
1149
1284
|
warn(`texture "${path}": ${e?.message ?? e}`);
|
package/dist/io/heightmap.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Heightfield } from '../core/heightfield.ts';
|
|
|
3
3
|
export type HeightmapFormat = 'png16' | 'r16' | 'mask_png';
|
|
4
4
|
export declare const HEIGHTMAP_FORMATS: HeightmapFormat[];
|
|
5
5
|
export interface HeightmapOptions {
|
|
6
|
-
/** png16/r16: the layer to write (default "height"); mask_png: the layer when `layers` is not given (default "mask") */
|
|
6
|
+
/** png16/r16: the layer to write (default "height"); mask_png: the layer when `layers` is not given (default "mask"); unreal_landscape: the height layer */
|
|
7
7
|
layer?: string;
|
|
8
8
|
/** mask_png: layers to write, one 8-bit PNG each */
|
|
9
9
|
layers?: string[];
|
package/dist/io/import.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ export interface ImportOptions {
|
|
|
10
10
|
path?: string;
|
|
11
11
|
/** receives non-fatal problems (skipped primitives or attributes) */
|
|
12
12
|
warn?: (msg: string) => void;
|
|
13
|
-
/** glTF EXT_mesh_gpu_instancing: 'packed' (default) = packed prims
|
|
13
|
+
/** glTF shared meshes / EXT_mesh_gpu_instancing: 'packed' (default) = packed prims; 'expand' = real geometry */
|
|
14
14
|
instancing?: 'packed' | 'expand';
|
|
15
15
|
}
|
|
16
|
-
/** Format from a file name: the extension, with ".points.json" → pointsjson. */
|
|
16
|
+
/** Format from a file name: the extension, with ".points.json" → pointsjson (".png" → png: an image). */
|
|
17
17
|
export declare function importFormatFromPath(path: string): string;
|
|
18
18
|
/** Synchronous import for every format except glb/gltf (see importGeometryAsync). */
|
|
19
19
|
export declare function importGeometry(bytes: Uint8Array, fmt: string, opts?: ImportOptions): Geometry;
|
package/dist/io/import.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Importers: OBJ, STL, PLY, glTF/GLB (gltf-transform, src/io/gltf.ts), native JSON, Houdini .geo (src/io/hgeo.ts),
|
|
2
|
-
// CSV and points JSON (src/io/points.ts).
|
|
2
|
+
// CSV and points JSON (src/io/points.ts), and PNG as a geometry holding one image volume (src/io/textures.ts).
|
|
3
3
|
//
|
|
4
4
|
// importGeometry() is synchronous and covers every format except glTF, whose reader (gltf-transform) is
|
|
5
5
|
// asynchronous: use importGeometryAsync() for glb/gltf (it handles every format, and reads external .bin files).
|
|
@@ -8,12 +8,13 @@ import { geoFromJSON } from './json.js';
|
|
|
8
8
|
import { importGLTFAsync } from './gltf.js';
|
|
9
9
|
import { importHoudiniBgeo, importHoudiniGeo, isBinaryJSON } from './hgeo.js';
|
|
10
10
|
import { importCSV, importPointsJSON, isPointsJSON } from './points.js';
|
|
11
|
+
import { imageFromPNGFile } from './textures.js';
|
|
11
12
|
export { importGLTFAsync } from './gltf.js';
|
|
12
13
|
export { importHoudiniBgeo, importHoudiniGeo } from './hgeo.js';
|
|
13
14
|
export { importCSV, importPointsJSON } from './points.js';
|
|
14
15
|
const td = new TextDecoder();
|
|
15
|
-
export const IMPORT_FORMATS = ['obj', 'stl', 'ply', 'glb', 'gltf', 'json', 'geo', 'bgeo', 'csv', 'pointsjson'];
|
|
16
|
-
/** Format from a file name: the extension, with ".points.json" → pointsjson. */
|
|
16
|
+
export const IMPORT_FORMATS = ['obj', 'stl', 'ply', 'glb', 'gltf', 'blend', 'json', 'geo', 'bgeo', 'csv', 'pointsjson', 'png'];
|
|
17
|
+
/** Format from a file name: the extension, with ".points.json" → pointsjson (".png" → png: an image). */
|
|
17
18
|
export function importFormatFromPath(path) {
|
|
18
19
|
const p = String(path).split('?')[0].toLowerCase();
|
|
19
20
|
if (p.endsWith('.points.json'))
|
|
@@ -42,6 +43,12 @@ export function importGeometry(bytes, fmt, opts = {}) {
|
|
|
42
43
|
case 'bgeo': return isBinaryJSON(bytes) ? importHoudiniBgeo(bytes, opts.warn) : importHoudiniGeo(td.decode(bytes), opts.warn);
|
|
43
44
|
case 'csv': return importCSV(td.decode(bytes));
|
|
44
45
|
case 'pointsjson': return importPointsJSON(JSON.parse(td.decode(bytes)));
|
|
46
|
+
case 'png': {
|
|
47
|
+
// an image volume named after the file, as the image_file node reads it with its defaults
|
|
48
|
+
const g = new Geometry();
|
|
49
|
+
g.volumes = [imageFromPNGFile(bytes, opts.path ?? 'image.png')];
|
|
50
|
+
return g;
|
|
51
|
+
}
|
|
45
52
|
case 'glb':
|
|
46
53
|
case 'gltf': throw new Error(`${fmt} import is asynchronous (gltf-transform): use importGeometryAsync`);
|
|
47
54
|
case 'bgeo.sc': throw new Error('compressed Houdini geometry (.bgeo.sc / .gz) is not supported: save as .geo or uncompressed .bgeo');
|
package/dist/io/png-decode.d.ts
CHANGED
|
@@ -14,8 +14,11 @@ export interface PngImage {
|
|
|
14
14
|
/** samples, row-major, channels interleaved; row 0 is the top of the image */
|
|
15
15
|
data: Uint16Array;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Decode a PNG file. Throws a readable Error for unsupported or corrupt files, and for images of more than maxPixels
|
|
19
|
+
* pixels before any pixel memory is allocated.
|
|
20
|
+
*/
|
|
21
|
+
export declare function decodePNG(bytes: Uint8Array, maxPixels?: number): PngImage;
|
|
19
22
|
export type PngChannel = 'luminance' | 'average' | 'r' | 'g' | 'b' | 'a';
|
|
20
23
|
/** One channel of a decoded image as floats 0..1 (luminance = Rec. 709 weights; grey images ignore r/g/b). */
|
|
21
24
|
export declare function pngChannel(img: PngImage, channel?: PngChannel): Float32Array;
|
package/dist/io/png-decode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Minimal PNG decoder (pure TypeScript, no host APIs): non-interlaced grey, grey+alpha, RGB, RGBA and palette
|
|
2
2
|
// images at 1/2/4/8/16 bits per channel, with a small inflate (RFC 1950/1951) so it runs anywhere the core runs.
|
|
3
|
-
// Used by heightfield_from_image
|
|
3
|
+
// Used by heightfield_from_image, attribute_from_map and image_file (io/textures.ts), and the glTF importer.
|
|
4
4
|
// ---------------------------------------------------------------- inflate
|
|
5
5
|
class Bits {
|
|
6
6
|
data;
|
|
@@ -180,8 +180,11 @@ export function inflateZlib(data, sizeHint = 0) {
|
|
|
180
180
|
return inflateRaw(data, sizeHint, 2);
|
|
181
181
|
}
|
|
182
182
|
const SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
183
|
-
/**
|
|
184
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Decode a PNG file. Throws a readable Error for unsupported or corrupt files, and for images of more than maxPixels
|
|
185
|
+
* pixels before any pixel memory is allocated.
|
|
186
|
+
*/
|
|
187
|
+
export function decodePNG(bytes, maxPixels = Infinity) {
|
|
185
188
|
for (let i = 0; i < 8; i++)
|
|
186
189
|
if (bytes[i] !== SIG[i])
|
|
187
190
|
throw new Error('not a PNG file (bad signature)');
|
|
@@ -226,6 +229,10 @@ export function decodePNG(bytes) {
|
|
|
226
229
|
throw new Error(`unsupported PNG bit depth ${depth} for colour type ${ctype}`);
|
|
227
230
|
if (ctype === 3 && !palette)
|
|
228
231
|
throw new Error('palette PNG without a PLTE chunk');
|
|
232
|
+
if (width * height > maxPixels) {
|
|
233
|
+
const side = Math.floor(Math.sqrt(maxPixels));
|
|
234
|
+
throw new Error(`the image is ${width} × ${height} pixels, more than the ${maxPixels.toLocaleString('en-US')} pixels allowed here (e.g. ${side} × ${side}): scale it down`);
|
|
235
|
+
}
|
|
229
236
|
let total = 0;
|
|
230
237
|
for (const c of idat)
|
|
231
238
|
total += c.length;
|
package/dist/io/textures.d.ts
CHANGED
|
@@ -7,8 +7,37 @@ import type { PngImage } from './png-decode.ts';
|
|
|
7
7
|
* per channel). deflate compresses (Node: zlib.deflateSync); without it the PNG uses stored (uncompressed) blocks.
|
|
8
8
|
*/
|
|
9
9
|
export declare function imagePNG(img: Image, layer?: string, deflate?: (d: Uint8Array) => Uint8Array): Uint8Array;
|
|
10
|
-
/**
|
|
11
|
-
export declare
|
|
10
|
+
/** How a PNG's channels become layers: see ImageFromPNGOptions. */
|
|
11
|
+
export declare const PNG_LAYERS: readonly ['auto', 'grey', 'rgba'];
|
|
12
|
+
export type PngLayers = (typeof PNG_LAYERS)[number];
|
|
13
|
+
export interface ImageFromPNGOptions {
|
|
14
|
+
/**
|
|
15
|
+
* 'auto' (default): grey files (1 or 2 channels) give a grey layer C, colour files an RGBA one; 'grey': a grey C
|
|
16
|
+
* (Rec. 709 luminance of a colour file); 'rgba': an RGBA C (grey g as g, g, g). A grey C puts the file's alpha, when
|
|
17
|
+
* it has one, in the grey layer "A" (Houdini's alpha plane).
|
|
18
|
+
*/
|
|
19
|
+
layers?: PngLayers;
|
|
20
|
+
/**
|
|
21
|
+
* true (default): the samples are display (sRGB-encoded) values, which colour layers hold too, so they are kept as
|
|
22
|
+
* they are; false: they are linear light, and grey and RGB are encoded to display values (alpha never is).
|
|
23
|
+
*/
|
|
24
|
+
srgb?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** An image volume from a decoded PNG: values = samples / maxValue (16-bit files keep their precision). */
|
|
27
|
+
export declare function imageFromPNG(png: PngImage, name: string, o?: ImageFromPNGOptions): Image;
|
|
28
|
+
/** The image name a file gives: its name without folders and extension ("inputs/brick.png" → "brick"). */
|
|
29
|
+
export declare function imageNameOf(path: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* An image volume from the bytes of the PNG file `path` (the image_file node and the file node's .png path), named
|
|
32
|
+
* `name` or after the file (imageNameOf). Errors name the file; images over MAX_PIXELS are refused before decoding.
|
|
33
|
+
*/
|
|
34
|
+
export declare function imageFromPNGFile(bytes: Uint8Array, path: string, o?: ImageFromPNGOptions & {
|
|
35
|
+
name?: string;
|
|
36
|
+
}): Image;
|
|
37
|
+
/** imageFromPNGFile on a file read through `readFile` (usually Host.readFile). */
|
|
38
|
+
export declare function readImageFile(readFile: (path: string) => Promise<Uint8Array>, path: string, o?: ImageFromPNGOptions & {
|
|
39
|
+
name?: string;
|
|
40
|
+
}): Promise<Image>;
|
|
12
41
|
/** A file-name-safe version of an image name ("" → "image"). */
|
|
13
42
|
export declare function safeFileName(name: string): string;
|
|
14
43
|
/** The images a material table references ("image:<name>"), with the slots using each. */
|
package/dist/io/textures.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Image, channelsOf, imageRef, imageToBytes, opaque, referenceableImages } from '../core/image.js';
|
|
1
|
+
import { Image, LUMA, MAX_PIXELS, channelsOf, imageRef, imageToBytes, opaque, referenceableImages } from '../core/image.js';
|
|
2
2
|
import { TEXTURE_SLOTS, mergeMaterialTables } from '../core/materials.js';
|
|
3
3
|
import { encodePNG } from '../render/png.js';
|
|
4
4
|
import { encodeGreyPNG, zlibStored } from './heightmap.js';
|
|
5
|
+
import { decodePNG } from './png-decode.js';
|
|
5
6
|
import { baseOf, dirOf, joinPath } from './common.js';
|
|
6
7
|
/**
|
|
7
8
|
* PNG bytes of an image layer ("" = the main layer): 8-bit grey for 1-channel layers, RGBA for 4-channel ones (8 bits
|
|
@@ -13,32 +14,79 @@ export function imagePNG(img, layer = '', deflate) {
|
|
|
13
14
|
return encodeGreyPNG(bytes, img.width, img.height, 8, deflate);
|
|
14
15
|
return encodePNG(bytes, img.width, img.height, deflate ?? zlibStored);
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
export
|
|
18
|
-
|
|
17
|
+
/** How a PNG's channels become layers: see ImageFromPNGOptions. */
|
|
18
|
+
export const PNG_LAYERS = ['auto', 'grey', 'rgba'];
|
|
19
|
+
/** Display (sRGB-encoded) value of a linear one (io/gltf.ts converts back and forth for glTF's linear factors). */
|
|
20
|
+
const toDisplay = (c) => (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
21
|
+
/** An image volume from a decoded PNG: values = samples / maxValue (16-bit files keep their precision). */
|
|
22
|
+
export function imageFromPNG(png, name, o = {}) {
|
|
23
|
+
const n = png.width * png.height, ch = png.channels, d = png.data, max = png.maxValue || 255, k = 1 / max;
|
|
24
|
+
// samples are integers 0..max: tables give every value once (alpha is never converted)
|
|
25
|
+
const lin = new Float32Array(max + 1);
|
|
26
|
+
for (let s = 0; s <= max; s++)
|
|
27
|
+
lin[s] = s * k;
|
|
28
|
+
const col = o.srgb === false ? Float32Array.from(lin, (_, s) => toDisplay(s * k)) : lin;
|
|
19
29
|
const img = new Image(png.width, png.height, name);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
const mode = o.layers ?? 'auto';
|
|
31
|
+
if (mode === 'grey' || (mode === 'auto' && ch <= 2)) {
|
|
32
|
+
const C = new Float32Array(n);
|
|
33
|
+
if (ch <= 2)
|
|
34
|
+
for (let i = 0; i < n; i++)
|
|
35
|
+
C[i] = col[d[i * ch]];
|
|
36
|
+
else
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
const p = i * ch;
|
|
39
|
+
C[i] = col[d[p]] * LUMA[0] + col[d[p + 1]] * LUMA[1] + col[d[p + 2]] * LUMA[2];
|
|
40
|
+
}
|
|
41
|
+
img.set('C', C);
|
|
42
|
+
if (ch === 2 || ch === 4) {
|
|
43
|
+
const A = new Float32Array(n);
|
|
44
|
+
for (let i = 0; i < n; i++)
|
|
45
|
+
A[i] = lin[d[i * ch + ch - 1]];
|
|
46
|
+
img.set('A', A);
|
|
47
|
+
}
|
|
48
|
+
return img;
|
|
25
49
|
}
|
|
26
50
|
const a = new Float32Array(n * 4);
|
|
27
51
|
for (let i = 0; i < n; i++) {
|
|
28
|
-
const
|
|
29
|
-
if (ch
|
|
30
|
-
a[
|
|
31
|
-
a[i * 4 + 3] = d[o + 1] * k;
|
|
32
|
-
}
|
|
52
|
+
const p = i * ch, q = i * 4;
|
|
53
|
+
if (ch <= 2)
|
|
54
|
+
a[q] = a[q + 1] = a[q + 2] = col[d[p]];
|
|
33
55
|
else {
|
|
34
|
-
a[
|
|
35
|
-
a[
|
|
36
|
-
a[
|
|
37
|
-
a[i * 4 + 3] = ch === 4 ? d[o + 3] * k : 1;
|
|
56
|
+
a[q] = col[d[p]];
|
|
57
|
+
a[q + 1] = col[d[p + 1]];
|
|
58
|
+
a[q + 2] = col[d[p + 2]];
|
|
38
59
|
}
|
|
60
|
+
a[q + 3] = ch === 2 || ch === 4 ? lin[d[p + ch - 1]] : 1;
|
|
39
61
|
}
|
|
40
62
|
return img.set('C', a);
|
|
41
63
|
}
|
|
64
|
+
/** The image name a file gives: its name without folders and extension ("inputs/brick.png" → "brick"). */
|
|
65
|
+
export function imageNameOf(path) {
|
|
66
|
+
let p = String(path);
|
|
67
|
+
if (/^\w+:\/\//.test(p))
|
|
68
|
+
p = p.split(/[?#]/)[0];
|
|
69
|
+
return baseOf(p.replace(/[\\/]+$/, '')).replace(/\.[^.]*$/, '') || 'image';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* An image volume from the bytes of the PNG file `path` (the image_file node and the file node's .png path), named
|
|
73
|
+
* `name` or after the file (imageNameOf). Errors name the file; images over MAX_PIXELS are refused before decoding.
|
|
74
|
+
*/
|
|
75
|
+
export function imageFromPNGFile(bytes, path, o = {}) {
|
|
76
|
+
let png;
|
|
77
|
+
try {
|
|
78
|
+
png = decodePNG(bytes, MAX_PIXELS);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
throw new Error(`"${path}" cannot be read as a PNG image: ${e?.message ?? e}`);
|
|
82
|
+
}
|
|
83
|
+
return imageFromPNG(png, o.name || imageNameOf(path), o);
|
|
84
|
+
}
|
|
85
|
+
/** imageFromPNGFile on a file read through `readFile` (usually Host.readFile). */
|
|
86
|
+
export async function readImageFile(readFile, path, o = {}) {
|
|
87
|
+
const bytes = await readFile(path).catch((e) => { throw new Error(`cannot read "${path}": ${e?.message ?? e}`); });
|
|
88
|
+
return imageFromPNGFile(bytes, path, o);
|
|
89
|
+
}
|
|
42
90
|
/** A file-name-safe version of an image name ("" → "image"). */
|
|
43
91
|
export function safeFileName(name) {
|
|
44
92
|
return String(name).replace(/[^A-Za-z0-9_.-]+/g, '_').replace(/^\.+/, '') || 'image';
|