aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/main.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:51:36 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:51:36 +0900
commit257516e2bde90e65439d01f19434cd2d9d5a03f7 (patch)
treecab680958a6bb1249c288d5974b083cfa9de1f3f /public/bluebey-studio/src/main.js
parent1bf7ee115176bc366315a90ab31740e6cd968744 (diff)
bluebey-studio: 帽子切替のエラー修正とGLB帽子インポート、閉じ目の目線範囲・まつげ位置を調整
帽子切替時にトラバース中でハルを外して子がundefinedになる不具合を修正。GLBの帽子モデルを読み込めるようにし、なしで解除できるようにした。閉じ目の目線範囲を通常並みに広げ、まつげを上まぶたの外側へ配置。
Diffstat (limited to 'public/bluebey-studio/src/main.js')
-rw-r--r--public/bluebey-studio/src/main.js102
1 files changed, 78 insertions, 24 deletions
diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js
index 849d3d1..322a0e6 100644
--- a/public/bluebey-studio/src/main.js
+++ b/public/bluebey-studio/src/main.js
@@ -1,5 +1,6 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { loadModel } from './model.js';
import { Face } from './face.js';
import { Styles, isLineStyle } from './styles.js';
@@ -380,6 +381,8 @@ async function init() {
let hatKindCurrent = null;
let hatObject = null;
+ /** A GLB hat the user loaded; kept so it can be re-chosen without re-importing. */
+ let customHatObject = null;
const hatMeshes = [];
/** Last transform applied, so the render loop only wakes when it changes. */
const hatXform = { x: null, z: null, height: null, tiltX: null, tiltZ: null };
@@ -388,32 +391,52 @@ async function init() {
/** Set once the style system exists, so a hat can register for styles + outline. */
let hatStyleRegister = null;
let hatStyleUnregister = null;
- function applyHat(kind) {
- if (!hatMount) return;
- if (kind !== hatKindCurrent) {
- hatKindCurrent = kind;
- if (hatObject) {
- hatObject.traverse((o) => { if (o.isMesh) hatStyleUnregister?.(o); });
- hatMount.remove(hatObject);
- hatObject.traverse((o) => {
- o.geometry?.dispose();
- if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose());
- else o.material?.dispose();
- });
- hatObject = null;
+
+ /**
+ * Swap the mounted hat for `built` (or clear it), keeping the style system and
+ * the outline exclusion in step.
+ *
+ * The old meshes are collected BEFORE they are unregistered: `removeMesh`
+ * deletes the outline hull (a child of the hat group) while we traverse, which
+ * used to shorten `children` mid-loop and call `.traverse` on `undefined`.
+ */
+ function mountHat(built) {
+ if (hatObject) {
+ const old = [];
+ hatObject.traverse((o) => { if (o.isMesh) old.push(o); });
+ hatMount.remove(hatObject);
+ for (const mesh of old) hatStyleUnregister?.(mesh);
+ // The custom model is kept so it can be re-chosen; a built hat is dropped.
+ if (hatObject !== customHatObject) {
+ for (const mesh of old) {
+ mesh.geometry?.dispose();
+ if (Array.isArray(mesh.material)) mesh.material.forEach((m) => m.dispose());
+ else mesh.material?.dispose();
+ }
}
- hatMeshes.length = 0;
- const built = buildHat(kind);
- if (built) {
- hatMount.add(built);
- hatObject = built;
- built.traverse((o) => {
- if (!o.isMesh) return;
- hatMeshes.push(o);
- hatStyleRegister?.(o);
- });
+ hatObject = null;
+ }
+ hatMeshes.length = 0;
+ if (built) {
+ hatMount.add(built);
+ hatObject = built;
+ const meshes = [];
+ built.traverse((o) => { if (o.isMesh) meshes.push(o); });
+ for (const mesh of meshes) {
+ hatMeshes.push(mesh);
+ hatStyleRegister?.(mesh);
}
- refreshOutlineExclusion?.();
+ }
+ refreshOutlineExclusion?.();
+ }
+
+ function applyHat(kind) {
+ if (!hatMount) return;
+ const custom = state.face.hat?.custom === true && customHatObject;
+ const key = custom ? 'custom' : kind;
+ if (key !== hatKindCurrent) {
+ hatKindCurrent = key;
+ mountHat(custom ? customHatObject : buildHat(kind));
}
// 高さ and 傾き. The mount is world-aligned (see above), so +y is up, +z is the
@@ -438,6 +461,36 @@ async function init() {
app.needsRender = true;
}
}
+ /**
+ * Import a GLB hat. It is centred sideways, dropped so its base sits at y = 0
+ * (the mount sits on the head), and scaled to about the head's width, then
+ * mounted like any built hat.
+ */
+ async function loadHatModel(file) {
+ if (!hatMount) return;
+ const buffer = await exporter.readFileAsArrayBuffer(file);
+ const loader = new GLTFLoader();
+ const gltf = await new Promise((resolve, reject) => loader.parse(buffer, '', resolve, reject));
+ const root = gltf.scene;
+ const box = new THREE.Box3().setFromObject(root);
+ const size = box.getSize(new THREE.Vector3());
+ const target = Math.max(0.2, model.size.x * 0.95);
+ const scale = target / Math.max(size.x, size.z, 0.001);
+ root.scale.setScalar(scale);
+ box.setFromObject(root);
+ const center = box.getCenter(new THREE.Vector3());
+ root.position.x -= center.x;
+ root.position.z -= center.z;
+ root.position.y -= box.min.y;
+ root.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = false; } });
+ customHatObject = root;
+ state.face.hat.custom = true;
+ state.face.hat.kind = 'none';
+ hatKindCurrent = null;
+ refresh('all');
+ app.panel?.sync();
+ }
+
applyHat(state.face.hat?.kind ?? 'none');
const halfHeight = model.size.y * 0.5;
@@ -1428,6 +1481,7 @@ async function init() {
capturePose,
applyState,
refresh,
+ loadHatModel,
patch: (scope, value) => applyState({ [scope]: value }, { scope }),
syncPanel: () => app.panel?.sync(),
imageCanvas: (options) => captureImage(options),