auto-git:

[change] src/app/App.tsx
 [change] src/document/scene-document.ts
 [change] src/geometry/box-face-uvs.ts
 [change] src/materials/starter-material-textures.ts
 [change] src/runtime-three/runtime-host.ts
 [change] src/viewport-three/viewport-host.ts
This commit is contained in:
2026-04-15 04:05:05 +02:00
parent 34276718de
commit 1fdbf50d16
6 changed files with 258 additions and 129 deletions

View File

@@ -1,89 +1,161 @@
import { CanvasTexture, RepeatWrapping, SRGBColorSpace } from "three";
import {
RepeatWrapping,
SRGBColorSpace,
Texture,
TextureLoader
} from "three";
import type { MaterialDef } from "./starter-material-library";
import {
getStarterMaterialBaseColorUrl,
getStarterMaterialMetallicUrl,
getStarterMaterialNormalUrl,
getStarterMaterialRoughnessUrl,
getStarterMaterialSpecularUrl,
getStarterMaterialTextureRepeat,
type MaterialDef
} from "./starter-material-library";
export interface StarterMaterialTextureSet {
baseColor: Texture;
normal: Texture;
roughness: Texture;
metallic: Texture | null;
specular: Texture | null;
}
export function createStarterMaterialSignature(material: MaterialDef): string {
return `${material.baseColorHex}|${material.accentColorHex}|${material.pattern}`;
return [
material.assetFolder,
material.workflow,
material.previewImageName,
material.sizeCm.width,
material.sizeCm.height,
material.swatchColorHex
].join("|");
}
function fillMaterialPattern(context: CanvasRenderingContext2D, material: MaterialDef, size: number) {
context.fillStyle = material.baseColorHex;
context.fillRect(0, 0, size, size);
context.strokeStyle = material.accentColorHex;
context.fillStyle = material.accentColorHex;
function isPowerOfTwo(value: number): boolean {
return value > 0 && (value & (value - 1)) === 0;
}
switch (material.pattern) {
case "grid":
context.lineWidth = Math.max(2, size / 32);
function floorPowerOfTwo(value: number): number {
return 2 ** Math.max(0, Math.floor(Math.log2(Math.max(1, value))));
}
for (let offset = 0; offset <= size; offset += size / 4) {
context.beginPath();
context.moveTo(offset, 0);
context.lineTo(offset, size);
context.stroke();
context.beginPath();
context.moveTo(0, offset);
context.lineTo(size, offset);
context.stroke();
}
break;
case "checker": {
const checkerSize = size / 4;
for (let row = 0; row < 4; row += 1) {
for (let column = 0; column < 4; column += 1) {
if ((row + column) % 2 === 0) {
context.fillRect(column * checkerSize, row * checkerSize, checkerSize, checkerSize);
}
}
}
break;
}
case "stripes":
context.lineWidth = size / 6;
for (let offset = -size; offset <= size * 2; offset += size / 3) {
context.beginPath();
context.moveTo(offset, size);
context.lineTo(offset + size, 0);
context.stroke();
}
break;
case "diamond":
context.lineWidth = Math.max(2, size / 28);
for (let offset = -size; offset <= size; offset += size / 3) {
context.beginPath();
context.moveTo(size * 0.5, offset);
context.lineTo(size - offset, size * 0.5);
context.lineTo(size * 0.5, size - offset);
context.lineTo(-offset, size * 0.5);
context.closePath();
context.stroke();
}
break;
function createRepeatableTextureImage(
image: { width: number; height: number }
): { width: number; height: number } | HTMLCanvasElement {
if (isPowerOfTwo(image.width) && isPowerOfTwo(image.height)) {
return image;
}
}
export function createStarterMaterialTexture(material: MaterialDef, size = 128): CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
canvas.width = floorPowerOfTwo(image.width);
canvas.height = floorPowerOfTwo(image.height);
const context = canvas.getContext("2d");
if (context === null) {
throw new Error("2D canvas context is unavailable for starter material texture generation.");
throw new Error("2D canvas context is unavailable for starter material texture conversion.");
}
fillMaterialPattern(context, material, size);
context.drawImage(
image as CanvasImageSource,
0,
0,
canvas.width,
canvas.height
);
const texture = new CanvasTexture(canvas);
return canvas;
}
function configureTexture(
texture: Texture,
material: MaterialDef,
options: { colorSpace?: typeof SRGBColorSpace | null }
) {
const repeat = getStarterMaterialTextureRepeat(material);
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
texture.colorSpace = SRGBColorSpace;
texture.needsUpdate = true;
texture.repeat.set(repeat.x, repeat.y);
texture.colorSpace = options.colorSpace ?? texture.colorSpace;
}
function loadMaterialTexture(
loader: TextureLoader,
url: string,
material: MaterialDef,
options: { colorSpace?: typeof SRGBColorSpace | null }
): Texture {
const texture = loader.load(url, (loadedTexture) => {
loadedTexture.image = createRepeatableTextureImage(
loadedTexture.image as { width: number; height: number }
);
configureTexture(loadedTexture, material, options);
loadedTexture.needsUpdate = true;
});
configureTexture(texture, material, options);
return texture;
}
export function createStarterMaterialTextureSet(
material: MaterialDef,
loader: TextureLoader = new TextureLoader()
): StarterMaterialTextureSet {
const metallicUrl = getStarterMaterialMetallicUrl(material);
const specularUrl = getStarterMaterialSpecularUrl(material);
return {
baseColor: loadMaterialTexture(loader, getStarterMaterialBaseColorUrl(material), material, {
colorSpace: SRGBColorSpace
}),
normal: loadMaterialTexture(loader, getStarterMaterialNormalUrl(material), material, {
colorSpace: null
}),
roughness: loadMaterialTexture(
loader,
getStarterMaterialRoughnessUrl(material),
material,
{
colorSpace: null
}
),
metallic:
metallicUrl === null
? null
: loadMaterialTexture(loader, metallicUrl, material, {
colorSpace: null
}),
specular:
specularUrl === null
? null
: loadMaterialTexture(loader, specularUrl, material, {
colorSpace: null
})
};
}
export function disposeStarterMaterialTextureSet(
textureSet: StarterMaterialTextureSet
): void {
const textures = new Set<Texture>([
textureSet.baseColor,
textureSet.normal,
textureSet.roughness
]);
if (textureSet.metallic !== null) {
textures.add(textureSet.metallic);
}
if (textureSet.specular !== null) {
textures.add(textureSet.specular);
}
for (const texture of textures) {
texture.dispose();
}
}