partforge 0.123.0 → 0.125.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 +3 -2
- package/docs/AUTHORING-PARTS.md +72 -36
- package/package.json +1 -1
- package/src/framework/annotate/annotate-mode.js +6 -1
- package/src/framework/app.css +69 -57
- package/src/framework/chrome.css +38 -25
- package/src/framework/cutaway-gizmo.js +2 -1
- package/src/framework/cutaway.js +33 -0
- package/src/framework/geometry/threemf.js +3 -2
- package/src/framework/lint/rules-materials.js +3 -3
- package/src/framework/materials/assets/SOURCES.md +1 -1
- package/src/framework/materials/assets/ground-pei.jpg +0 -0
- package/src/framework/materials/contact-shadow.js +28 -1
- package/src/framework/materials/environment.js +16 -4
- package/src/framework/materials/patterns.js +58 -7
- package/src/framework/materials/physical.js +1 -0
- package/src/framework/materials/presets.js +9 -9
- package/src/framework/materials/print-bed.js +17 -0
- package/src/framework/materials/resolve.js +38 -14
- package/src/framework/mount.js +111 -44
- package/src/framework/projection.js +48 -1
- package/src/framework/view-state.js +4 -10
- package/src/framework/view-style-controls.js +298 -0
- package/src/framework/view-style-state.js +27 -0
- package/src/framework/viewcube/viewcube-controls.js +23 -64
- package/src/framework/viewcube/viewcube-mode.js +5 -3
- package/src/framework/viewer.js +637 -51
- package/src/parts/material-swatches.js +8 -1
- package/types/index.d.ts +14 -8
- package/src/framework/realistic-controls.js +0 -78
|
@@ -110,7 +110,12 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
110
110
|
// While a part moves (lowRes) only the soft layer is redrawn and the tight
|
|
111
111
|
// one is hidden: an outline left where the part WAS would be wrong, and the
|
|
112
112
|
// settle render that follows brings it back.
|
|
113
|
-
|
|
113
|
+
//
|
|
114
|
+
// `clippingPlanes` (world space) clip the casters the way the cutaway clips
|
|
115
|
+
// what is on screen. The depth pass draws every caster with the one override
|
|
116
|
+
// material, which carries none of the casters' own clipping planes, so
|
|
117
|
+
// without this the half the cutaway removed still cast its shadow.
|
|
118
|
+
function render(scene, casters, { lowRes = false, clippingPlanes = null } = {}) {
|
|
114
119
|
const bg = scene.background;
|
|
115
120
|
const overrideBefore = scene.overrideMaterial;
|
|
116
121
|
const clear = renderer.getClearAlpha();
|
|
@@ -123,6 +128,7 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
123
128
|
renderer.setClearAlpha(0);
|
|
124
129
|
for (const l of layers) {
|
|
125
130
|
if (lowRes && l.spec.name === "tight") continue;
|
|
131
|
+
setClipping(l.depthMaterial, clippingPlanes);
|
|
126
132
|
scene.overrideMaterial = l.depthMaterial;
|
|
127
133
|
renderer.setRenderTarget(l.rt);
|
|
128
134
|
renderer.render(scene, cam);
|
|
@@ -143,6 +149,27 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
// A new plane count needs a new program; the same planes moved do not (three
|
|
153
|
+
// reads their values every frame).
|
|
154
|
+
//
|
|
155
|
+
// A clipped pass also draws BOTH sides, depth-tested. The pass looks UP at
|
|
156
|
+
// the casters, and a cut that removes a part's underside (a kept side facing
|
|
157
|
+
// up, e.g. a top-view cut after Flip) leaves no downward-facing faces at the
|
|
158
|
+
// cut — from below only back faces remain, which FrontSide culls, and the
|
|
159
|
+
// shadow shrinks to a faint ring of side walls. The interior back faces fill
|
|
160
|
+
// the silhouette instead, and the depth test keeps the LOWEST surface under
|
|
161
|
+
// each texel (the one the ground's darkness is measured from) rather than
|
|
162
|
+
// whichever face drew last. Unclipped, the pass is exactly as before:
|
|
163
|
+
// front faces, no depth test.
|
|
164
|
+
function setClipping(material, planes) {
|
|
165
|
+
const next = planes?.length ? planes : null;
|
|
166
|
+
const side = next ? THREE.DoubleSide : THREE.FrontSide;
|
|
167
|
+
if ((material.clippingPlanes?.length ?? 0) !== (next?.length ?? 0) || material.side !== side) material.needsUpdate = true;
|
|
168
|
+
material.clippingPlanes = next;
|
|
169
|
+
material.side = side;
|
|
170
|
+
material.depthTest = material.depthWrite = !!next;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
return {
|
|
147
174
|
group,
|
|
148
175
|
render,
|
|
@@ -94,21 +94,32 @@ export async function loadEnvironmentRig(renderer, requestedId, { loadHdr, loadT
|
|
|
94
94
|
if (env.ground.bed) {
|
|
95
95
|
const material = groundMaterial(tex, rough, normal, { ...env.ground, bed: true });
|
|
96
96
|
const bed = createPrintBed({ pei: material, tileMm: env.ground.tileMm ?? env.ground.sizeMm, createCanvas });
|
|
97
|
-
// An enclosure's LED bar: one hard overhead light,
|
|
98
|
-
//
|
|
97
|
+
// An enclosure's LED bar: one hard overhead light, aimed at the bed's own
|
|
98
|
+
// origin. The LIGHT itself is deliberately not a child of bed.object:
|
|
99
|
+
// updateForCamera hides that whole group from below, and a hidden parent
|
|
100
|
+
// stops three from rendering its children too — the light would go dark
|
|
101
|
+
// exactly when the part needs it lit from underneath. It rides in
|
|
102
|
+
// `lights` instead and is repositioned by hand wherever the bed moves.
|
|
103
|
+
// The target CAN stay a child (matrixWorld updates regardless of
|
|
104
|
+
// visibility), which is what lets it keep tracking the bed for free.
|
|
99
105
|
let keyLight = null;
|
|
100
106
|
if (env.keyLight) {
|
|
101
107
|
keyLight = new THREE.DirectionalLight(0xffffff, env.keyLight.intensity);
|
|
102
|
-
|
|
103
|
-
bed.object.add(keyLight, keyLight.target);
|
|
108
|
+
bed.object.add(keyLight.target);
|
|
104
109
|
}
|
|
105
110
|
return {
|
|
106
111
|
id, exposure: env.exposure, envMap, background, backgroundBlurriness, backgroundIntensity,
|
|
107
112
|
rotationY: ((env.rotationDeg ?? 0) * Math.PI) / 180,
|
|
108
113
|
ground: bed.object,
|
|
114
|
+
lights: keyLight ? [keyLight] : [],
|
|
109
115
|
shadow,
|
|
116
|
+
updateForCamera(camera) { bed.updateForCamera(camera); },
|
|
110
117
|
setGround({ y, centerX = 0, centerZ = 0, radius, footprintMm = radius }) {
|
|
111
118
|
bed.place({ y, centerX, centerZ, footprintMm });
|
|
119
|
+
if (keyLight) {
|
|
120
|
+
const p = bed.object.position; // the fixed offset off the bed's origin
|
|
121
|
+
keyLight.position.set(p.x + env.keyLight.direction[0], p.y + env.keyLight.direction[1], p.z + env.keyLight.direction[2]);
|
|
122
|
+
}
|
|
112
123
|
shadow.group.position.set(centerX, y, centerZ);
|
|
113
124
|
// The shadow plane never hangs past the plate's edge into thin air.
|
|
114
125
|
const s = Math.min(Math.max(radius * 3, 20), bed.sizeMm);
|
|
@@ -147,6 +158,7 @@ export async function loadEnvironmentRig(renderer, requestedId, { loadHdr, loadT
|
|
|
147
158
|
backgroundIntensity,
|
|
148
159
|
rotationY: ((env.rotationDeg ?? 0) * Math.PI) / 180,
|
|
149
160
|
ground,
|
|
161
|
+
lights: [],
|
|
150
162
|
shadow,
|
|
151
163
|
setGround,
|
|
152
164
|
dispose() {
|
|
@@ -45,10 +45,22 @@ uniform sampler2D pfNormalMap;
|
|
|
45
45
|
uniform sampler2D pfRoughMap;
|
|
46
46
|
uniform float pfRoughMean;
|
|
47
47
|
uniform float pfNormalStrength;
|
|
48
|
+
uniform vec3 pfGrainSwap;
|
|
48
49
|
varying vec3 vPfNmX;
|
|
49
50
|
varying vec3 vPfNmY;
|
|
50
51
|
varying vec3 vPfNmZ;
|
|
51
52
|
vec3 pfTriplanarWeights(vec3 n) { vec3 w = pow(abs(n), vec3(4.0)); return w / (w.x + w.y + w.z); }
|
|
53
|
+
// One grain direction on every face: each projection's UV pair is transposed
|
|
54
|
+
// where pfGrainSwap says so (grainSwaps below), so the texture's grain lands on
|
|
55
|
+
// the sub-part's grain axis. A transpose is a mirror, not a rotation, which is
|
|
56
|
+
// what keeps the normal map exact: its slopes just swap axes (t.yx), no signs.
|
|
57
|
+
vec2 pfGrainUv(vec2 ab, float s) { return mix(ab, ab.yx, s); }
|
|
58
|
+
vec3 pfTriplanarWood(sampler2D map, vec3 p, vec3 n) {
|
|
59
|
+
vec3 w = pfTriplanarWeights(n);
|
|
60
|
+
return texture2D(map, pfGrainUv(p.yz, pfGrainSwap.x) / pfPatternScale).rgb * w.x
|
|
61
|
+
+ texture2D(map, pfGrainUv(p.xz, pfGrainSwap.y) / pfPatternScale).rgb * w.y
|
|
62
|
+
+ texture2D(map, pfGrainUv(p.xy, pfGrainSwap.z) / pfPatternScale).rgb * w.z;
|
|
63
|
+
}
|
|
52
64
|
// Triplanar tangent-space normal map, whiteout-blended (Golus, "Normal Mapping
|
|
53
65
|
// for a Triplanar Shader"). Each projection's map is read as a height field over
|
|
54
66
|
// the two object axes its UVs run along, so its tangent normal (x, y) is a slope
|
|
@@ -59,10 +71,13 @@ vec3 pfTriplanarWeights(vec3 n) { vec3 w = pow(abs(n), vec3(4.0)); return w / (w
|
|
|
59
71
|
// Poly Haven "nor_gl" convention. Returns an OBJECT-space unit normal.
|
|
60
72
|
vec3 pfTriplanarNormal(vec3 p, vec3 n, float scale) {
|
|
61
73
|
vec3 w = pfTriplanarWeights(n);
|
|
62
|
-
vec3 tx = texture2D(pfNormalMap, p.yz / scale).xyz * 2.0 - 1.0;
|
|
63
|
-
vec3 ty = texture2D(pfNormalMap, p.xz / scale).xyz * 2.0 - 1.0;
|
|
64
|
-
vec3 tz = texture2D(pfNormalMap, p.xy / scale).xyz * 2.0 - 1.0;
|
|
65
|
-
|
|
74
|
+
vec3 tx = texture2D(pfNormalMap, pfGrainUv(p.yz, pfGrainSwap.x) / scale).xyz * 2.0 - 1.0;
|
|
75
|
+
vec3 ty = texture2D(pfNormalMap, pfGrainUv(p.xz, pfGrainSwap.y) / scale).xyz * 2.0 - 1.0;
|
|
76
|
+
vec3 tz = texture2D(pfNormalMap, pfGrainUv(p.xy, pfGrainSwap.z) / scale).xyz * 2.0 - 1.0;
|
|
77
|
+
// a transposed sample's slopes run along the swapped axes
|
|
78
|
+
tx.xy = pfGrainUv(tx.xy, pfGrainSwap.x) * pfNormalStrength;
|
|
79
|
+
ty.xy = pfGrainUv(ty.xy, pfGrainSwap.y) * pfNormalStrength;
|
|
80
|
+
tz.xy = pfGrainUv(tz.xy, pfGrainSwap.z) * pfNormalStrength;
|
|
66
81
|
tx = vec3(tx.xy + n.yz, abs(tx.z) * n.x); // uv = (y, z), face axis x
|
|
67
82
|
ty = vec3(ty.xy + n.xz, abs(ty.z) * n.y); // uv = (x, z), face axis y
|
|
68
83
|
tz = vec3(tz.xy + n.xy, abs(tz.z) * n.z); // uv = (x, y), face axis z
|
|
@@ -102,11 +117,12 @@ const FRAG_BODY = {
|
|
|
102
117
|
// author tinted it, physical.js — and the roughness map varies roughnessFactor
|
|
103
118
|
// around the preset's value (divided by the map's own mean, so the preset sets
|
|
104
119
|
// the average). Mipmaps (and RepeatWrapping) do the far-distance filtering.
|
|
120
|
+
// All three maps follow the grain axis (pfTriplanarWood / setGrainAxis).
|
|
105
121
|
wood: `
|
|
106
122
|
{
|
|
107
123
|
vec3 pfN = normalize(vPfObjNormal);
|
|
108
|
-
diffuseColor.rgb *=
|
|
109
|
-
float r =
|
|
124
|
+
diffuseColor.rgb *= pfTriplanarWood(pfPatternMap, vPfObjPos, pfN);
|
|
125
|
+
float r = pfTriplanarWood(pfRoughMap, vPfObjPos, pfN).g;
|
|
110
126
|
roughnessFactor = clamp(roughnessFactor * r / pfRoughMean, 0.02, 1.0);
|
|
111
127
|
}`,
|
|
112
128
|
// Carbon samples a luminance MASK (raw, not sRGB-decoded: physical.js) and
|
|
@@ -164,7 +180,40 @@ const FRAG_NORMAL = {
|
|
|
164
180
|
}`,
|
|
165
181
|
};
|
|
166
182
|
|
|
167
|
-
|
|
183
|
+
// Wood grain: real wood has ONE grain direction, the board's long axis, but a
|
|
184
|
+
// triplanar projection with fixed UVs (X-faces (y, z), Y-faces (x, z), Z-faces
|
|
185
|
+
// (x, y)) lands the texture's grain on a different object axis per face — on a
|
|
186
|
+
// block, adjacent faces showed grain at right angles. So each sub-part gets a
|
|
187
|
+
// grain axis (its longest object-space bounding-box axis, X on a tie) and every
|
|
188
|
+
// projection whose face CONTAINS that axis is transposed where needed so the
|
|
189
|
+
// texture's grain runs along it. Faces across the axis are end grain; there is
|
|
190
|
+
// no end-grain texture, so they keep the default sample.
|
|
191
|
+
//
|
|
192
|
+
// `grain` is which texture axis a scan's figure runs along ("u" horizontal, "v"
|
|
193
|
+
// vertical in the image), declared per texture set in presets.js.
|
|
194
|
+
const PLANE_AXES = [[1, 2], [0, 2], [0, 1]]; // each projection's (u, v) object axes
|
|
195
|
+
export function grainAxisFor(box) {
|
|
196
|
+
if (!box || box.isEmpty?.()) return 0;
|
|
197
|
+
const s = [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
|
|
198
|
+
const eps = Math.max(...s) * 1e-6;
|
|
199
|
+
let axis = 0;
|
|
200
|
+
for (let i = 1; i < 3; i++) if (s[i] > s[axis] + eps) axis = i;
|
|
201
|
+
return axis;
|
|
202
|
+
}
|
|
203
|
+
export function grainSwaps(axis, grain = "u") {
|
|
204
|
+
const want = grain === "v" ? 1 : 0;
|
|
205
|
+
return PLANE_AXES.map(([u], face) => (face === axis ? 0 : (axis === u ? 0 : 1) !== want ? 1 : 0));
|
|
206
|
+
}
|
|
207
|
+
// Point a wood material (and every clone sharing its uniforms) at a grain axis.
|
|
208
|
+
// A no-op on any other material.
|
|
209
|
+
export function setGrainAxis(material, axis) {
|
|
210
|
+
const u = material?.userData?.patternUniforms?.pfGrainSwap;
|
|
211
|
+
if (!u) return;
|
|
212
|
+
material.userData.pfGrainAxis = axis;
|
|
213
|
+
u.value.fromArray(grainSwaps(axis, material.userData.pfGrain));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function applyPattern(material, { kind, scale = 1, printFrame, texture, normalMap, roughnessMap, roughnessMean = 0.5, normalScale = 1, grain = "u" } = {}) {
|
|
168
217
|
if (!kind || !FRAG_BODY[kind]) return material;
|
|
169
218
|
const wood = kind === "wood";
|
|
170
219
|
const uniforms = {
|
|
@@ -176,9 +225,11 @@ export function applyPattern(material, { kind, scale = 1, printFrame, texture, n
|
|
|
176
225
|
pfRoughMap: { value: roughnessMap ?? null },
|
|
177
226
|
pfRoughMean: { value: roughnessMean },
|
|
178
227
|
pfNormalStrength: { value: normalScale },
|
|
228
|
+
pfGrainSwap: { value: new THREE.Vector3() },
|
|
179
229
|
} : {}),
|
|
180
230
|
};
|
|
181
231
|
material.userData.patternUniforms = uniforms;
|
|
232
|
+
if (wood) { material.userData.pfGrain = grain; setGrainAxis(material, 0); }
|
|
182
233
|
material.onBeforeCompile = (shader) => {
|
|
183
234
|
Object.assign(shader.uniforms, uniforms);
|
|
184
235
|
shader.vertexShader = shader.vertexShader
|
|
@@ -8,15 +8,12 @@
|
|
|
8
8
|
// converted to sRGB. Adding a preset is one record here plus a look at the
|
|
9
9
|
// contact sheet (materials.html) in every environment.
|
|
10
10
|
|
|
11
|
-
export const DEFAULT_PRESET_ID = "default";
|
|
12
|
-
|
|
13
11
|
const P = (id, label, category, use, fields) => ({ id, label, category, use, tintable: false, ...fields });
|
|
14
12
|
|
|
15
13
|
export const PRESETS = {
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
{ color: 0x9fb4cc, metalness: 0.25, roughness: 0.55 }),
|
|
14
|
+
// Every record here is a documented, nameable material. A sub-part that names
|
|
15
|
+
// none is drawn by resolve.js (the blue-grey CAD look; a PLA print in that
|
|
16
|
+
// colour in realistic mode) — there is no hidden "default" record any more.
|
|
20
17
|
|
|
21
18
|
"machined-aluminum": P("machined-aluminum", "Machined aluminium", "aluminium", "Bare CNC-milled aluminium, fine tool marks, satin sheen.",
|
|
22
19
|
{ color: 0xf5f6f6, metalness: 1, roughness: 0.32 }),
|
|
@@ -77,13 +74,16 @@ export const PRESETS = {
|
|
|
77
74
|
// `roughnessMean` is the roughness map's mean, so the preset `roughness` (or an
|
|
78
75
|
// override) sets the AVERAGE roughness and the map only varies it. `normalScale`
|
|
79
76
|
// strengthens the normal map: these scans' tangent normals barely leave +Z (a
|
|
80
|
-
// ~1% tilt), which reads as flat under an environment map.
|
|
77
|
+
// ~1% tilt), which reads as flat under an environment map. `grain` is the
|
|
78
|
+
// texture axis the scan's figure runs along — oak's runs up the image ("v"),
|
|
79
|
+
// walnut's across it ("u") — so patterns.js can lay it along each sub-part's
|
|
80
|
+
// long axis.
|
|
81
81
|
oak: P("oak", "Oak", "natural", "Light oak with open grain.",
|
|
82
82
|
{ color: 0xa17e57, metalness: 0, roughness: 0.65, pattern: "wood", textureScale: 250,
|
|
83
|
-
textures: { color: "pattern-oak-color.jpg", normal: "pattern-oak-normal.jpg", roughness: "pattern-oak-rough.jpg", roughnessMean: 0.53, normalScale: 2.5 } }),
|
|
83
|
+
textures: { color: "pattern-oak-color.jpg", normal: "pattern-oak-normal.jpg", roughness: "pattern-oak-rough.jpg", roughnessMean: 0.53, normalScale: 2.5, grain: "v" } }),
|
|
84
84
|
walnut: P("walnut", "Walnut", "natural", "Dark oiled walnut.",
|
|
85
85
|
{ color: 0x5d2a18, metalness: 0, roughness: 0.5, clearcoat: 0.3, clearcoatRoughness: 0.4, pattern: "wood", textureScale: 400,
|
|
86
|
-
textures: { color: "pattern-walnut-color.jpg", normal: "pattern-walnut-normal.jpg", roughness: "pattern-walnut-rough.jpg", roughnessMean: 0.628, normalScale: 3 } }),
|
|
86
|
+
textures: { color: "pattern-walnut-color.jpg", normal: "pattern-walnut-normal.jpg", roughness: "pattern-walnut-rough.jpg", roughnessMean: 0.628, normalScale: 3, grain: "u" } }),
|
|
87
87
|
"carbon-fiber": P("carbon-fiber", "Carbon fibre", "natural", "2x2 twill carbon fibre under clear coat.",
|
|
88
88
|
{ color: 0x1b1c1e, metalness: 0.2, roughness: 0.35, clearcoat: 1, clearcoatRoughness: 0.05, pattern: "carbon", textureScale: 48 }),
|
|
89
89
|
};
|
|
@@ -154,6 +154,14 @@ export function createPrintBed({ pei, tileMm, createCanvas }) {
|
|
|
154
154
|
ink.scale.set(size, 1, size);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Below this, the whole plate hides — like the other environments' single-
|
|
158
|
+
// sided ground discs, which cull away once the camera passes their back
|
|
159
|
+
// face. The epsilon keeps a camera sitting exactly at plate height from
|
|
160
|
+
// flickering the bed in and out as float error nudges it either side.
|
|
161
|
+
const HIDE_EPSILON_MM = 0.05;
|
|
162
|
+
const _camWorld = new THREE.Vector3();
|
|
163
|
+
const _topWorld = new THREE.Vector3();
|
|
164
|
+
|
|
157
165
|
return {
|
|
158
166
|
object: group,
|
|
159
167
|
get sizeMm() { return size; },
|
|
@@ -161,6 +169,15 @@ export function createPrintBed({ pei, tileMm, createCanvas }) {
|
|
|
161
169
|
setSize(bedSizeFor(footprintMm));
|
|
162
170
|
group.position.set(centerX, y - 0.01, centerZ);
|
|
163
171
|
},
|
|
172
|
+
// Called once per render, with the camera actually doing the rendering
|
|
173
|
+
// (the live camera, or an offscreen capture's) — never a cached "current
|
|
174
|
+
// view" — so a canonical "from below" render also sees the part rather
|
|
175
|
+
// than the plate's underside.
|
|
176
|
+
updateForCamera(camera) {
|
|
177
|
+
camera.getWorldPosition(_camWorld);
|
|
178
|
+
group.getWorldPosition(_topWorld); // local y = 0 is the plate's top surface
|
|
179
|
+
group.visible = _camWorld.y >= _topWorld.y - HIDE_EPSILON_MM;
|
|
180
|
+
},
|
|
164
181
|
dispose() {
|
|
165
182
|
plate.geometry.dispose(); ink.geometry.dispose();
|
|
166
183
|
edge.dispose(); inkMat.dispose(); inkTex?.dispose();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/framework/materials/resolve.js
|
|
2
2
|
// Pure resolution of a sub-part's `display` block against the library. Never
|
|
3
|
-
// throws and never rejects a value: an unknown preset falls back to the
|
|
3
|
+
// throws and never rejects a value: an unknown preset falls back to the
|
|
4
|
+
// no-material look (below),
|
|
4
5
|
// an out-of-range override clamps, an unknown key is ignored — and every one of
|
|
5
6
|
// those is returned as an `issue` so lint can warn about it. Appearance must
|
|
6
7
|
// never be able to fail a build.
|
|
7
|
-
import { PRESETS
|
|
8
|
+
import { PRESETS } from "./presets.js";
|
|
8
9
|
import { ENVIRONMENTS, DEFAULT_ENVIRONMENT_ID } from "./environments.js";
|
|
9
10
|
|
|
10
11
|
export const OVERRIDE_RANGES = {
|
|
@@ -20,16 +21,34 @@ export const DISPLAY_KEYS = ["color", "opacity", "material", ...Object.keys(OVER
|
|
|
20
21
|
|
|
21
22
|
const isColor = (v) => Number.isInteger(v) && v >= 0 && v <= 0xffffff;
|
|
22
23
|
|
|
24
|
+
// A sub-part with no usable material — none named, or one the library does not
|
|
25
|
+
// know. The CAD view draws it in the viewer's neutral blue-grey drafting look
|
|
26
|
+
// (the look every part had before the library existed; viewer.js's base
|
|
27
|
+
// material and threemf.js's uncoloured-object colour are the same literal).
|
|
28
|
+
// Realistic mode draws it as a PLA print — the pla-print finish, layer lines
|
|
29
|
+
// and all — in that same colour, or the part's own `color`, so a part is the
|
|
30
|
+
// same colour in both views, just printed. `declaresMaterials` still counts
|
|
31
|
+
// only a NAMED material: this default never makes a part "declare" one.
|
|
32
|
+
export const NO_MATERIAL_COLOR = 0x9fb4cc;
|
|
33
|
+
export const NO_MATERIAL_PRESET_ID = "pla-print";
|
|
34
|
+
const NO_MATERIAL_CAD = { color: NO_MATERIAL_COLOR, metalness: 0.25, roughness: 0.55 };
|
|
35
|
+
|
|
36
|
+
const knownMaterial = (m) => typeof m === "string" && Object.hasOwn(PRESETS, m);
|
|
37
|
+
|
|
23
38
|
export function resolveMaterial(display) {
|
|
24
39
|
const d = display && typeof display === "object" ? display : {};
|
|
25
40
|
const issues = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
if (d.material != null && !knownMaterial(d.material)) issues.push({ kind: "unknown-material", key: "material", value: d.material });
|
|
42
|
+
const preset = knownMaterial(d.material) ? PRESETS[d.material] : PRESETS[NO_MATERIAL_PRESET_ID];
|
|
43
|
+
const baseColor = knownMaterial(d.material) ? preset.color : NO_MATERIAL_COLOR;
|
|
44
|
+
return { preset, ...applyDisplay(d, preset, baseColor, issues) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A preset record's numbers with the display block's colour, opacity and clamped
|
|
48
|
+
// overrides applied. Pushes onto `issues`; returns them with the params.
|
|
49
|
+
function applyDisplay(d, preset, baseColor, issues) {
|
|
31
50
|
const params = {
|
|
32
|
-
color:
|
|
51
|
+
color: baseColor,
|
|
33
52
|
metalness: preset.metalness,
|
|
34
53
|
roughness: preset.roughness,
|
|
35
54
|
clearcoat: preset.clearcoat ?? 0,
|
|
@@ -63,7 +82,7 @@ export function resolveMaterial(display) {
|
|
|
63
82
|
}
|
|
64
83
|
// `tinted`: the author named a colour. A textured preset's colour map carries
|
|
65
84
|
// its own colour, so only an explicit tint multiplies it (physical.js).
|
|
66
|
-
return {
|
|
85
|
+
return { params, issues, tinted: isColor(d.color) };
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
// CAD-view appearance: the resolved colour on the CAD MeshStandardMaterial,
|
|
@@ -73,11 +92,15 @@ export function resolveMaterial(display) {
|
|
|
73
92
|
const MIN_CAD_LUMA = 60; // 0..255, Rec. 709
|
|
74
93
|
export function cadAppearance(display) {
|
|
75
94
|
const d = display && typeof display === "object" ? display : {};
|
|
76
|
-
const { params } = resolveMaterial(d);
|
|
77
95
|
if (d.material == null) {
|
|
78
|
-
//
|
|
79
|
-
|
|
96
|
+
// The drafting look, untouched by overrides — as it always was.
|
|
97
|
+
const { params } = resolveMaterial(d);
|
|
98
|
+
return { color: params.color, metalness: NO_MATERIAL_CAD.metalness, roughness: NO_MATERIAL_CAD.roughness, opacity: params.opacity };
|
|
80
99
|
}
|
|
100
|
+
// An unknown material flattens the drafting look rather than the realistic
|
|
101
|
+
// PLA default, exactly as it did when it fell back to the retired "default"
|
|
102
|
+
// preset.
|
|
103
|
+
const { params } = knownMaterial(d.material) ? resolveMaterial(d) : applyDisplay(d, NO_MATERIAL_CAD, NO_MATERIAL_COLOR, []);
|
|
81
104
|
return {
|
|
82
105
|
color: liftLuma(params.color, MIN_CAD_LUMA),
|
|
83
106
|
metalness: Math.min(params.metalness, 0.5),
|
|
@@ -97,8 +120,9 @@ function liftLuma(color, min) {
|
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
// The colour a 3MF object carries: the explicit tint, else the preset's own
|
|
100
|
-
// colour
|
|
101
|
-
//
|
|
123
|
+
// colour (the blue-grey for an unknown material, never the realistic PLA
|
|
124
|
+
// default's). Null when the sub-part says nothing about appearance, which is
|
|
125
|
+
// what keeps an appearance-free export byte-for-byte unchanged.
|
|
102
126
|
export function printColor(display) {
|
|
103
127
|
if (!display || typeof display !== "object") return null;
|
|
104
128
|
if (isColor(display.color)) return display.color;
|