@selvajs/visualization 1.6.0-beta.9 → 1.6.1-beta.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.
@@ -191,8 +191,8 @@ const LOOK_PRESETS = {
191
191
  sunlightIntensity: .14,
192
192
  background: 658962,
193
193
  bloom: {
194
- strength: .26,
195
- radius: .5,
194
+ strength: .2,
195
+ radius: .3,
196
196
  threshold: 1.8
197
197
  },
198
198
  emissiveLights: true,
@@ -312,6 +312,33 @@ function observeMaxAnisotropy(observe) {
312
312
  observe(maxAnisotropy);
313
313
  return () => observers.delete(observe);
314
314
  }
315
+ // src/shared/glass.ts
316
+ /**
317
+ * `userData` key marking a material the parser built as glass. AO, the edge pass and shadow maps
318
+ * skip it: each treats a surface as opaque, so glass would darken, outline and shade what it shows.
319
+ */
320
+ const GLASS = "selvaGlass";
321
+ function isGlass(material) {
322
+ if (!material) return false;
323
+ return Array.isArray(material) ? material.some((m) => m.userData[GLASS] === true) : material.userData[GLASS] === true;
324
+ }
325
+ /**
326
+ * Hides every visible mesh drawn in glass under `root` and returns the call that shows them again.
327
+ * For passes that redraw the scene with an override material, which would otherwise draw glass as
328
+ * a solid surface.
329
+ */
330
+ function hideGlass(root) {
331
+ const hidden = [];
332
+ root.traverse((object) => {
333
+ if (object.visible && isGlass(object.material)) {
334
+ object.visible = false;
335
+ hidden.push(object);
336
+ }
337
+ });
338
+ return () => {
339
+ for (const object of hidden) object.visible = true;
340
+ };
341
+ }
315
342
  // src/shared/gpu-dispose.ts
316
343
  /** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */
317
344
  function disposeMaterial(material) {
@@ -369,6 +396,12 @@ Object.defineProperty(exports, "ErrorCodes", {
369
396
  return ErrorCodes;
370
397
  }
371
398
  });
399
+ Object.defineProperty(exports, "GLASS", {
400
+ enumerable: true,
401
+ get: function() {
402
+ return GLASS;
403
+ }
404
+ });
372
405
  Object.defineProperty(exports, "LOOK_PRESETS", {
373
406
  enumerable: true,
374
407
  get: function() {
@@ -423,6 +456,18 @@ Object.defineProperty(exports, "getLogger", {
423
456
  return getLogger;
424
457
  }
425
458
  });
459
+ Object.defineProperty(exports, "hideGlass", {
460
+ enumerable: true,
461
+ get: function() {
462
+ return hideGlass;
463
+ }
464
+ });
465
+ Object.defineProperty(exports, "isGlass", {
466
+ enumerable: true,
467
+ get: function() {
468
+ return isGlass;
469
+ }
470
+ });
426
471
  Object.defineProperty(exports, "lookEnvMapIntensity", {
427
472
  enumerable: true,
428
473
  get: function() {
@@ -466,4 +511,4 @@ Object.defineProperty(exports, "setLogger", {
466
511
  }
467
512
  });
468
513
 
469
- //# sourceMappingURL=gpu-dispose-fRI12OCL.cjs.map
514
+ //# sourceMappingURL=gpu-dispose-D66GqIWS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-dispose-D66GqIWS.cjs","names":["THREE","THREE","canDisposeMaterial","THREE","cachedSourceOf"],"sources":["../src/shared/errors.ts","../src/shared/logger.ts","../src/shared/looks.ts","../src/shared/geometry.ts","../src/shared/gpu-capabilities.ts","../src/shared/glass.ts","../src/shared/gpu-dispose.ts"],"sourcesContent":["/**\n * Errors for the visualization package. Replaces `@selvajs/compute`'s `RhinoComputeError`, which\n * mis-named failures on paths (e.g. the plugin WebSocket) that never touch Rhino.Compute. `code`\n * values match compute's so existing catch-sites keep working.\n */\n\nexport const ErrorCodes = {\n\t/** Structural check failed: bad magic bytes, out-of-window index, malformed metadata. */\n\tVALIDATION_ERROR: 'VALIDATION_ERROR',\n\tINVALID_STATE: 'INVALID_STATE',\n\t/** No `DecompressionStream`, no WebGL context, etc. */\n\tENVIRONMENT_ERROR: 'ENVIRONMENT_ERROR',\n\tINVALID_CONFIG: 'INVALID_CONFIG',\n\t/** Base64 input could not be decoded. */\n\tENCODING_ERROR: 'ENCODING_ERROR',\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR'\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nexport class VisualizationError extends Error {\n\tpublic readonly code: ErrorCode;\n\tpublic readonly context?: Record<string, unknown>;\n\tpublic readonly originalError?: Error;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: ErrorCode = ErrorCodes.UNKNOWN_ERROR,\n\t\toptions?: { context?: Record<string, unknown>; originalError?: Error }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'VisualizationError';\n\t\tthis.code = code;\n\t\tthis.context = options?.context;\n\t\tthis.originalError = options?.originalError;\n\t\tif (options?.originalError) {\n\t\t\t(this as { cause?: unknown }).cause = options.originalError;\n\t\t}\n\t}\n}\n","/**\n * Logging facility for the visualization package. Deliberately local rather than imported from\n * `@selvajs/compute` (logging isn't a compute concern). Mirrors compute's logger shape so a host\n * wanting one sink for both can call `setLogger(computeLogger.getLogger())`.\n */\n\nimport { VisualizationError, ErrorCodes } from './errors.js';\n\nexport interface Logger {\n\tdebug(message: string, ...args: unknown[]): void;\n\tinfo(message: string, ...args: unknown[]): void;\n\twarn(message: string, ...args: unknown[]): void;\n\terror(message: string, ...args: unknown[]): void;\n}\n\nclass NoOpLogger implements Logger {\n\tdebug(): void {}\n\tinfo(): void {}\n\twarn(): void {}\n\terror(): void {}\n}\n\nclass ConsoleLogger implements Logger {\n\tdebug(message: string, ...args: unknown[]): void {\n\t\t// eslint-disable-next-line no-console -- this class exists to be the console sink\n\t\tconsole.debug(message, ...args);\n\t}\n\n\tinfo(message: string, ...args: unknown[]): void {\n\t\tconsole.info(message, ...args);\n\t}\n\n\twarn(message: string, ...args: unknown[]): void {\n\t\tconsole.warn(message, ...args);\n\t}\n\n\terror(message: string, ...args: unknown[]): void {\n\t\tconsole.error(message, ...args);\n\t}\n}\n\n// Defaults to no-op: a library that writes to the console uninvited is a nuisance in a host with\n// its own logging. Opt in with setLogger or enableDebugLogging.\nlet internalLogger: Logger = new NoOpLogger();\n\n// Call per-use rather than caching — the sink is settable at any time.\nexport function getLogger(): Logger {\n\treturn internalLogger;\n}\n\n// Validates logger shape up front: failing here beats a confusing \"getLogger().debug is not a\n// function\" at some later, unrelated call site.\nexport function setLogger(logger: Logger | Console | null): void {\n\tif (logger === null) {\n\t\tinternalLogger = new NoOpLogger();\n\t\treturn;\n\t}\n\n\tconst missing = (['debug', 'info', 'warn', 'error'] as const).filter(\n\t\t(method) => typeof (logger as unknown as Record<string, unknown>)[method] !== 'function'\n\t);\n\tif (missing.length > 0) {\n\t\tthrow new VisualizationError(\n\t\t\t`Logger is missing required method(s): ${missing.join(', ')}. A logger must implement debug, info, warn and error.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { missingMethods: missing } }\n\t\t);\n\t}\n\n\tinternalLogger = logger as Logger;\n}\n\nexport function enableDebugLogging(): void {\n\tsetLogger(new ConsoleLogger());\n}\n","import * as THREE from 'three';\n\nimport type { Look, LookPreset, MaterialAppearanceOptions } from './types.js';\n\n/** The look applied when the caller passes no `look` option. */\nexport const DEFAULT_LOOK: Look = 'technical';\n\n/**\n * Single source of truth for both `applyDefaults` (construction) and `setLook` (runtime), so the two\n * can't drift.\n *\n * Every shaded look pairs a key light with contact shading (GTAO), because image-based lighting on\n * its own lights all faces of a box nearly equally — the result is a white silhouette with no\n * readable form. GTAO is a full-screen pass and does cost frames; `xray` skips it since there are no\n * opaque contacts left to shade.\n */\nexport const LOOK_PRESETS: Record<Look, LookPreset> = {\n\t// Fills shadows in (vs. showcase, which lets them fall off), but still key-lit: ACES plus a\n\t// moderate sun, with fill high enough that shadowed faces keep detail.\n\tstudio: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 0.85,\n\t\themisphereIntensity: 0.5,\n\t\tambientIntensity: 0.25,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 1.6\n\t},\n\t// The sun carries this look and the ambient/IBL only lift shadows off black. Fill was the thing\n\t// making it read as flat white paper: every source except the sun is orientation-independent, so\n\t// the more of them you stack the less the three faces of a corner differ.\n\ttechnical: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.55,\n\t\tenvironmentIntensity: 0.6,\n\t\themisphereIntensity: 0.25,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.2\n\t},\n\t// Fill pulled furthest DOWN and the sun pushed hardest, so light-to-shadow falloff is steepest —\n\t// a dramatic hero shot, unlike `studio` which fills shadows back in.\n\tshowcase: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1.15,\n\t\tenvMapIntensity: 1.4,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.2,\n\t\tambientIntensity: 0.1,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.6\n\t},\n\t// Every mesh forced to one near-white clay, so form and shadow carry the image instead of the\n\t// model's own colours. AO on: with all albedo differences gone, contact shading is the only thing\n\t// left separating touching parts — without it the model reads as one white blob.\n\tarctic: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.05,\n\t\tenvMapIntensity: 0.85,\n\t\tenvironmentIntensity: 1.1,\n\t\themisphereIntensity: 0.3,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xeef1f5,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.75,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// See-through blue-white with depth writes off, so interior geometry shows through the shell.\n\t// Exposure pushed up because stacked transparent layers darken fast; the sun is dialled back\n\t// because a hard highlight on a transparent shell hides what is behind it.\n\txray: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.3,\n\t\tenvMapIntensity: 0.4,\n\t\tenvironmentIntensity: 0.8,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 0.6,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0.6,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x9fc4e8,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.9,\n\t\t\temissiveIntensity: 0,\n\t\t\topacity: 0.28,\n\t\t\tdepthWrite: false,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t},\n\t// The architectural line drawing: feature edges over flat white faces. The faces are the whole\n\t// point even though you never really see them — they still write depth, so a near member hides\n\t// the ones behind it. True wireframe can't do that, and on a dense frame (thousands of members,\n\t// every edge of every one drawn at once) it collapses into a black mass.\n\t//\n\t// Unlit on purpose: any directional shading would compete with the lines for the eye. Ambient\n\t// alone lands every face on the same flat white, so the only contrast in the image is the edges.\n\t// AO is off for the same reason — a grey contact smear under a line drawing just muddies it.\n\tlineart: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// The faces are MeshPhysicalMaterial, so they take their brightness from lighting, not from\n\t\t// `color`: zero out the lights and they render black however white the override is. Hemisphere\n\t\t// and ambient carry it rather than IBL, because `scene.environment` only exists if the host\n\t\t// loaded an HDR — leaning on IBL would render this look black in any viewer without one. Both\n\t\t// are orientation-independent, so every face lands on the same flat white and the edges stay\n\t\t// the only contrast in the image. Ambient is weighted over hemisphere because hemisphere's\n\t\t// ground colour is a warm brown by default: leaning on it tints every downward face grey and\n\t\t// the drawing stops reading as one flat white.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 3.2,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\trequiresEdges: true,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xf7f8fa,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// Triangle edges only — the tessellation itself, diagonals and all. That makes it a mesh-\n\t// inspection tool, not a drawing: for a readable line drawing use `lineart`, which draws\n\t// feature edges and lets near faces occlude far ones. Nothing here is a lit surface:\n\t// the sun is off and fill carries the image, because a directional light on a line renders it\n\t// black wherever the line runs away from the key. AO is off for the same reason — a full-screen\n\t// contact pass has no contacts to find between wires, and it only costs frames.\n\t// After dark: the only real light comes from what the model emits, so a lamp lights its desk.\n\t// A little sky stays so the unlit half of the model keeps its shape instead of going black.\n\tnight: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.32,\n\t\tenvironmentIntensity: 0.15,\n\t\themisphereIntensity: 0.26,\n\t\tambientIntensity: 0.05,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 0.14,\n\t\tbackground: 0x0a0e12,\n\t\t// Above what the environment reaches on polished metal, so only emitters glow.\n\t\tbloom: { strength: 0.2, radius: 0.3, threshold: 1.8 },\n\t\temissiveLights: true,\n\t\t// Full-screen passes are the cost here: AO alone takes a quarter of the frame on an integrated\n\t\t// GPU, and without it there is no composer. A bounce light costs next to nothing and is all\n\t\t// that lights the room outside the shade, so one stays with its key.\n\t\tlowPower: {\n\t\t\tambientOcclusion: false,\n\t\t\tbloom: undefined,\n\t\t\tmaxEmissiveLights: 1,\n\t\t\tmaxPixelRatio: 1.5\n\t\t}\n\t},\n\twireframe: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// Lit for the same reason as lineart (see there): PBR lines are black unlit, and hemisphere\n\t\t// plus ambient work with no HDR loaded. Dimmer than lineart because these lines are dark\n\t\t// on white, not white on dark.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.4,\n\t\tambientIntensity: 2.0,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x2b3138,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0,\n\t\t\twireframe: true,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t}\n};\n\n/** A look's preset, with its `lowPower` fields applied on a low-power device. */\nexport function lookPreset(look: Look, lowPower = false): LookPreset {\n\tconst preset = LOOK_PRESETS[look];\n\treturn lowPower ? { ...preset, ...preset.lowPower } : preset;\n}\n\n/** `userData` key marking a material whose emission came from the wire, not from a selection tint. */\nexport const OWN_EMISSIVE = 'selvaEmissive';\n\n/** `userData` key holding the light a material casts, from real lumens rather than its look. */\nexport const EMISSIVE_LIGHT = 'selvaEmissiveLight';\n\n/**\n * `userData` key holding a material's own `envMapIntensity`, set by the parser when the wire\n * material carries one. `setLook` keeps it instead of the look's value, except in looks with a\n * `materialOverride`: those restyle every mesh alike, so the look's value wins there.\n */\nexport const OWN_ENV_MAP_INTENSITY = 'selvaEnvMapIntensity';\n\n/**\n * A look dims `envMapIntensity` to turn down fill, but three scales a material's diffuse and\n * specular IBL together. A metal has no diffuse, so the same dimming would only take away its\n * reflection, which is all a metal shows. Blending toward 1 by metalness keeps a dielectric's fill\n * on the look and a metal's reflection the same in every look.\n */\nexport function lookEnvMapIntensity(lookValue: number, metalness: number): number {\n\treturn lookValue + (1 - lookValue) * Math.min(1, Math.max(0, metalness));\n}\n\n/** Baked at parse time (not toggleable at runtime). */\nexport function materialAppearanceForLook(look: Look): MaterialAppearanceOptions {\n\tconst preset = LOOK_PRESETS[look];\n\treturn {\n\t\tenvMapIntensity: preset.envMapIntensity,\n\t\tcullBackfaces: preset.cullBackfaces\n\t};\n}\n","import * as THREE from 'three';\n\nimport { getLogger } from './logger.js';\n\n// parse/ needs these and must never import upward from render/, so they live in shared/ instead\n// of render's three-helpers.\n\nexport function parseColor(colorString: string): THREE.Color {\n\tif (!colorString || typeof colorString !== 'string') {\n\t\tgetLogger().warn(`Invalid color input: ${colorString}, using white`);\n\t\treturn new THREE.Color(0xffffff);\n\t}\n\n\tconst trimmed = colorString.trim();\n\n\tif (/^#?[0-9A-Fa-f]{6}$/.test(trimmed)) {\n\t\ttry {\n\t\t\tconst hex = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;\n\t\t\treturn new THREE.Color(hex);\n\t\t} catch {\n\t\t\tgetLogger().warn(`Invalid hex color: ${colorString}, using white`);\n\t\t\treturn new THREE.Color(0xffffff);\n\t\t}\n\t}\n\n\tif (trimmed.includes(',')) {\n\t\tconst rgb = trimmed.split(',').map((c) => parseInt(c.trim(), 10));\n\t\tif (rgb.length === 3 && rgb.every((n) => !isNaN(n) && n >= 0 && n <= 255)) {\n\t\t\treturn new THREE.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255);\n\t\t}\n\t}\n\n\t// `new THREE.Color(name)` never throws on unknown names (it logs its own warning and leaves the\n\t// color white), so validate against three's CSS name table instead.\n\tconst named = trimmed.toLowerCase();\n\tif (named in THREE.Color.NAMES) {\n\t\treturn new THREE.Color(THREE.Color.NAMES[named as keyof typeof THREE.Color.NAMES]);\n\t}\n\n\tgetLogger().warn(`Invalid color string: ${colorString}, using white`);\n\treturn new THREE.Color(0xffffff);\n}\n\n/** Defaults to `z` (Rhino's up axis); pass an explicit axis when the scene uses a different `sceneUp`. */\nexport function applyOffset(\n\tmeshes: THREE.Object3D[],\n\toffset: number,\n\taxis: 'x' | 'y' | 'z' = 'z'\n): void {\n\tmeshes.forEach((mesh) => {\n\t\tmesh.position[axis] -= offset;\n\t});\n}\n\nexport function computeCombinedBoundingBox(meshes: THREE.Object3D[]): THREE.Box3 {\n\tconst combinedBoundingBox = new THREE.Box3();\n\tif (meshes.length === 0) return combinedBoundingBox;\n\tmeshes.forEach((mesh) => {\n\t\tmesh.updateMatrixWorld(true);\n\t\tconst bbox = new THREE.Box3().setFromObject(mesh);\n\t\tcombinedBoundingBox.union(bbox);\n\t});\n\treturn combinedBoundingBox;\n}\n","// ============================================================================\n// GPU capabilities — published by the renderer, read by whoever needs them\n// ============================================================================\n\n// Lets a renderer publish GPU capabilities (max anisotropy) without `render/` and `parse/` importing\n// each other: the renderer publishes on init, interested parties subscribe.\n\ntype AnisotropyObserver = (value: number) => void;\n\nconst observers = new Set<AnisotropyObserver>();\n\n// Module load order between layers isn't guaranteed, so a subscriber arriving after init still\n// needs the current value rather than waiting for a second renderer to publish.\nlet maxAnisotropy = 1;\n\nexport function publishMaxAnisotropy(value: number): void {\n\tconst next = Math.max(1, value);\n\tif (next === maxAnisotropy) return;\n\tmaxAnisotropy = next;\n\tfor (const observe of observers) observe(next);\n}\n\n/** Fires immediately with the current value (1 until a renderer has reported), then on every change. */\nexport function observeMaxAnisotropy(observe: AnisotropyObserver): () => void {\n\tobservers.add(observe);\n\tobserve(maxAnisotropy);\n\treturn () => observers.delete(observe);\n}\n","import type * as THREE from 'three';\n\n/**\n * `userData` key marking a material the parser built as glass. AO, the edge pass and shadow maps\n * skip it: each treats a surface as opaque, so glass would darken, outline and shade what it shows.\n */\nexport const GLASS = 'selvaGlass';\n\nexport function isGlass(material: THREE.Material | THREE.Material[] | undefined): boolean {\n\tif (!material) return false;\n\treturn Array.isArray(material)\n\t\t? material.some((m) => m.userData[GLASS] === true)\n\t\t: material.userData[GLASS] === true;\n}\n\n/**\n * Hides every visible mesh drawn in glass under `root` and returns the call that shows them again.\n * For passes that redraw the scene with an override material, which would otherwise draw glass as\n * a solid surface.\n */\nexport function hideGlass(root: THREE.Object3D): () => void {\n\tconst hidden: THREE.Object3D[] = [];\n\troot.traverse((object) => {\n\t\tif (object.visible && isGlass((object as THREE.Mesh).material)) {\n\t\t\tobject.visible = false;\n\t\t\thidden.push(object);\n\t\t}\n\t});\n\treturn () => {\n\t\tfor (const object of hidden) object.visible = true;\n\t};\n}\n","import * as THREE from 'three';\n\nimport { cachedSourceOf, canDisposeMaterial } from './gpu-ownership.js';\n\n// ============================================================================\n// The disposal walkers — every teardown path in the package goes through these\n// ============================================================================\n\n/** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */\nexport function disposeMaterial(material: THREE.Material): void {\n\tif (!canDisposeMaterial(material)) return;\n\n\tfor (const [key, value] of Object.entries(material)) {\n\t\t// An envMap is the scene's, shared by every metal: freeing it here would blank them all.\n\t\tif (key === 'envMap') continue;\n\t\tif (value instanceof THREE.Texture) {\n\t\t\tvalue.dispose();\n\t\t}\n\t}\n\tmaterial.dispose();\n}\n\nfunction disposeMaterials(material: THREE.Material | THREE.Material[] | undefined): void {\n\tif (!material) return;\n\tif (Array.isArray(material)) material.forEach(disposeMaterial);\n\telse disposeMaterial(material);\n}\n\nexport interface DisposeOptions {\n\t/** Free materials and their textures too. Default true; the solve memo passes false. */\n\tmaterials?: boolean;\n\t/** Called for each geometry before disposal, so a subsystem can free derived resources keyed on it (e.g. edge caches). */\n\tonGeometry?: (geometry: THREE.BufferGeometry) => void;\n\t/**\n\t * Cached geometries whose GPU copy survives. When given, every other cached geometry this walk\n\t * reaches loses its GPU copy.\n\t */\n\tkeepUploaded?: ReadonlySet<THREE.BufferGeometry>;\n}\n\n/**\n * Frees the GPU resources of an object subtree, respecting ownership. **The only traversal that\n * should dispose scene content** — a second disposing `traverse` risks missing an ownership guard.\n */\nexport function disposeObjectTree(root: THREE.Object3D, options: DisposeOptions = {}): void {\n\tconst { materials = true, keepUploaded } = options;\n\n\troot.traverse((object) => {\n\t\tconst renderable = object as Partial<THREE.Mesh> & THREE.Object3D;\n\t\tif (!renderable.geometry && !renderable.material) return;\n\n\t\tif (renderable.geometry) disposeGeometry(renderable.geometry, keepUploaded);\n\n\t\tif (materials) disposeMaterials(renderable.material);\n\t});\n}\n\nfunction disposeGeometry(\n\tgeometry: THREE.BufferGeometry,\n\tkeepUploaded: ReadonlySet<THREE.BufferGeometry> | undefined\n): void {\n\tconst source = cachedSourceOf(geometry);\n\tif (source !== geometry) {\n\t\t// three's dispose frees the GPU buffers of whatever attributes are attached, so a borrower\n\t\t// lets go of the cache's attributes first.\n\t\tif (source) {\n\t\t\tgeometry.setIndex(null);\n\t\t\tfor (const name of Object.keys(geometry.attributes)) geometry.deleteAttribute(name);\n\t\t}\n\t\tgeometry.dispose();\n\t}\n\tif (source && keepUploaded && !keepUploaded.has(source)) source.dispose();\n}\n"],"mappings":"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"}
@@ -190,8 +190,8 @@ const LOOK_PRESETS = {
190
190
  sunlightIntensity: .14,
191
191
  background: 658962,
192
192
  bloom: {
193
- strength: .26,
194
- radius: .5,
193
+ strength: .2,
194
+ radius: .3,
195
195
  threshold: 1.8
196
196
  },
197
197
  emissiveLights: true,
@@ -311,6 +311,33 @@ function observeMaxAnisotropy(observe) {
311
311
  observe(maxAnisotropy);
312
312
  return () => observers.delete(observe);
313
313
  }
314
+ // src/shared/glass.ts
315
+ /**
316
+ * `userData` key marking a material the parser built as glass. AO, the edge pass and shadow maps
317
+ * skip it: each treats a surface as opaque, so glass would darken, outline and shade what it shows.
318
+ */
319
+ const GLASS = "selvaGlass";
320
+ function isGlass(material) {
321
+ if (!material) return false;
322
+ return Array.isArray(material) ? material.some((m) => m.userData[GLASS] === true) : material.userData[GLASS] === true;
323
+ }
324
+ /**
325
+ * Hides every visible mesh drawn in glass under `root` and returns the call that shows them again.
326
+ * For passes that redraw the scene with an override material, which would otherwise draw glass as
327
+ * a solid surface.
328
+ */
329
+ function hideGlass(root) {
330
+ const hidden = [];
331
+ root.traverse((object) => {
332
+ if (object.visible && isGlass(object.material)) {
333
+ object.visible = false;
334
+ hidden.push(object);
335
+ }
336
+ });
337
+ return () => {
338
+ for (const object of hidden) object.visible = true;
339
+ };
340
+ }
314
341
  // src/shared/gpu-dispose.ts
315
342
  /** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */
316
343
  function disposeMaterial(material) {
@@ -350,6 +377,6 @@ function disposeGeometry(geometry, keepUploaded) {
350
377
  }
351
378
  if (source && keepUploaded && !keepUploaded.has(source)) source.dispose();
352
379
  }
353
- export { setLogger as _, computeCombinedBoundingBox as a, EMISSIVE_LIGHT as c, OWN_ENV_MAP_INTENSITY as d, lookEnvMapIntensity as f, getLogger as g, enableDebugLogging as h, applyOffset as i, LOOK_PRESETS as l, materialAppearanceForLook as m, observeMaxAnisotropy as n, parseColor as o, lookPreset as p, publishMaxAnisotropy as r, DEFAULT_LOOK as s, disposeObjectTree as t, OWN_EMISSIVE as u, ErrorCodes as v, VisualizationError as y };
380
+ export { VisualizationError as S, materialAppearanceForLook as _, observeMaxAnisotropy as a, setLogger as b, computeCombinedBoundingBox as c, EMISSIVE_LIGHT as d, LOOK_PRESETS as f, lookPreset as g, lookEnvMapIntensity as h, isGlass as i, parseColor as l, OWN_ENV_MAP_INTENSITY as m, GLASS as n, publishMaxAnisotropy as o, OWN_EMISSIVE as p, hideGlass as r, applyOffset as s, disposeObjectTree as t, DEFAULT_LOOK as u, enableDebugLogging as v, ErrorCodes as x, getLogger as y };
354
381
 
355
- //# sourceMappingURL=gpu-dispose-Cl3RXm-v.js.map
382
+ //# sourceMappingURL=gpu-dispose-Di_nxeXF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-dispose-Di_nxeXF.js","names":[],"sources":["../src/shared/errors.ts","../src/shared/logger.ts","../src/shared/looks.ts","../src/shared/geometry.ts","../src/shared/gpu-capabilities.ts","../src/shared/glass.ts","../src/shared/gpu-dispose.ts"],"sourcesContent":["/**\n * Errors for the visualization package. Replaces `@selvajs/compute`'s `RhinoComputeError`, which\n * mis-named failures on paths (e.g. the plugin WebSocket) that never touch Rhino.Compute. `code`\n * values match compute's so existing catch-sites keep working.\n */\n\nexport const ErrorCodes = {\n\t/** Structural check failed: bad magic bytes, out-of-window index, malformed metadata. */\n\tVALIDATION_ERROR: 'VALIDATION_ERROR',\n\tINVALID_STATE: 'INVALID_STATE',\n\t/** No `DecompressionStream`, no WebGL context, etc. */\n\tENVIRONMENT_ERROR: 'ENVIRONMENT_ERROR',\n\tINVALID_CONFIG: 'INVALID_CONFIG',\n\t/** Base64 input could not be decoded. */\n\tENCODING_ERROR: 'ENCODING_ERROR',\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR'\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nexport class VisualizationError extends Error {\n\tpublic readonly code: ErrorCode;\n\tpublic readonly context?: Record<string, unknown>;\n\tpublic readonly originalError?: Error;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: ErrorCode = ErrorCodes.UNKNOWN_ERROR,\n\t\toptions?: { context?: Record<string, unknown>; originalError?: Error }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'VisualizationError';\n\t\tthis.code = code;\n\t\tthis.context = options?.context;\n\t\tthis.originalError = options?.originalError;\n\t\tif (options?.originalError) {\n\t\t\t(this as { cause?: unknown }).cause = options.originalError;\n\t\t}\n\t}\n}\n","/**\n * Logging facility for the visualization package. Deliberately local rather than imported from\n * `@selvajs/compute` (logging isn't a compute concern). Mirrors compute's logger shape so a host\n * wanting one sink for both can call `setLogger(computeLogger.getLogger())`.\n */\n\nimport { VisualizationError, ErrorCodes } from './errors.js';\n\nexport interface Logger {\n\tdebug(message: string, ...args: unknown[]): void;\n\tinfo(message: string, ...args: unknown[]): void;\n\twarn(message: string, ...args: unknown[]): void;\n\terror(message: string, ...args: unknown[]): void;\n}\n\nclass NoOpLogger implements Logger {\n\tdebug(): void {}\n\tinfo(): void {}\n\twarn(): void {}\n\terror(): void {}\n}\n\nclass ConsoleLogger implements Logger {\n\tdebug(message: string, ...args: unknown[]): void {\n\t\t// eslint-disable-next-line no-console -- this class exists to be the console sink\n\t\tconsole.debug(message, ...args);\n\t}\n\n\tinfo(message: string, ...args: unknown[]): void {\n\t\tconsole.info(message, ...args);\n\t}\n\n\twarn(message: string, ...args: unknown[]): void {\n\t\tconsole.warn(message, ...args);\n\t}\n\n\terror(message: string, ...args: unknown[]): void {\n\t\tconsole.error(message, ...args);\n\t}\n}\n\n// Defaults to no-op: a library that writes to the console uninvited is a nuisance in a host with\n// its own logging. Opt in with setLogger or enableDebugLogging.\nlet internalLogger: Logger = new NoOpLogger();\n\n// Call per-use rather than caching — the sink is settable at any time.\nexport function getLogger(): Logger {\n\treturn internalLogger;\n}\n\n// Validates logger shape up front: failing here beats a confusing \"getLogger().debug is not a\n// function\" at some later, unrelated call site.\nexport function setLogger(logger: Logger | Console | null): void {\n\tif (logger === null) {\n\t\tinternalLogger = new NoOpLogger();\n\t\treturn;\n\t}\n\n\tconst missing = (['debug', 'info', 'warn', 'error'] as const).filter(\n\t\t(method) => typeof (logger as unknown as Record<string, unknown>)[method] !== 'function'\n\t);\n\tif (missing.length > 0) {\n\t\tthrow new VisualizationError(\n\t\t\t`Logger is missing required method(s): ${missing.join(', ')}. A logger must implement debug, info, warn and error.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { missingMethods: missing } }\n\t\t);\n\t}\n\n\tinternalLogger = logger as Logger;\n}\n\nexport function enableDebugLogging(): void {\n\tsetLogger(new ConsoleLogger());\n}\n","import * as THREE from 'three';\n\nimport type { Look, LookPreset, MaterialAppearanceOptions } from './types.js';\n\n/** The look applied when the caller passes no `look` option. */\nexport const DEFAULT_LOOK: Look = 'technical';\n\n/**\n * Single source of truth for both `applyDefaults` (construction) and `setLook` (runtime), so the two\n * can't drift.\n *\n * Every shaded look pairs a key light with contact shading (GTAO), because image-based lighting on\n * its own lights all faces of a box nearly equally — the result is a white silhouette with no\n * readable form. GTAO is a full-screen pass and does cost frames; `xray` skips it since there are no\n * opaque contacts left to shade.\n */\nexport const LOOK_PRESETS: Record<Look, LookPreset> = {\n\t// Fills shadows in (vs. showcase, which lets them fall off), but still key-lit: ACES plus a\n\t// moderate sun, with fill high enough that shadowed faces keep detail.\n\tstudio: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 0.85,\n\t\themisphereIntensity: 0.5,\n\t\tambientIntensity: 0.25,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 1.6\n\t},\n\t// The sun carries this look and the ambient/IBL only lift shadows off black. Fill was the thing\n\t// making it read as flat white paper: every source except the sun is orientation-independent, so\n\t// the more of them you stack the less the three faces of a corner differ.\n\ttechnical: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.55,\n\t\tenvironmentIntensity: 0.6,\n\t\themisphereIntensity: 0.25,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.2\n\t},\n\t// Fill pulled furthest DOWN and the sun pushed hardest, so light-to-shadow falloff is steepest —\n\t// a dramatic hero shot, unlike `studio` which fills shadows back in.\n\tshowcase: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1.15,\n\t\tenvMapIntensity: 1.4,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.2,\n\t\tambientIntensity: 0.1,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.6\n\t},\n\t// Every mesh forced to one near-white clay, so form and shadow carry the image instead of the\n\t// model's own colours. AO on: with all albedo differences gone, contact shading is the only thing\n\t// left separating touching parts — without it the model reads as one white blob.\n\tarctic: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.05,\n\t\tenvMapIntensity: 0.85,\n\t\tenvironmentIntensity: 1.1,\n\t\themisphereIntensity: 0.3,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xeef1f5,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.75,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// See-through blue-white with depth writes off, so interior geometry shows through the shell.\n\t// Exposure pushed up because stacked transparent layers darken fast; the sun is dialled back\n\t// because a hard highlight on a transparent shell hides what is behind it.\n\txray: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.3,\n\t\tenvMapIntensity: 0.4,\n\t\tenvironmentIntensity: 0.8,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 0.6,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0.6,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x9fc4e8,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.9,\n\t\t\temissiveIntensity: 0,\n\t\t\topacity: 0.28,\n\t\t\tdepthWrite: false,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t},\n\t// The architectural line drawing: feature edges over flat white faces. The faces are the whole\n\t// point even though you never really see them — they still write depth, so a near member hides\n\t// the ones behind it. True wireframe can't do that, and on a dense frame (thousands of members,\n\t// every edge of every one drawn at once) it collapses into a black mass.\n\t//\n\t// Unlit on purpose: any directional shading would compete with the lines for the eye. Ambient\n\t// alone lands every face on the same flat white, so the only contrast in the image is the edges.\n\t// AO is off for the same reason — a grey contact smear under a line drawing just muddies it.\n\tlineart: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// The faces are MeshPhysicalMaterial, so they take their brightness from lighting, not from\n\t\t// `color`: zero out the lights and they render black however white the override is. Hemisphere\n\t\t// and ambient carry it rather than IBL, because `scene.environment` only exists if the host\n\t\t// loaded an HDR — leaning on IBL would render this look black in any viewer without one. Both\n\t\t// are orientation-independent, so every face lands on the same flat white and the edges stay\n\t\t// the only contrast in the image. Ambient is weighted over hemisphere because hemisphere's\n\t\t// ground colour is a warm brown by default: leaning on it tints every downward face grey and\n\t\t// the drawing stops reading as one flat white.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 3.2,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\trequiresEdges: true,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xf7f8fa,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// Triangle edges only — the tessellation itself, diagonals and all. That makes it a mesh-\n\t// inspection tool, not a drawing: for a readable line drawing use `lineart`, which draws\n\t// feature edges and lets near faces occlude far ones. Nothing here is a lit surface:\n\t// the sun is off and fill carries the image, because a directional light on a line renders it\n\t// black wherever the line runs away from the key. AO is off for the same reason — a full-screen\n\t// contact pass has no contacts to find between wires, and it only costs frames.\n\t// After dark: the only real light comes from what the model emits, so a lamp lights its desk.\n\t// A little sky stays so the unlit half of the model keeps its shape instead of going black.\n\tnight: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.32,\n\t\tenvironmentIntensity: 0.15,\n\t\themisphereIntensity: 0.26,\n\t\tambientIntensity: 0.05,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 0.14,\n\t\tbackground: 0x0a0e12,\n\t\t// Above what the environment reaches on polished metal, so only emitters glow.\n\t\tbloom: { strength: 0.2, radius: 0.3, threshold: 1.8 },\n\t\temissiveLights: true,\n\t\t// Full-screen passes are the cost here: AO alone takes a quarter of the frame on an integrated\n\t\t// GPU, and without it there is no composer. A bounce light costs next to nothing and is all\n\t\t// that lights the room outside the shade, so one stays with its key.\n\t\tlowPower: {\n\t\t\tambientOcclusion: false,\n\t\t\tbloom: undefined,\n\t\t\tmaxEmissiveLights: 1,\n\t\t\tmaxPixelRatio: 1.5\n\t\t}\n\t},\n\twireframe: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// Lit for the same reason as lineart (see there): PBR lines are black unlit, and hemisphere\n\t\t// plus ambient work with no HDR loaded. Dimmer than lineart because these lines are dark\n\t\t// on white, not white on dark.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.4,\n\t\tambientIntensity: 2.0,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x2b3138,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0,\n\t\t\twireframe: true,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t}\n};\n\n/** A look's preset, with its `lowPower` fields applied on a low-power device. */\nexport function lookPreset(look: Look, lowPower = false): LookPreset {\n\tconst preset = LOOK_PRESETS[look];\n\treturn lowPower ? { ...preset, ...preset.lowPower } : preset;\n}\n\n/** `userData` key marking a material whose emission came from the wire, not from a selection tint. */\nexport const OWN_EMISSIVE = 'selvaEmissive';\n\n/** `userData` key holding the light a material casts, from real lumens rather than its look. */\nexport const EMISSIVE_LIGHT = 'selvaEmissiveLight';\n\n/**\n * `userData` key holding a material's own `envMapIntensity`, set by the parser when the wire\n * material carries one. `setLook` keeps it instead of the look's value, except in looks with a\n * `materialOverride`: those restyle every mesh alike, so the look's value wins there.\n */\nexport const OWN_ENV_MAP_INTENSITY = 'selvaEnvMapIntensity';\n\n/**\n * A look dims `envMapIntensity` to turn down fill, but three scales a material's diffuse and\n * specular IBL together. A metal has no diffuse, so the same dimming would only take away its\n * reflection, which is all a metal shows. Blending toward 1 by metalness keeps a dielectric's fill\n * on the look and a metal's reflection the same in every look.\n */\nexport function lookEnvMapIntensity(lookValue: number, metalness: number): number {\n\treturn lookValue + (1 - lookValue) * Math.min(1, Math.max(0, metalness));\n}\n\n/** Baked at parse time (not toggleable at runtime). */\nexport function materialAppearanceForLook(look: Look): MaterialAppearanceOptions {\n\tconst preset = LOOK_PRESETS[look];\n\treturn {\n\t\tenvMapIntensity: preset.envMapIntensity,\n\t\tcullBackfaces: preset.cullBackfaces\n\t};\n}\n","import * as THREE from 'three';\n\nimport { getLogger } from './logger.js';\n\n// parse/ needs these and must never import upward from render/, so they live in shared/ instead\n// of render's three-helpers.\n\nexport function parseColor(colorString: string): THREE.Color {\n\tif (!colorString || typeof colorString !== 'string') {\n\t\tgetLogger().warn(`Invalid color input: ${colorString}, using white`);\n\t\treturn new THREE.Color(0xffffff);\n\t}\n\n\tconst trimmed = colorString.trim();\n\n\tif (/^#?[0-9A-Fa-f]{6}$/.test(trimmed)) {\n\t\ttry {\n\t\t\tconst hex = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;\n\t\t\treturn new THREE.Color(hex);\n\t\t} catch {\n\t\t\tgetLogger().warn(`Invalid hex color: ${colorString}, using white`);\n\t\t\treturn new THREE.Color(0xffffff);\n\t\t}\n\t}\n\n\tif (trimmed.includes(',')) {\n\t\tconst rgb = trimmed.split(',').map((c) => parseInt(c.trim(), 10));\n\t\tif (rgb.length === 3 && rgb.every((n) => !isNaN(n) && n >= 0 && n <= 255)) {\n\t\t\treturn new THREE.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255);\n\t\t}\n\t}\n\n\t// `new THREE.Color(name)` never throws on unknown names (it logs its own warning and leaves the\n\t// color white), so validate against three's CSS name table instead.\n\tconst named = trimmed.toLowerCase();\n\tif (named in THREE.Color.NAMES) {\n\t\treturn new THREE.Color(THREE.Color.NAMES[named as keyof typeof THREE.Color.NAMES]);\n\t}\n\n\tgetLogger().warn(`Invalid color string: ${colorString}, using white`);\n\treturn new THREE.Color(0xffffff);\n}\n\n/** Defaults to `z` (Rhino's up axis); pass an explicit axis when the scene uses a different `sceneUp`. */\nexport function applyOffset(\n\tmeshes: THREE.Object3D[],\n\toffset: number,\n\taxis: 'x' | 'y' | 'z' = 'z'\n): void {\n\tmeshes.forEach((mesh) => {\n\t\tmesh.position[axis] -= offset;\n\t});\n}\n\nexport function computeCombinedBoundingBox(meshes: THREE.Object3D[]): THREE.Box3 {\n\tconst combinedBoundingBox = new THREE.Box3();\n\tif (meshes.length === 0) return combinedBoundingBox;\n\tmeshes.forEach((mesh) => {\n\t\tmesh.updateMatrixWorld(true);\n\t\tconst bbox = new THREE.Box3().setFromObject(mesh);\n\t\tcombinedBoundingBox.union(bbox);\n\t});\n\treturn combinedBoundingBox;\n}\n","// ============================================================================\n// GPU capabilities — published by the renderer, read by whoever needs them\n// ============================================================================\n\n// Lets a renderer publish GPU capabilities (max anisotropy) without `render/` and `parse/` importing\n// each other: the renderer publishes on init, interested parties subscribe.\n\ntype AnisotropyObserver = (value: number) => void;\n\nconst observers = new Set<AnisotropyObserver>();\n\n// Module load order between layers isn't guaranteed, so a subscriber arriving after init still\n// needs the current value rather than waiting for a second renderer to publish.\nlet maxAnisotropy = 1;\n\nexport function publishMaxAnisotropy(value: number): void {\n\tconst next = Math.max(1, value);\n\tif (next === maxAnisotropy) return;\n\tmaxAnisotropy = next;\n\tfor (const observe of observers) observe(next);\n}\n\n/** Fires immediately with the current value (1 until a renderer has reported), then on every change. */\nexport function observeMaxAnisotropy(observe: AnisotropyObserver): () => void {\n\tobservers.add(observe);\n\tobserve(maxAnisotropy);\n\treturn () => observers.delete(observe);\n}\n","import type * as THREE from 'three';\n\n/**\n * `userData` key marking a material the parser built as glass. AO, the edge pass and shadow maps\n * skip it: each treats a surface as opaque, so glass would darken, outline and shade what it shows.\n */\nexport const GLASS = 'selvaGlass';\n\nexport function isGlass(material: THREE.Material | THREE.Material[] | undefined): boolean {\n\tif (!material) return false;\n\treturn Array.isArray(material)\n\t\t? material.some((m) => m.userData[GLASS] === true)\n\t\t: material.userData[GLASS] === true;\n}\n\n/**\n * Hides every visible mesh drawn in glass under `root` and returns the call that shows them again.\n * For passes that redraw the scene with an override material, which would otherwise draw glass as\n * a solid surface.\n */\nexport function hideGlass(root: THREE.Object3D): () => void {\n\tconst hidden: THREE.Object3D[] = [];\n\troot.traverse((object) => {\n\t\tif (object.visible && isGlass((object as THREE.Mesh).material)) {\n\t\t\tobject.visible = false;\n\t\t\thidden.push(object);\n\t\t}\n\t});\n\treturn () => {\n\t\tfor (const object of hidden) object.visible = true;\n\t};\n}\n","import * as THREE from 'three';\n\nimport { cachedSourceOf, canDisposeMaterial } from './gpu-ownership.js';\n\n// ============================================================================\n// The disposal walkers — every teardown path in the package goes through these\n// ============================================================================\n\n/** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */\nexport function disposeMaterial(material: THREE.Material): void {\n\tif (!canDisposeMaterial(material)) return;\n\n\tfor (const [key, value] of Object.entries(material)) {\n\t\t// An envMap is the scene's, shared by every metal: freeing it here would blank them all.\n\t\tif (key === 'envMap') continue;\n\t\tif (value instanceof THREE.Texture) {\n\t\t\tvalue.dispose();\n\t\t}\n\t}\n\tmaterial.dispose();\n}\n\nfunction disposeMaterials(material: THREE.Material | THREE.Material[] | undefined): void {\n\tif (!material) return;\n\tif (Array.isArray(material)) material.forEach(disposeMaterial);\n\telse disposeMaterial(material);\n}\n\nexport interface DisposeOptions {\n\t/** Free materials and their textures too. Default true; the solve memo passes false. */\n\tmaterials?: boolean;\n\t/** Called for each geometry before disposal, so a subsystem can free derived resources keyed on it (e.g. edge caches). */\n\tonGeometry?: (geometry: THREE.BufferGeometry) => void;\n\t/**\n\t * Cached geometries whose GPU copy survives. When given, every other cached geometry this walk\n\t * reaches loses its GPU copy.\n\t */\n\tkeepUploaded?: ReadonlySet<THREE.BufferGeometry>;\n}\n\n/**\n * Frees the GPU resources of an object subtree, respecting ownership. **The only traversal that\n * should dispose scene content** — a second disposing `traverse` risks missing an ownership guard.\n */\nexport function disposeObjectTree(root: THREE.Object3D, options: DisposeOptions = {}): void {\n\tconst { materials = true, keepUploaded } = options;\n\n\troot.traverse((object) => {\n\t\tconst renderable = object as Partial<THREE.Mesh> & THREE.Object3D;\n\t\tif (!renderable.geometry && !renderable.material) return;\n\n\t\tif (renderable.geometry) disposeGeometry(renderable.geometry, keepUploaded);\n\n\t\tif (materials) disposeMaterials(renderable.material);\n\t});\n}\n\nfunction disposeGeometry(\n\tgeometry: THREE.BufferGeometry,\n\tkeepUploaded: ReadonlySet<THREE.BufferGeometry> | undefined\n): void {\n\tconst source = cachedSourceOf(geometry);\n\tif (source !== geometry) {\n\t\t// three's dispose frees the GPU buffers of whatever attributes are attached, so a borrower\n\t\t// lets go of the cache's attributes first.\n\t\tif (source) {\n\t\t\tgeometry.setIndex(null);\n\t\t\tfor (const name of Object.keys(geometry.attributes)) geometry.deleteAttribute(name);\n\t\t}\n\t\tgeometry.dispose();\n\t}\n\tif (source && keepUploaded && !keepUploaded.has(source)) source.dispose();\n}\n"],"mappings":"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"}
package/dist/parse.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_gpu_ownership = require("./gpu-ownership-ADRZBxxs.cjs");
3
- const require_gpu_dispose = require("./gpu-dispose-fRI12OCL.cjs");
3
+ const require_gpu_dispose = require("./gpu-dispose-D66GqIWS.cjs");
4
4
  let three = require("three");
5
5
  three = require_gpu_ownership.__toESM(three, 1);
6
6
  let three_addons_lines_Line2_js = require("three/addons/lines/Line2.js");
@@ -1681,6 +1681,32 @@ function applyTexture(material, url, slot, mapSize) {
1681
1681
  });
1682
1682
  }
1683
1683
  // src/parse/webdisplay/batch/surface-finish.ts
1684
+ const f$1 = (n) => n.toFixed(4);
1685
+ const PITH = 120;
1686
+ const PITH_SPREAD = 120;
1687
+ const PLANK = 180;
1688
+ const LEAN = .0105;
1689
+ const wood = ({ ring, latewood, band, pores, streaks }) => `
1690
+ float plank = floor( uv.y / ${f$1(PLANK)} );
1691
+ float s = uv.y - ( plank + 0.5 ) * ${f$1(PLANK)};
1692
+ float pith = ${f$1(PITH)} + selvaHash( vec2( plank, 2.0 ) ) * ${f$1(PITH_SPREAD)};
1693
+ // Rings wander a few mm and vary in spacing, as growth does from year to year.
1694
+ float wander = ( selvaFbm( uv * vec2( 0.003, 0.008 ) ) - 0.5 ) * ${f$1(ring * 2)};
1695
+ float r = sqrt( pith * pith + s * s ) + uv.x * ${f$1(LEAN)} + wander;
1696
+ r += selvaFbm( vec2( r * 0.02, plank ) ) * ${f$1(ring * 3)} + selvaHash( vec2( plank, 1.0 ) ) * ${f$1(ring * 20)};
1697
+ float year = fract( r / ${f$1(ring)} );
1698
+ float late = smoothstep( ${f$1(1 - band)}, ${f$1(1 - band * .4)}, year ) * ( 1.0 - smoothstep( 0.95, 1.0, year ) );
1699
+ // Open-pored wood shows its rings as a broken band of pores, not a clean line.
1700
+ late *= 1.0 - ${f$1(pores * .6)} * ( 1.0 - smoothstep( 0.3, 0.7, selvaNoise( uv * vec2( 0.25, 2.0 ) ) ) );
1701
+ late *= selvaDetail( ${f$1(ring)} );
1702
+ float fibre = ( selvaNoise( uv * vec2( 0.04, 1.6 ) ) - 0.5 ) * selvaDetail( 1.0 );
1703
+ float pore = smoothstep( 0.62, 0.8, selvaNoise( uv * vec2( 0.3, 2.5 ) ) ) * ( 1.0 - 0.6 * late );
1704
+ pore *= ${f$1(pores)} * selvaDetail( 0.8 );
1705
+ float streak = ( selvaFbm( uv * vec2( 0.0015, 0.012 ) ) - 0.5 ) * ${f$1(streaks)};
1706
+ float shade = 1.0 + ${f$1(latewood * band)} - late * ${f$1(latewood)} + fibre * 0.1 - pore * 0.45 + streak * 0.6;
1707
+ tone = vec3( shade );
1708
+ gloss = 1.0 + late * 0.08 + pore * 0.4 - fibre * 0.2;
1709
+ height = ( fibre * 0.00008 - pore * 0.00015 ) * selvaDetail( 0.6 );`;
1684
1710
  /**
1685
1711
  * Procedural surfaces, so a preset looks like its material with no texture files to ship or upload.
1686
1712
  * UVs are millimetres, so a brick is brick-sized at any model scale. Each recipe sets `tone` (albedo
@@ -1689,15 +1715,41 @@ function applyTexture(material, url, slot, mapSize) {
1689
1715
  * samples to static, so a whole building has to settle to its flat colour.
1690
1716
  */
1691
1717
  const RECIPES$1 = {
1692
- wood: `
1693
- float warp = selvaFbm( uv * vec2( 0.003, 0.02 ) );
1694
- float rings = 0.5 + 0.5 * sin( ( uv.y + warp * 90.0 ) * 0.28 );
1695
- float fibre = selvaNoise( uv * vec2( 0.03, 1.4 ) );
1696
- float grain = ( rings * 0.65 + fibre * 0.35 ) - 0.5;
1697
- grain *= selvaDetail( 4.0 );
1698
- tone = vec3( 1.0 + grain * 0.42 );
1699
- gloss = 1.0 + grain * 0.35;
1700
- height = fibre * 0.00012 * selvaDetail( 1.0 );`,
1718
+ wood: wood({
1719
+ ring: 4,
1720
+ latewood: .3,
1721
+ band: .4,
1722
+ pores: .4,
1723
+ streaks: .35
1724
+ }),
1725
+ walnut: wood({
1726
+ ring: 5,
1727
+ latewood: .2,
1728
+ band: .4,
1729
+ pores: 1,
1730
+ streaks: .45
1731
+ }),
1732
+ oak: wood({
1733
+ ring: 3,
1734
+ latewood: .3,
1735
+ band: .35,
1736
+ pores: .9,
1737
+ streaks: .25
1738
+ }),
1739
+ maple: wood({
1740
+ ring: 3,
1741
+ latewood: .2,
1742
+ band: .1,
1743
+ pores: 0,
1744
+ streaks: .12
1745
+ }),
1746
+ wenge: wood({
1747
+ ring: 1.5,
1748
+ latewood: .7,
1749
+ band: .5,
1750
+ pores: .6,
1751
+ streaks: .4
1752
+ }),
1701
1753
  concrete: `
1702
1754
  float blotch = ( selvaFbm( uv * 0.0035 ) - 0.5 ) * selvaDetail( 40.0 );
1703
1755
  float holes = smoothstep( 0.8, 0.88, selvaNoise( uv * 0.22 ) ) * selvaDetail( 5.0 );
@@ -1722,6 +1774,127 @@ const RECIPES$1 = {
1722
1774
  tone = mix( vec3( face ), vec3( 1.5, 1.7, 1.8 ), joint );
1723
1775
  gloss = 1.0 + joint * 0.15;
1724
1776
  height = -joint * 0.005;`,
1777
+ limestone: `
1778
+ float cloud = ( selvaFbm( uv * 0.004 ) - 0.5 ) * selvaDetail( 60.0 );
1779
+ float mottle = ( selvaFbm( uv * 0.025 + vec2( 3.0, 7.0 ) ) - 0.5 ) * selvaDetail( 12.0 );
1780
+ float bed = ( selvaNoise( vec2( uv.x * 0.003, uv.y * 0.05 ) ) - 0.5 ) * selvaDetail( 10.0 );
1781
+ float fleck = smoothstep( 0.8, 0.9, selvaNoise( uv * 0.5 ) ) * selvaDetail( 2.0 );
1782
+ vec3 fossil = selvaCell( uv / 14.0 );
1783
+ float shell = ( 1.0 - smoothstep( 0.08, 0.16, fossil.x ) ) * step( 0.85, fossil.z ) * selvaDetail( 6.0 );
1784
+ tone = vec3( 1.0 + cloud * 0.14 + mottle * 0.1 + bed * 0.03 - fleck * 0.12 - shell * 0.1 );
1785
+ gloss = 1.0 + cloud * 0.1;
1786
+ height = -( fleck + shell ) * 0.00005;`,
1787
+ travertine: `
1788
+ float band = selvaFbm( vec2( uv.x * 0.002, uv.y * 0.012 ) );
1789
+ float layer = ( band - 0.5 ) * selvaDetail( 25.0 );
1790
+ float pocket = selvaNoise( uv * vec2( 0.025, 0.15 ) ) * 0.7 + selvaNoise( uv * vec2( 0.08, 0.4 ) ) * 0.3;
1791
+ float voids = smoothstep( 0.68, 0.78, pocket ) * smoothstep( 0.35, 0.6, band ) * selvaDetail( 4.0 );
1792
+ float grain = ( selvaNoise( uv * 0.6 ) - 0.5 ) * selvaDetail( 1.5 );
1793
+ tone = vec3( 1.0 + layer * 0.2 + grain * 0.04 ) * mix( vec3( 1.0 ), vec3( 0.62, 0.56, 0.48 ), voids );
1794
+ gloss = 1.0 + voids * 0.3;
1795
+ height = -voids * 0.002;`,
1796
+ granite: `
1797
+ vec3 crystal = selvaCell( uv / 4.0 );
1798
+ // Feldspar, quartz and biotite in about the shares of a grey granite. The dark crystals
1799
+ // gather in clusters, which is what still reads as granite from a few metres.
1800
+ float cluster = selvaFbm( uv * 0.04 ) - 0.5;
1801
+ float dark = 0.2 + cluster * 0.2;
1802
+ float mineral = crystal.z < dark ? 0.35 : ( crystal.z < dark + 0.3 ? 1.35 : 1.0 );
1803
+ mineral *= 1.0 + ( selvaHash( vec2( crystal.z, 3.0 ) ) - 0.5 ) * 0.15;
1804
+ mineral = mix( 1.0 - cluster * 0.25 * selvaDetail( 15.0 ), mineral, selvaDetail( 4.0 ) );
1805
+ float cloud = ( selvaFbm( uv * 0.005 ) - 0.5 ) * selvaDetail( 50.0 );
1806
+ tone = vec3( mineral + cloud * 0.06 );
1807
+ gloss = 1.0 - ( mineral - 1.0 ) * 0.15;
1808
+ height = ( selvaNoise( uv * 0.8 ) - 0.5 ) * 0.0002 * selvaDetail( 1.2 );`,
1809
+ marble: `
1810
+ vec2 w = vec2( uv.x * 0.7 + uv.y * 0.7, uv.y * 0.7 - uv.x * 0.7 ) * vec2( 0.0012, 0.004 );
1811
+ w += vec2( selvaFbm( w * 2.0 ), selvaFbm( w * 2.0 + vec2( 4.0, 2.0 ) ) ) * 0.6;
1812
+ float feather = smoothstep( 0.3, 0.7, selvaFbm( uv * 0.006 + vec2( 2.0, 5.0 ) ) );
1813
+ float major = 1.0 - smoothstep( 0.0, 0.05, abs( selvaFbm( w ) - 0.5 ) );
1814
+ float fine = 1.0 - smoothstep( 0.0, 0.02, abs( selvaFbm( w * 3.0 + vec2( 9.0, 1.0 ) ) - 0.5 ) );
1815
+ float cloud = ( selvaFbm( uv * 0.003 ) - 0.5 ) * selvaDetail( 60.0 );
1816
+ float vein = ( major * 0.35 * selvaDetail( 6.0 ) + fine * 0.2 * selvaDetail( 2.0 ) ) * feather;
1817
+ tone = vec3( 1.0 + cloud * 0.06 - vein - feather * 0.04 );
1818
+ gloss = 1.0 + vein * 0.3;`,
1819
+ slate: `
1820
+ float cleft = selvaFbm( uv * vec2( 0.012, 0.02 ) );
1821
+ float flake = mix( cleft, floor( cleft * 4.0 ) / 4.0, 0.6 );
1822
+ float seen = selvaDetail( 6.0 );
1823
+ float glint = ( selvaNoise( uv * 0.3 ) - 0.5 ) * selvaDetail( 3.0 );
1824
+ tone = vec3( 1.0 + ( flake - 0.5 ) * 0.18 * seen + glint * 0.06 );
1825
+ gloss = 1.0 - ( flake - 0.5 ) * 0.3 * seen + glint * 0.2;
1826
+ height = flake * 0.0015 * seen;`,
1827
+ terrazzo: `
1828
+ vec3 chipCell = selvaCell( uv / 9.0 );
1829
+ float chip = smoothstep( 0.1, 0.16, chipCell.y - chipCell.x ) * step( 0.12, selvaHash( vec2( chipCell.z, 5.0 ) ) );
1830
+ float k = selvaHash( vec2( chipCell.z, 7.0 ) );
1831
+ vec3 stone = k < 0.6 ? vec3( 1.1 ) : k < 0.75 ? vec3( 0.65 ) : k < 0.85 ? vec3( 0.22 ) : k < 0.9 ? vec3( 0.85, 0.69, 0.44 ) : k < 0.95 ? vec3( 0.64, 0.35, 0.28 ) : vec3( 0.93, 0.72, 0.66 );
1832
+ vec3 gritCell = selvaCell( uv / 2.5 );
1833
+ float grit = smoothstep( 0.1, 0.2, gritCell.y - gritCell.x ) * step( 0.6, selvaHash( vec2( gritCell.z, 2.0 ) ) );
1834
+ float g = selvaHash( vec2( gritCell.z, 9.0 ) ) < 0.5 ? 0.4 : 1.1;
1835
+ vec3 near = mix( vec3( 1.0 ), stone, chip );
1836
+ near = mix( near, vec3( g ), grit * ( 1.0 - chip ) * selvaDetail( 2.5 ) );
1837
+ // From afar the chips average to a slightly darker matrix.
1838
+ tone = mix( vec3( 0.92 ), near, selvaDetail( 9.0 ) );
1839
+ gloss = 1.0 + chip * 0.1;`,
1840
+ paving: `
1841
+ vec2 size = vec2( 600.0, 400.0 );
1842
+ float row = floor( uv.y / size.y );
1843
+ vec2 laid = vec2( uv.x + mod( row, 2.0 ) * size.x * 0.5, uv.y );
1844
+ vec2 id = floor( laid / size );
1845
+ vec2 edge = abs( fract( laid / size ) - 0.5 ) * size;
1846
+ float inset = min( size.x * 0.5 - edge.x, size.y * 0.5 - edge.y );
1847
+ float joint = ( 1.0 - smoothstep( 2.5, 3.5, inset ) ) * selvaDetail( 5.0 );
1848
+ float chamfer = ( 1.0 - smoothstep( 2.5, 6.0, inset ) ) * selvaDetail( 4.0 );
1849
+ float slab = ( selvaHash( id ) - 0.5 ) * 0.12 * selvaDetail( 100.0 );
1850
+ float blotch = ( selvaFbm( uv * 0.006 ) - 0.5 ) * 0.12 * selvaDetail( 40.0 );
1851
+ float speck = ( selvaNoise( uv * 0.7 ) - 0.5 ) * 0.06 * selvaDetail( 1.5 );
1852
+ tone = vec3( 1.0 + slab + blotch + speck ) * mix( 1.0, 0.55, joint );
1853
+ gloss = 1.0 + joint * 0.1;
1854
+ height = -chamfer * 0.002 - joint * 0.003;`,
1855
+ setts: `
1856
+ vec2 size = vec2( 110.0, 100.0 );
1857
+ vec2 warped = uv + ( vec2( selvaNoise( uv * 0.03 ), selvaNoise( uv * 0.03 + vec2( 3.0, 8.0 ) ) ) - 0.5 ) * 10.0;
1858
+ float row = floor( warped.y / size.y );
1859
+ vec2 laid = vec2( warped.x + selvaHash( vec2( row, 4.0 ) ) * size.x, warped.y );
1860
+ vec2 id = floor( laid / size );
1861
+ vec2 edge = abs( fract( laid / size ) - 0.5 ) * size;
1862
+ float inset = min( size.x * 0.5 - edge.x, size.y * 0.5 - edge.y );
1863
+ float joint = ( 1.0 - smoothstep( 4.0, 6.0, inset ) ) * selvaDetail( 10.0 );
1864
+ float dome = smoothstep( 4.0, 25.0, inset );
1865
+ vec3 crystal = selvaCell( uv / 4.0 );
1866
+ // Feldspar, quartz and biotite in about the shares of a grey granite. The dark crystals
1867
+ // gather in clusters, which is what still reads as granite from a few metres.
1868
+ float cluster = selvaFbm( uv * 0.04 ) - 0.5;
1869
+ float dark = 0.2 + cluster * 0.2;
1870
+ float mineral = crystal.z < dark ? 0.35 : ( crystal.z < dark + 0.3 ? 1.35 : 1.0 );
1871
+ mineral *= 1.0 + ( selvaHash( vec2( crystal.z, 3.0 ) ) - 0.5 ) * 0.15;
1872
+ mineral = mix( 1.0 - cluster * 0.25 * selvaDetail( 15.0 ), mineral, selvaDetail( 4.0 ) );
1873
+ float tint = selvaHash( id + vec2( 5.0, 2.0 ) ) - 0.5;
1874
+ vec3 sett = ( 1.0 + ( selvaHash( id ) - 0.5 ) * 0.16 ) * ( 1.0 + vec3( 0.05, 0.0, -0.06 ) * tint * 2.0 );
1875
+ sett = mix( vec3( 1.0 ), sett, selvaDetail( 100.0 ) );
1876
+ tone = mix( sett * mineral, vec3( 1.05, 0.97, 0.82 ), joint );
1877
+ gloss = 1.0 + joint * 0.1;
1878
+ height = ( dome * 0.006 + ( selvaNoise( uv * 0.15 ) - 0.5 ) * 0.001 ) * selvaDetail( 10.0 );`,
1879
+ gravel: `
1880
+ vec3 pebble = selvaCell( uv / 12.0 );
1881
+ float gap = 1.0 - smoothstep( 0.04, 0.18, pebble.y - pebble.x );
1882
+ float dome = sqrt( clamp( 1.0 - pebble.x * 1.4, 0.0, 1.0 ) );
1883
+ float k = selvaHash( vec2( pebble.z, 3.0 ) );
1884
+ vec3 stone = k < 0.3 ? vec3( 1.13, 1.14, 1.11 ) : k < 0.55 ? vec3( 0.96, 0.85, 0.71 ) : k < 0.7 ? vec3( 0.84, 0.64, 0.59 ) : k < 0.9 ? vec3( 0.74, 0.8, 0.89 ) : vec3( 0.47, 0.5, 0.56 );
1885
+ stone *= 1.0 + ( selvaHash( vec2( pebble.z, 8.0 ) ) - 0.5 ) * 0.15;
1886
+ float seen = selvaDetail( 12.0 );
1887
+ tone = mix( vec3( 0.85 ), stone * mix( 1.0, 0.35, gap ) * ( 0.85 + 0.15 * dome ), seen );
1888
+ gloss = 1.0 + gap * 0.1 - dome * 0.1;
1889
+ height = dome * 0.004 * seen;`,
1890
+ molded: `
1891
+ float pit = selvaNoise( uv * 1.6 );
1892
+ float mottle = selvaNoise( uv * 0.5 + vec2( 7.0, 3.0 ) );
1893
+ float grit = ( pit * 0.65 + mottle * 0.35 ) - 0.5;
1894
+ float seen = selvaDetail( 0.6 );
1895
+ tone = vec3( 1.0 + grit * 0.1 * seen );
1896
+ gloss = 1.0 + grit * 0.9 * seen;
1897
+ height = ( grit + 0.5 ) * 0.00008 * seen;`,
1725
1898
  corten: `
1726
1899
  float bloom = 0.5 + ( selvaFbm( uv * vec2( 0.006, 0.0025 ) ) - 0.5 ) * selvaDetail( 25.0 );
1727
1900
  float scale = 0.5 + ( selvaFbm( uv * 0.06 ) - 0.5 ) * selvaDetail( 8.0 );
@@ -1730,7 +1903,6 @@ const RECIPES$1 = {
1730
1903
  gloss = 1.0 + ( scale - 0.5 ) * 0.25;
1731
1904
  height = scale * 0.0004;`
1732
1905
  };
1733
- const f$1 = (n) => n.toFixed(4);
1734
1906
  /** Null for the finishes `finish.ts` draws itself. */
1735
1907
  function surfacePatch(finish, strength) {
1736
1908
  const recipe = RECIPES$1[finish];
@@ -1764,6 +1936,30 @@ function surfacePatch(finish, strength) {
1764
1936
  }
1765
1937
  return sum / 0.9375;
1766
1938
  }
1939
+ // Cellular noise: distance to the nearest and second-nearest jittered point, and the
1940
+ // nearest one's id. y - x is small along a cell border.
1941
+ vec3 selvaCell( vec2 p ) {
1942
+ vec2 i = floor( p );
1943
+ vec2 fp = fract( p );
1944
+ float f1 = 8.0;
1945
+ float f2 = 8.0;
1946
+ float id = 0.0;
1947
+ for ( int y = -1; y <= 1; y ++ ) {
1948
+ for ( int x = -1; x <= 1; x ++ ) {
1949
+ vec2 g = vec2( float( x ), float( y ) );
1950
+ vec2 o = vec2( selvaHash( i + g ), selvaHash( i + g + vec2( 19.0, 7.0 ) ) );
1951
+ float d = length( g + o * 0.8 + 0.1 - fp );
1952
+ if ( d < f1 ) {
1953
+ f2 = f1;
1954
+ f1 = d;
1955
+ id = selvaHash( i + g + vec2( 3.0, 11.0 ) );
1956
+ } else if ( d < f2 ) {
1957
+ f2 = d;
1958
+ }
1959
+ }
1960
+ }
1961
+ return vec3( f1, f2, id );
1962
+ }
1767
1963
  float selvaDetail( float featureMm ) {
1768
1964
  return 1.0 - smoothstep( 0.5, 1.5, selvaFootprint / featureMm );
1769
1965
  }`).replace("#include <color_fragment>", `#include <color_fragment>
@@ -1900,6 +2096,10 @@ function cullsBackFaces(matData, options) {
1900
2096
  if (options?.appearance?.cullBackfaces) return true;
1901
2097
  return (options?.closed ?? false) && !matData.transparent && matData.opacity >= 1;
1902
2098
  }
2099
+ const FROSTED = .2;
2100
+ function isPane(matData) {
2101
+ return (matData.transmission ?? 0) > 0 && !((matData.thickness ?? 0) > 0) && matData.roughness < FROSTED;
2102
+ }
1903
2103
  function createMaterialPicker(materialsSrc, options) {
1904
2104
  const built = /* @__PURE__ */ new Map();
1905
2105
  return (materialId, closed) => {
@@ -1952,15 +2152,18 @@ function createMaterial(matData, options) {
1952
2152
  material.clearcoat = matData.clearcoat;
1953
2153
  material.clearcoatRoughness = matData.clearcoatRoughness ?? 0;
1954
2154
  }
2155
+ if (matData.sheen != null && matData.sheen > 0) {
2156
+ material.sheen = matData.sheen;
2157
+ material.sheenColor.set(matData.sheenColor ?? "#ffffff");
2158
+ material.sheenRoughness = matData.sheenRoughness ?? .5;
2159
+ }
1955
2160
  if (matData.anisotropy != null && matData.anisotropy > 0 && options?.hasUvs) {
1956
2161
  material.anisotropy = matData.anisotropy;
1957
2162
  material.anisotropyRotation = matData.anisotropyRotation ?? 0;
1958
2163
  }
1959
- if (matData.transmission != null) {
1960
- material.transmission = matData.transmission;
1961
- material.thickness = 0;
1962
- }
1963
2164
  if (matData.ior != null) material.ior = matData.ior;
2165
+ if (matData.transmission != null && matData.transmission > 0) applyGlass(material, matData);
2166
+ if (matData.metalness > 0) dimSunOnMetal(material);
1964
2167
  if (vertexColors) applyVertexColorSRGBDecode(material);
1965
2168
  const finish = matData.finish && options?.hasUvs ? finishPatch(matData.finish, matData.finishStrength ?? 1) : null;
1966
2169
  if (finish) {
@@ -1976,6 +2179,50 @@ function createMaterial(matData, options) {
1976
2179
  if (matData.normalMap) applyTexture(material, matData.normalMap, "normalMap", mapSize);
1977
2180
  return material;
1978
2181
  }
2182
+ const SUN_ON_METAL = .1;
2183
+ const SUN_BEGIN = "#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )";
2184
+ const SUN_END = "#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )";
2185
+ /** Scales the directional lights' specular by metalness; point lights keep theirs. */
2186
+ function dimSunOnMetal(material) {
2187
+ addShaderPatch(material, "sun-on-metal", (shader) => {
2188
+ shader.fragmentShader = shader.fragmentShader.replace("#include <lights_fragment_begin>", three.ShaderChunk.lights_fragment_begin.replace(SUN_BEGIN, `vec3 specularBeforeSun = reflectedLight.directSpecular;
2189
+ ${SUN_BEGIN}`).replace(SUN_END, `reflectedLight.directSpecular = specularBeforeSun + ( reflectedLight.directSpecular - specularBeforeSun ) * mix( 1.0, ${SUN_ON_METAL.toFixed(2)}, metalnessFactor );
2190
+ ${SUN_END}`));
2191
+ });
2192
+ }
2193
+ /**
2194
+ * three's transmission draws every opaque object a second time into its own buffer each frame, and
2195
+ * that buffer never holds other glass: one pane seen through another vanishes. So only glass that
2196
+ * needs it gets it: with a thickness, to refract and tint, or frosted, to blur. A pane blends
2197
+ * instead: cheap, and panes stack.
2198
+ */
2199
+ function applyGlass(material, matData) {
2200
+ const transmission = Math.min(1, matData.transmission);
2201
+ material.userData[require_gpu_dispose.GLASS] = true;
2202
+ if (!isPane(matData)) {
2203
+ const thickness = matData.thickness ?? 0;
2204
+ material.transmission = transmission;
2205
+ material.thickness = thickness;
2206
+ if (matData.attenuationColor && thickness > 0) {
2207
+ material.attenuationColor.set(matData.attenuationColor);
2208
+ material.attenuationDistance = thickness;
2209
+ addShaderPatch(material, "glass-attenuation", (shader) => {
2210
+ shader.fragmentShader = shader.fragmentShader.replace("#include <transmission_fragment>", three.ShaderChunk.transmission_fragment.replace("material.attenuationDistance = attenuationDistance;", "material.attenuationDistance = attenuationDistance * length( modelMatrix[ 0 ].xyz );"));
2211
+ });
2212
+ }
2213
+ return;
2214
+ }
2215
+ material.transparent = true;
2216
+ material.opacity = matData.opacity * (1 - transmission * .96);
2217
+ material.depthWrite = false;
2218
+ material.blending = three.CustomBlending;
2219
+ material.blendSrc = three.OneFactor;
2220
+ material.blendDst = three.OneMinusSrcAlphaFactor;
2221
+ addShaderPatch(material, "glass-pane", (shader) => {
2222
+ shader.fragmentShader = shader.fragmentShader.replace("#include <opaque_fragment>", `vec3 paneSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;
2223
+ gl_FragColor = vec4( ( outgoingLight - paneSpecular ) * diffuseColor.a + paneSpecular, diffuseColor.a );`);
2224
+ });
2225
+ }
1979
2226
  /**
1980
2227
  * Chains `patch` after any earlier one. three caches compiled programs by `customProgramCacheKey`,
1981
2228
  * which defaults to the `onBeforeCompile` source; a chained wrapper's source is the same whatever
@@ -2210,7 +2457,7 @@ function finalizeMergedMesh(geometry, group, material) {
2210
2457
  const firstMesh = group.meshes[0];
2211
2458
  const meshNames = group.meshes.map((m) => m.name).filter((name) => name && name.length > 0);
2212
2459
  threeMesh.name = meshNames.length > 0 ? meshNames[0] : `merged_material_${group.materialId}`;
2213
- threeMesh.castShadow = true;
2460
+ threeMesh.castShadow = !require_gpu_dispose.isGlass(material);
2214
2461
  threeMesh.receiveShadow = true;
2215
2462
  threeMesh.userData = {
2216
2463
  source: "compute",
@@ -2261,7 +2508,7 @@ function finalizeSingleMesh(geometry, meshMeta, material) {
2261
2508
  trackingKey: meshMeta.id,
2262
2509
  metadata: meshMeta.metadata ?? {}
2263
2510
  };
2264
- mesh.castShadow = true;
2511
+ mesh.castShadow = !require_gpu_dispose.isGlass(material);
2265
2512
  mesh.receiveShadow = true;
2266
2513
  return mesh;
2267
2514
  }