Modeller (13 → 26): chest, bench, torch, lantern, bell, sign, hay bale,
sack, broom, tankard, book, phial, coin pouch. Kürek, çapa ve yaba
render'lara bakılarak yeniden yazıldı.
core:
- createKitModel artık tipli `actions` ve `update(dt)` taşıyor
- BuiltPart.extras: bir parça birden çok materyal yuvası kullanabiliyor
(sandığın kapağı = meşe tahta + demir kayış, birlikte dönmek zorunda)
- BuiltPart.origin: parça başına dönme merkezi (menteşe, sarkaç)
- bendGeometry / roughenGeometry / mottleGeometry
- 5 yeni materyal yuvası: steel, brass, straw, cloth, leather, glass
- bandGeometry({ inner }) — serbest halkalar kapalı katı olabilsin
my-registry/meta.ts: katalog metadata'sı artık build.ts ve viewer için
tek kaynak. Doğrulama modelle metadata'nın uyuştuğunu denetliyor ve bu
eklendiği gün dört modelde gerçek bir sapma yakaladı.
Doğrulama (~500 kontrol):
- metadata uyumu, eylem semantiği, kare hızı bağımsızlığı
- scripts/render.ts: tarayıcısız yazılım rasterleyici. Bütün doğrulama
geometrikti; "bu kürek küreğe benzemiyor" diyebilmek için modele
bakmak gerekiyordu
- scripts/verify-glb.ts: dışa aktar → geri oku → karşılaştır
GLB dışa aktarımı: viewer düğmesi ve toplu CLI aynı kodu kullanıyor.
Viewer:
- parça paneli (solo/izole) ve materyal yuvası paneli (canlı renk override)
- kit görünümü: 26 model tek sahnede, ölçek ve ton tutarlılığı için
- configure() sonrası gölge bayraklarının düşmesi düzeltildi
139 lines
5.5 KiB
TypeScript
139 lines
5.5 KiB
TypeScript
/**
|
|
* @medieval-kit/linen-sack
|
|
*
|
|
* Ağzı iple bağlanmış tahıl çuvalı. Depo, değirmen, pazar tezgâhı, at arabası —
|
|
* kitin en çok yere yakışan parçalarından.
|
|
*
|
|
* Çuvalı çuval yapan şey, içindeki şeyin biçimini ALMASI. Bu yüzden gövde
|
|
* silindir değil: dipte tahılın ağırlığıyla yayvanlaşıyor, ortada şişiyor,
|
|
* ağza doğru toplanıyor. Sonra `roughenGeometry` yüzeyi bozuyor, çünkü dolu
|
|
* bir çuvalın hiçbir yeri düz olmaz.
|
|
*
|
|
* Dip köşeleri ayrı bir mesele: gerçek çuval dört köşesinden büzülür ve o
|
|
* köşeler kulak gibi dışarı çıkar. Onlar olmadan model bir vazoya benziyordu.
|
|
*/
|
|
import type { BufferGeometry } from 'three'
|
|
|
|
import {
|
|
bandGeometry,
|
|
boxGeometry,
|
|
createKitModel,
|
|
createTinter,
|
|
jitter,
|
|
latheGeometry,
|
|
mergeColoured,
|
|
roughenGeometry,
|
|
type Level,
|
|
} from '../core/index.ts'
|
|
|
|
export interface LinenSackConfig {
|
|
/** Toplam yükseklik (metre). */
|
|
readonly height: number
|
|
/** En geniş yerin yarıçapı (metre). */
|
|
readonly radius: number
|
|
/** Ne kadar dolu. 1 = tıka basa, 0.4 = yarı boş ve sarkık. */
|
|
readonly fill: number
|
|
/** Ağzın üstünde kalan bez payı, yüksekliğin oranı olarak. */
|
|
readonly collar: number
|
|
/** Dipteki büzülme kulakları. */
|
|
readonly ears: number
|
|
readonly seed: number
|
|
}
|
|
|
|
export const linenSackDefaults: LinenSackConfig = {
|
|
height: 0.52,
|
|
radius: 0.16,
|
|
fill: 0.85,
|
|
collar: 0.14,
|
|
ears: 4,
|
|
seed: 53,
|
|
}
|
|
|
|
export type LinenSackParts = 'body' | 'collar' | 'cord'
|
|
|
|
export function createModel(overrides: Partial<LinenSackConfig> = {}) {
|
|
return createKitModel<LinenSackConfig, 'cloth', LinenSackParts>({
|
|
id: 'linen-sack',
|
|
defaults: linenSackDefaults,
|
|
slots: ['cloth'],
|
|
build: ({ config, random }) => {
|
|
const tint = createTinter(random)
|
|
const half = config.height / 2
|
|
const fill = Math.max(0.15, Math.min(1, config.fill))
|
|
// Ağız boğumunun yeri: dolu çuvalda yukarıda, boş çuvalda aşağıda kalır.
|
|
const neckY = half - config.height * config.collar
|
|
const bodyTop = neckY - config.height * 0.06
|
|
|
|
// --- Gövde ----------------------------------------------------------
|
|
// Profil doluluğa bağlı: az dolu bir çuval hem alçalır hem yanlara
|
|
// yayılır. Tek bir `fill` sayısının siluetin tamamını değiştirmesi,
|
|
// aynı modelden dolu ve yarı boş iki çuval çıkarabilmek demek.
|
|
const wide = config.radius * (0.72 + fill * 0.36)
|
|
const profile: Level[] = [
|
|
{ y: -half, radius: wide * 0.72 },
|
|
{ y: -half + config.height * 0.05, radius: wide * 0.95 },
|
|
{ y: -half + config.height * 0.26 * fill, radius: wide },
|
|
{ y: -half + config.height * 0.52 * fill, radius: wide * 0.93 },
|
|
{ y: bodyTop - config.height * 0.16, radius: wide * 0.7 },
|
|
{ y: bodyTop, radius: config.radius * 0.34 },
|
|
]
|
|
const body = latheGeometry(profile, 9, [0, 0, 0], tint('cloth', -0.06, 1.3), {
|
|
colourTop: tint('cloth', 0.05, 1.3),
|
|
})
|
|
// Bez sert değil: yüzey bozulması burada dokunun kendisi.
|
|
roughenGeometry(body, config.radius * 0.05, { salt: 21, scaleY: 0.7 })
|
|
|
|
const pieces: BufferGeometry[] = [body]
|
|
|
|
// --- Dip kulakları ---------------------------------------------------
|
|
// Çuval dikişten büzülür ve köşeleri dışarı fırlar. Onlar olmadan
|
|
// silindir bir vazo çıkıyor.
|
|
const ears = Math.max(0, Math.round(config.ears))
|
|
for (let i = 0; i < ears; i += 1) {
|
|
const angle = (i / ears) * Math.PI * 2 + jitter(random, 0.12)
|
|
const reach = wide * (0.3 + random() * 0.16)
|
|
const ear = boxGeometry(
|
|
[reach, config.height * 0.05, config.radius * 0.2],
|
|
[reach * 0.36, 0, 0], // kökü gövdenin İÇİNDE kalsın
|
|
tint('cloth', -0.1, 1.2),
|
|
)
|
|
// Önce yönlendir, sonra taşı — ters sıra kulağı yörüngeye savurur.
|
|
ear.rotateZ(-0.22 + jitter(random, 0.1))
|
|
ear.rotateY(-angle)
|
|
ear.translate(
|
|
Math.sin(angle) * wide * 0.6,
|
|
-half + config.height * 0.035,
|
|
Math.cos(angle) * wide * 0.6,
|
|
)
|
|
pieces.push(ear)
|
|
}
|
|
|
|
// --- Ağız payı -------------------------------------------------------
|
|
// Bağın ÜSTÜNDE kalan, dışa devrilen bez. Çuvalı kapalı bir torbadan
|
|
// ayıran şey bu: bağlanmış bir ağzın hep fazlası olur.
|
|
const collarPieces: BufferGeometry[] = []
|
|
const flare: Level[] = [
|
|
{ y: neckY - config.height * 0.02, radius: config.radius * 0.3 },
|
|
{ y: neckY + config.height * 0.03, radius: config.radius * 0.27 },
|
|
{ y: half - config.height * 0.02, radius: config.radius * 0.46 },
|
|
{ y: half, radius: config.radius * 0.4 },
|
|
]
|
|
const collar = latheGeometry(flare, 9, [0, 0, 0], tint('cloth', 0.02, 1.3), {
|
|
colourTop: tint('cloth', 0.1, 1.3),
|
|
capTop: true,
|
|
})
|
|
roughenGeometry(collar, config.radius * 0.035, { salt: 22, scaleY: 0.6 })
|
|
collarPieces.push(collar)
|
|
|
|
// --- İp ---------------------------------------------------------------
|
|
const cord = bandGeometry(config.radius * 0.29, neckY, config.height * 0.035,
|
|
config.radius * 0.045, 9, tint('cloth', -0.24, 0.8), { inner: true })
|
|
|
|
return {
|
|
body: { slot: 'cloth' as const, geometry: mergeColoured(pieces) },
|
|
collar: { slot: 'cloth' as const, geometry: mergeColoured(collarPieces) },
|
|
cord: { slot: 'cloth' as const, geometry: mergeColoured([cord]) },
|
|
}
|
|
},
|
|
}, overrides)
|
|
}
|