@7ots/cli 0.1.6 → 0.1.8
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 -3
- package/cli/7ots.mjs +13 -1
- package/cli/lib/account.mjs +152 -2
- package/cli/lib/assistant.mjs +107 -38
- package/cli/lib/brain.mjs +13 -2
- package/cli/lib/config.mjs +5 -1
- package/cli/lib/inferences.mjs +30 -6
- package/cli/lib/integrations.mjs +69 -5
- package/cli/lib/orquesta.mjs +44 -1
- package/cli/lib/pet-server.mjs +352 -11
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/pet/electron/director.cjs +37 -5
- package/cli/pet/electron/main.cjs +92 -10
- package/cli/pet/pet.html +149 -10
- package/cli/pet/settings.html +549 -43
- package/dist/7ots.esm.js +216 -137
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +216 -137
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +1 -1
- package/package.json +17 -14
- package/server/channels/agent.mjs +7 -1
- package/server/channels/index.mjs +3 -2
- package/server/platform/connect.mjs +4 -1
- package/server/platform/db.mjs +14 -1
- package/server/platform/device.mjs +101 -4
- package/server/platform/mods.mjs +222 -0
- package/server/platform/orquesta.mjs +78 -14
- package/server/platform/profile.mjs +161 -0
- package/server/platform/routes.mjs +39 -33
- package/server/platform/runtime.mjs +5 -3
- package/server/platform/store.mjs +1 -1
- package/server/platform/vm.mjs +688 -17
- package/server/server.mjs +12 -2
- package/server/tts.mjs +17 -4
- package/src/avatar/three.js +3 -2
- package/src/character/AvatarEditor.js +95 -7
- package/src/character/Character.js +116 -17
- package/src/character/TradingCard.js +226 -0
- package/src/character/mods/index.js +4 -0
- package/src/character/mods/orc.js +64 -0
- package/src/character/mods.js +251 -0
- package/src/character/more/acc-head.js +213 -0
- package/src/character/more/acc-rest.js +342 -0
- package/src/character/more/brows.js +39 -0
- package/src/character/more/cheeks.js +78 -0
- package/src/character/more/eyes.js +288 -0
- package/src/character/more/mouths.js +259 -0
- package/src/character/more/patterns.js +120 -0
- package/src/character/parts.js +100 -0
- package/src/character/skin.js +166 -1
- package/src/character/styles.js +1285 -9
- package/src/character3d/Character3D.js +253 -37
- package/src/character3d/index.js +7 -1
- package/src/character3d/model.js +367 -0
- package/src/i18n/messages/character.js +37 -22
- package/src/i18n/messages/platform.js +33 -0
- package/src/identity/random.js +17 -3
- package/src/identity/schema.js +10 -0
- package/src/index.js +1 -0
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import * as THREE from 'three';
|
|
12
12
|
import { normalizeCharacter, layout, bodyPoints } from '../character/Character.js';
|
|
13
13
|
import { stylizeSpec } from '../character/styles.js';
|
|
14
|
+
import { applyMods, modPart } from '../character/mods.js';
|
|
15
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Acabado 3D de cada estilo artístico (styles.js): la paleta sale de stylizeSpec y aquí se elige
|
|
@@ -19,20 +21,34 @@ import { stylizeSpec } from '../character/styles.js';
|
|
|
19
21
|
const STYLE_FINISH = {
|
|
20
22
|
ukiyoe: 'toon', glass: 'glossy', bauhaus: 'toon', manuscript: 'toon', glitch: 'neon', clay: 'clay', ink: 'sketch',
|
|
21
23
|
pop: 'toon', watercolor: 'soft3d', pixel: 'toon', synthwave: 'neon', nouveau: 'toon', cubism: 'toon', crayon: 'sketch', blueprint: 'neon', pointillism: 'soft3d', woodcut: 'sketch',
|
|
24
|
+
riso: 'toon', dotmatrix: 'toon', chalk: 'sketch', sticker: 'glossy', felt: 'clay', papercut: 'soft3d', lowpoly: 'toon', marble: 'soft3d', candy: 'glossy',
|
|
25
|
+
holo: 'glossy', sumie: 'sketch', tarot: 'toon', retro70: 'toon', mosaic: 'toon', neonsign: 'neon', graffiti: 'toon', thermal: 'soft3d',
|
|
22
26
|
};
|
|
27
|
+
/** Piezas del catálogo 2 sin modelo 3D propio → la existente más parecida (campo `as3d` en parts.js). */
|
|
28
|
+
function alias3D(s) {
|
|
29
|
+
s.eyes = { ...s.eyes, type: as3d('eyes', s.eyes.type) };
|
|
30
|
+
s.brows = { ...s.brows, type: as3d('brows', s.brows.type) };
|
|
31
|
+
s.mouth = { ...s.mouth, type: as3d('mouths', s.mouth.type) };
|
|
32
|
+
s.cheeks = { ...s.cheeks, type: as3d('cheeks', s.cheeks.type) };
|
|
33
|
+
s.body = { ...s.body, pattern: as3d('patterns', s.body.pattern) };
|
|
34
|
+
s.finish = as3d('finishes', s.finish);
|
|
35
|
+
s.accessories = s.accessories.map((a) => ({ ...a, id: as3d('accessories', a.id) }));
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
23
38
|
function styled3D(spec) {
|
|
24
|
-
const s = stylizeSpec(spec);
|
|
39
|
+
const s = alias3D(stylizeSpec(spec));
|
|
25
40
|
if (!s._style) return s;
|
|
26
41
|
const f2 = s.finish;
|
|
27
42
|
s.finish = STYLE_FINISH[s._style.id] || (f2 === 'clay' || f2 === 'sketch' ? f2 : s._style.lw > 0 ? 'toon' : f2 === 'flat' ? 'soft3d' : f2);
|
|
28
43
|
return s;
|
|
29
44
|
}
|
|
30
|
-
import { shade, luminance } from '../character/parts.js';
|
|
45
|
+
import { shade, luminance, as3d } from '../character/parts.js';
|
|
31
46
|
import { textureOf } from '../character/skin.js';
|
|
32
47
|
import { GESTURES, GESTURE_ALIAS, VISEMES, EASE, clamp, lerp, approach, textToVisemes, babbleVisemes, HAND_POSES, MORPHS, EFFECTS, shapePoints, seeded } from '../character/motion.js';
|
|
33
48
|
import { U, bodyGeometry, surfaceProbe } from './body.js';
|
|
34
49
|
import { bodyMaterials, surfaceMaterial, mat, shadowTexture, CHEEK_IDS } from './materials.js';
|
|
35
50
|
import { FX3D, glyph, disposeFx } from './fx.js';
|
|
51
|
+
import { buildModel, tickModel, modelGesture } from './model.js';
|
|
36
52
|
|
|
37
53
|
const DEG = Math.PI / 180;
|
|
38
54
|
const TAU = Math.PI * 2;
|
|
@@ -93,6 +109,26 @@ const nowMs = () => performance.now();
|
|
|
93
109
|
const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
94
110
|
const shared = (o) => { o.userData.shared = true; return o; };
|
|
95
111
|
|
|
112
|
+
// glb de los mods: uno por url, compartido entre ots (sus geometrías no se liberan con el rig)
|
|
113
|
+
const GLB = new Map();
|
|
114
|
+
function loadGlb(url) {
|
|
115
|
+
if (!GLB.has(url)) {
|
|
116
|
+
const p = new Promise((res, rej) => {
|
|
117
|
+
const loader = new GLTFLoader();
|
|
118
|
+
const done = (g) => res(g.scene);
|
|
119
|
+
if (url.startsWith('data:')) {
|
|
120
|
+
const bin = atob(url.slice(url.indexOf(',') + 1));
|
|
121
|
+
const buf = new Uint8Array(bin.length);
|
|
122
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
123
|
+
loader.parse(buf.buffer, '', done, rej);
|
|
124
|
+
} else loader.load(url, done, undefined, rej);
|
|
125
|
+
});
|
|
126
|
+
p.catch(() => GLB.delete(url));
|
|
127
|
+
GLB.set(url, p);
|
|
128
|
+
}
|
|
129
|
+
return GLB.get(url);
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
/** Malla con posición/rotación/escala opcionales. */
|
|
97
133
|
function M(geo, material, p, r, s) {
|
|
98
134
|
const m = new THREE.Mesh(geo, material);
|
|
@@ -143,7 +179,10 @@ export class Character3D {
|
|
|
143
179
|
this.clearMorphObjects();
|
|
144
180
|
for (const f of this.fxs) disposeFx(f.obj);
|
|
145
181
|
this.fxs = [];
|
|
146
|
-
const s = (this.spec = styled3D(normalizeCharacter(spec)));
|
|
182
|
+
const s = (this.spec = styled3D(applyMods(normalizeCharacter(spec), normalizeCharacter)));
|
|
183
|
+
this.modelMode = false;
|
|
184
|
+
this.modelTok = null;
|
|
185
|
+
if (s._model && s._model.url !== this.modelFailed) return this.buildModel(s); // mod de cuerpo completo (model.js)
|
|
147
186
|
const st = s._style; // estilo artístico: sin pelo, con su paleta y su contorno
|
|
148
187
|
const pts = bodyPoints(s);
|
|
149
188
|
const L = (this.L = layout(s));
|
|
@@ -155,7 +194,7 @@ export class Character3D {
|
|
|
155
194
|
const kk = (this.kk = Math.sqrt(W * H) / 1.25); // escala de accesorios (1 ≈ ot medio)
|
|
156
195
|
const Y = (this.Y = (y) => (B.bottom - y) * U);
|
|
157
196
|
const X = (this.X = (x) => (x - L.cx) * U);
|
|
158
|
-
const tex = (this.tex = textureOf(s));
|
|
197
|
+
const tex = (this.tex = as3d('textures', textureOf(s)));
|
|
159
198
|
const plush = (this.plush = !st && ((this.opts.material || 'plush') === 'plush' || ['fur', 'fuzz', 'feathers'].includes(tex)));
|
|
160
199
|
const furLen = (this.furLen = plush ? (tex === 'fuzz' || tex === 'none' ? 0.05 : 0.065) * kk : 0);
|
|
161
200
|
const density = (this.density = furLen ? (this.quality === 'high' ? 1 / (furLen * 0.2) : 1 / (furLen * 0.32)) : 0);
|
|
@@ -287,7 +326,7 @@ export class Character3D {
|
|
|
287
326
|
if (!o) continue;
|
|
288
327
|
// pivote en el centro del accesorio: escala/giro de la spec alrededor de sí mismo
|
|
289
328
|
o.updateMatrixWorld(true);
|
|
290
|
-
const c = o.userData.surface ? new THREE.Vector3() : new THREE.Box3().setFromObject(o).getCenter(new THREE.Vector3());
|
|
329
|
+
const c = o.userData.surface ? new THREE.Vector3() : o.userData.pivot ? o.userData.pivot.clone() : new THREE.Box3().setFromObject(o).getCenter(new THREE.Vector3());
|
|
291
330
|
const piv = G(o);
|
|
292
331
|
o.position.sub(c);
|
|
293
332
|
piv.position.copy(c);
|
|
@@ -651,7 +690,99 @@ export class Character3D {
|
|
|
651
690
|
h.rotation.z = -0.12;
|
|
652
691
|
return onTop(h, 0.06);
|
|
653
692
|
}
|
|
654
|
-
case '
|
|
693
|
+
case 'fedora': case 'bowler': {
|
|
694
|
+
const fm = mat('felt', col);
|
|
695
|
+
const fed = id === 'fedora';
|
|
696
|
+
const h = G(
|
|
697
|
+
// ala (fedora: algo más ancha y levantada atrás)
|
|
698
|
+
M(new THREE.CylinderGeometry((fed ? 0.4 : 0.32) * kk, (fed ? 0.4 : 0.32) * kk, 0.022 * kk, 48), fm, null, fed ? [-0.08, 0, 0] : null),
|
|
699
|
+
fed
|
|
700
|
+
? M(new THREE.CylinderGeometry(0.19 * kk, 0.23 * kk, 0.24 * kk, 40), fm, [0, 0.12 * kk, 0], null, [1, 1, 0.85])
|
|
701
|
+
: M(this.sph(0.23 * kk, 40, 20, 0, TAU, 0, Math.PI / 2), fm, [0, 0.06 * kk, 0], null, [1, 1.05, 0.92]),
|
|
702
|
+
M(new THREE.CylinderGeometry(0.235 * kk, 0.235 * kk, 0.05 * kk, 40), mat('felt', col2), [0, 0.045 * kk, 0], null, [1, 1, fed ? 0.88 : 0.93]),
|
|
703
|
+
);
|
|
704
|
+
if (!fed) h.add(M(new THREE.CylinderGeometry(0.23 * kk, 0.23 * kk, 0.06 * kk, 40), fm, [0, 0.03 * kk, 0], null, [1, 1, 0.92]));
|
|
705
|
+
if (fed) h.add(M(this.sph(0.12 * kk, 24, 10), mat('felt', shade(col, -0.25)), [0, 0.24 * kk, 0], null, [1, 0.25, 0.6])); // pellizco de la copa
|
|
706
|
+
h.rotation.z = fed ? -0.14 : -0.06;
|
|
707
|
+
return onTop(h, 0.06);
|
|
708
|
+
}
|
|
709
|
+
case 'witch': {
|
|
710
|
+
const fm = mat('felt', col);
|
|
711
|
+
const o = G(M(new THREE.CylinderGeometry(0.46 * kk, 0.46 * kk, 0.02 * kk, 48), fm));
|
|
712
|
+
// cono doblado: tres tramos cada vez más finos e inclinados
|
|
713
|
+
let y = 0;
|
|
714
|
+
let x = 0;
|
|
715
|
+
const segs = [[0.25, 0.17, 0.3, 0], [0.17, 0.09, 0.22, -0.35], [0.09, 0.01, 0.2, -0.9]];
|
|
716
|
+
for (const [r0, r1, h, rz] of segs) {
|
|
717
|
+
const m = M(new THREE.CylinderGeometry(r1 * kk, r0 * kk, h * kk, 32), fm);
|
|
718
|
+
m.geometry.translate(0, (h / 2) * kk, 0);
|
|
719
|
+
m.position.set(x, y, 0);
|
|
720
|
+
m.rotation.z = rz;
|
|
721
|
+
o.add(m);
|
|
722
|
+
x += -Math.sin(rz) * h * kk;
|
|
723
|
+
y += Math.cos(rz) * h * kk;
|
|
724
|
+
}
|
|
725
|
+
o.add(M(new THREE.CylinderGeometry(0.245 * kk, 0.25 * kk, 0.06 * kk, 40), mat('felt', col2), [0, 0.04 * kk, 0]));
|
|
726
|
+
o.add(M(new THREE.BoxGeometry(0.08 * kk, 0.06 * kk, 0.02 * kk), mat('metal', '#facc15'), [0, 0.04 * kk, 0.25 * kk]));
|
|
727
|
+
o.rotation.z = 0.1;
|
|
728
|
+
return onTop(o, 0.05);
|
|
729
|
+
}
|
|
730
|
+
case 'fez': {
|
|
731
|
+
const o = G(
|
|
732
|
+
M(new THREE.CylinderGeometry(0.16 * kk, 0.2 * kk, 0.3 * kk, 36), mat('felt', col), [0, 0.15 * kk, 0]),
|
|
733
|
+
M(new THREE.CylinderGeometry(0.006 * kk, 0.006 * kk, 0.14 * kk, 6), mat('felt', col2), [0.07 * kk, 0.305 * kk, 0.02 * kk], [0, 0, Math.PI / 2]),
|
|
734
|
+
);
|
|
735
|
+
const tas = G(
|
|
736
|
+
M(new THREE.CylinderGeometry(0.006 * kk, 0.006 * kk, 0.2 * kk, 6), mat('felt', col2), [0, -0.1 * kk, 0]),
|
|
737
|
+
M(new THREE.ConeGeometry(0.035 * kk, 0.09 * kk, 12), mat('felt', col2), [0, -0.2 * kk, 0]),
|
|
738
|
+
);
|
|
739
|
+
tas.position.set(0.14 * kk, 0.3 * kk, 0.02 * kk);
|
|
740
|
+
tas.rotation.z = 0.2;
|
|
741
|
+
o.add(tas);
|
|
742
|
+
this.anims.push((sec) => { tas.rotation.z = 0.2 + Math.sin(sec * 2.4) * 0.12; });
|
|
743
|
+
o.rotation.z = -0.1;
|
|
744
|
+
return onTop(o, 0.06);
|
|
745
|
+
}
|
|
746
|
+
case 'sombrero': {
|
|
747
|
+
const fm = mat('felt', col);
|
|
748
|
+
const o = G(
|
|
749
|
+
M(this.tor(0.5 * kk, 0.035 * kk, 10, 56), fm, [0, 0.03 * kk, 0], [Math.PI / 2, 0, 0]),
|
|
750
|
+
M(new THREE.CylinderGeometry(0.52 * kk, 0.52 * kk, 0.02 * kk, 56), fm),
|
|
751
|
+
M(new THREE.CylinderGeometry(0.13 * kk, 0.21 * kk, 0.26 * kk, 36), fm, [0, 0.13 * kk, 0]),
|
|
752
|
+
M(this.sph(0.13 * kk, 32, 12, 0, TAU, 0, Math.PI / 2), fm, [0, 0.26 * kk, 0], null, [1, 0.7, 1]),
|
|
753
|
+
M(new THREE.CylinderGeometry(0.205 * kk, 0.215 * kk, 0.05 * kk, 36), mat('felt', col2), [0, 0.04 * kk, 0]),
|
|
754
|
+
);
|
|
755
|
+
for (let i = 0; i < 12; i++) {
|
|
756
|
+
const a = (i / 12) * TAU;
|
|
757
|
+
o.add(M(this.sph(0.025 * kk, 10, 8), mat('felt', col2), [Math.cos(a) * 0.53 * kk, 0.0, Math.sin(a) * 0.53 * kk]));
|
|
758
|
+
}
|
|
759
|
+
return onTop(o, 0.05);
|
|
760
|
+
}
|
|
761
|
+
case 'bucket': {
|
|
762
|
+
const fm = mat('felt', col);
|
|
763
|
+
const o = G(
|
|
764
|
+
M(new THREE.CylinderGeometry(0.21 * kk, 0.26 * kk, 0.2 * kk, 40), fm, [0, 0.1 * kk, 0]),
|
|
765
|
+
M(this.sph(0.21 * kk, 40, 10, 0, TAU, 0, Math.PI / 2), fm, [0, 0.2 * kk, 0], null, [1, 0.25, 1]),
|
|
766
|
+
M(new THREE.CylinderGeometry(0.27 * kk, 0.37 * kk, 0.06 * kk, 48), mat('felt', shade(col, -0.08)), [0, -0.01 * kk, 0]),
|
|
767
|
+
);
|
|
768
|
+
return onTop(o, 0.08);
|
|
769
|
+
}
|
|
770
|
+
case 'jester': {
|
|
771
|
+
const o = G(fur(new THREE.TorusGeometry(0.24 * kk, 0.04 * kk, 12, 40).rotateX(Math.PI / 2), shade(col, -0.2), 0.6));
|
|
772
|
+
const cols = [col, col2, col];
|
|
773
|
+
[-1, 0, 1].forEach((s, i) => {
|
|
774
|
+
const len = s ? 0.42 : 0.5;
|
|
775
|
+
const horn = G(M(new THREE.ConeGeometry(0.12 * kk, len * kk, 24), mat('felt', cols[i]), [0, (len / 2) * kk, 0]));
|
|
776
|
+
horn.add(M(this.sph(0.04 * kk, 14, 10), mat('metal', '#facc15'), [0, len * kk, 0]));
|
|
777
|
+
horn.position.set(s * 0.12 * kk, 0.02 * kk, 0);
|
|
778
|
+
horn.rotation.z = -s * 0.95;
|
|
779
|
+
o.add(horn);
|
|
780
|
+
const ph = i * 1.7;
|
|
781
|
+
this.anims.push((sec) => { horn.rotation.z = -s * 0.95 + Math.sin(sec * 1.8 + ph) * 0.06; });
|
|
782
|
+
});
|
|
783
|
+
return onTop(o, 0.05);
|
|
784
|
+
}
|
|
785
|
+
case 'beanie': case 'santa': case 'cap': case 'hardhat': case 'viking': case 'beret': case 'propeller': {
|
|
655
786
|
const cp = capFor(id === 'beret' ? 0.14 : 0.24);
|
|
656
787
|
const dome = this.sph(1, 40, 20, 0, TAU, 0, Math.PI / 2).scale(cp.w * 1.08, top.y - cp.y + 0.05 * kk, cp.d * 1.1);
|
|
657
788
|
const o = G();
|
|
@@ -663,8 +794,19 @@ export class Character3D {
|
|
|
663
794
|
} else if (id === 'beret') {
|
|
664
795
|
o.add(M(dome, mat('felt', col), null, [0, 0, -0.25], [1.25, 0.45, 1.2]));
|
|
665
796
|
} else {
|
|
666
|
-
const m = id === 'viking' ? mat('metal', '#a3a3a3') : id === 'cap' ? mat('felt', col) : mat('plastic', col);
|
|
797
|
+
const m = id === 'viking' ? mat('metal', '#a3a3a3') : id === 'cap' || id === 'propeller' ? mat('felt', col) : mat('plastic', col);
|
|
667
798
|
o.add(M(dome, m));
|
|
799
|
+
if (id === 'propeller') {
|
|
800
|
+
// gorra de hélice: gajo de color, varilla y hélice que gira despacio
|
|
801
|
+
const hh = top.y - cp.y + 0.05 * kk;
|
|
802
|
+
o.add(M(this.sph(1, 24, 12, Math.PI / 2 - 0.35, 0.7, 0, Math.PI / 2).scale(cp.w * 1.085, hh * 1.005, cp.d * 1.105), mat('felt', col2)));
|
|
803
|
+
o.add(M(new THREE.CylinderGeometry(0.008 * kk, 0.008 * kk, 0.09 * kk, 8), mat('metal', '#9ca3af'), [0, hh + 0.04 * kk, 0]));
|
|
804
|
+
const prop = G(M(this.sph(0.02 * kk, 12, 8), mat('plastic', col2)));
|
|
805
|
+
for (const [sx, c2] of [[-1, '#ef4444'], [1, '#22c55e']]) prop.add(M(this.sph(1, 16, 8), mat('plastic', c2), [sx * 0.085 * kk, 0, 0], [sx * 0.3, 0, 0], [0.085 * kk, 0.008 * kk, 0.028 * kk]));
|
|
806
|
+
prop.position.y = hh + 0.085 * kk;
|
|
807
|
+
o.add(prop);
|
|
808
|
+
this.anims.push((sec) => { prop.rotation.y = sec * 5; });
|
|
809
|
+
}
|
|
668
810
|
if (id === 'cap') o.add(M(new THREE.CylinderGeometry(cp.w * 0.75, cp.w * 0.75, 0.02 * kk, 32, 1, false, -Math.PI / 2, Math.PI), mat('felt', col2 === '#ffffff' ? shade(col, -0.2) : col2), [0, 0.01, cp.d * 0.9], null, [1, 1, 0.9]));
|
|
669
811
|
if (id === 'hardhat') o.add(M(new THREE.CylinderGeometry(cp.w * 1.25, cp.w * 1.25, 0.02 * kk, 40), mat('plastic', col), [0, 0, 0.02]));
|
|
670
812
|
if (id === 'viking') for (const sx of [-1, 1]) o.add(M(new THREE.ConeGeometry(0.06 * kk, 0.3 * kk, 16), mat('plastic', '#fef3c7'), [sx * cp.w * 1.05, (top.y - cp.y) * 0.5, 0], [0, 0, -sx * 1.0]));
|
|
@@ -1044,16 +1186,77 @@ export class Character3D {
|
|
|
1044
1186
|
return g;
|
|
1045
1187
|
}
|
|
1046
1188
|
default:
|
|
1047
|
-
return null;
|
|
1189
|
+
return this.spec._modParts?.[id] ? this.buildModPart(this.spec._modParts[id], a, c) : null;
|
|
1048
1190
|
}
|
|
1049
1191
|
}
|
|
1050
1192
|
|
|
1193
|
+
/**
|
|
1194
|
+
* Pieza de un mod: su glb (unidades del mod: 1 = 1 px de un ot de 120 de ancho, y hacia
|
|
1195
|
+
* arriba) o, si no trae, su SVG en un plano. Se carga en segundo plano; el pivote es el ancla.
|
|
1196
|
+
*/
|
|
1197
|
+
buildModPart(p, a, c) {
|
|
1198
|
+
const { L, X, Y, probe } = c;
|
|
1199
|
+
const at = L.anchors[p.anchor] || L.anchors.center;
|
|
1200
|
+
const sc = (L.box.w / 120) * U;
|
|
1201
|
+
const x0 = X(at.x);
|
|
1202
|
+
const y0 = Y(at.y);
|
|
1203
|
+
const z0 = p.depth === 'center' ? 0 : (probe.front(x0, y0)?.p.z ?? 0) + this.furLen;
|
|
1204
|
+
const g = G();
|
|
1205
|
+
g.position.set(x0, y0, z0);
|
|
1206
|
+
g.userData.pivot = g.position.clone();
|
|
1207
|
+
const plane = () => this.modPlane(p, a, sc);
|
|
1208
|
+
if (!p.glb) return g.add(plane()), g;
|
|
1209
|
+
const holder = G();
|
|
1210
|
+
holder.scale.setScalar(sc);
|
|
1211
|
+
g.add(holder);
|
|
1212
|
+
const tint = p.tint === 'body' ? this.spec.body.color : p.tint === 'color' ? a.color : '';
|
|
1213
|
+
loadGlb(p.glb).then((scene) => {
|
|
1214
|
+
const o = scene.clone(true);
|
|
1215
|
+
o.traverse((m) => {
|
|
1216
|
+
if (!m.isMesh) return;
|
|
1217
|
+
m.geometry.userData.shared = true;
|
|
1218
|
+
if (tint) {
|
|
1219
|
+
m.material = m.material.clone();
|
|
1220
|
+
m.material.color?.set(tint);
|
|
1221
|
+
m.userData.ownMaterial = true;
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
holder.add(o);
|
|
1225
|
+
}).catch(() => g.add(plane()));
|
|
1226
|
+
return g;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** SVG de una pieza de mod pintado en un plano (respaldo 3D de los mods sin glb). */
|
|
1230
|
+
modPlane(p, a, sc) {
|
|
1231
|
+
const [vx, vy, vw, vh] = p.view;
|
|
1232
|
+
const def = modPart(this.spec, a.id, shade);
|
|
1233
|
+
const inner = def ? def.draw({ k: 1, color: a.color, color2: a.color2, body: this.spec.body.color }) : '';
|
|
1234
|
+
const px = 256 / Math.max(vw, vh);
|
|
1235
|
+
const cv = document.createElement('canvas');
|
|
1236
|
+
cv.width = Math.max(2, Math.round(vw * px));
|
|
1237
|
+
cv.height = Math.max(2, Math.round(vh * px));
|
|
1238
|
+
const tex = new THREE.CanvasTexture(cv);
|
|
1239
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
1240
|
+
const img = new Image();
|
|
1241
|
+
img.onload = () => {
|
|
1242
|
+
cv.getContext('2d').drawImage(img, 0, 0, cv.width, cv.height);
|
|
1243
|
+
tex.needsUpdate = true;
|
|
1244
|
+
};
|
|
1245
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vx} ${vy} ${vw} ${vh}" width="${cv.width}" height="${cv.height}">${inner}</svg>`)}`;
|
|
1246
|
+
const m = new THREE.Mesh(new THREE.PlaneGeometry(vw * sc, vh * sc), new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false, side: THREE.DoubleSide }));
|
|
1247
|
+
m.position.set((vx + vw / 2) * sc, -(vy + vh / 2) * sc, p.layer === 'back' ? -0.01 : 0.01);
|
|
1248
|
+
m.userData.ownMaterial = true;
|
|
1249
|
+
return m;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1051
1252
|
disposeRig() {
|
|
1052
1253
|
this.rig.traverse((m) => {
|
|
1053
1254
|
if (!m.isMesh) return;
|
|
1054
1255
|
if (!m.geometry.userData.shared) m.geometry.dispose();
|
|
1055
1256
|
if (m.userData.ownMaterial) m.material.dispose();
|
|
1056
1257
|
});
|
|
1258
|
+
this.model?.mixer?.stopAllAction();
|
|
1259
|
+
this.model = null;
|
|
1057
1260
|
this.rig.clear();
|
|
1058
1261
|
if (this.shadow) {
|
|
1059
1262
|
this.shadow.material.dispose();
|
|
@@ -1073,6 +1276,7 @@ export class Character3D {
|
|
|
1073
1276
|
|
|
1074
1277
|
/** Avanza un frame. t: performance.now() ms; dt: ms. */
|
|
1075
1278
|
tick(t, dt) {
|
|
1279
|
+
if (this.modelMode) return this.tickModel(t, dt);
|
|
1076
1280
|
const rm = reducedMotion();
|
|
1077
1281
|
const idle = this.opts.idle !== false;
|
|
1078
1282
|
// parpadeo y miradas espontáneas
|
|
@@ -1125,26 +1329,7 @@ export class Character3D {
|
|
|
1125
1329
|
}
|
|
1126
1330
|
if (rm) Object.assign(pose, { x: 0, y: 0, r: 0, sx: 1, sy: 1 });
|
|
1127
1331
|
|
|
1128
|
-
|
|
1129
|
-
let ksx = 1;
|
|
1130
|
-
let ksy = 1;
|
|
1131
|
-
const act = {};
|
|
1132
|
-
for (const [slot, mm] of this.morphs) {
|
|
1133
|
-
if (mm.phase !== 'out' && t >= mm.until) { mm.phase = 'out'; mm.outAt = t; mm.k0 = mm.k; }
|
|
1134
|
-
if (mm.phase === 'in') {
|
|
1135
|
-
const q = clamp((t - mm.start) / MORPH_IN);
|
|
1136
|
-
mm.k = rm ? 1 : mm.shape ? EASE.inOut(q) : EASE.backOut(q);
|
|
1137
|
-
if (q >= 1) mm.phase = 'on';
|
|
1138
|
-
} else if (mm.phase === 'on') mm.k = 1;
|
|
1139
|
-
else {
|
|
1140
|
-
const q = clamp((t - mm.outAt) / MORPH_OUT);
|
|
1141
|
-
mm.k = rm ? 0 : mm.k0 * (1 - EASE.inOut(q));
|
|
1142
|
-
if (q >= 1) { this.morphs.delete(slot); this.endMorph(slot); continue; }
|
|
1143
|
-
}
|
|
1144
|
-
act[slot] = mm;
|
|
1145
|
-
const sc = SCALE_MORPH[slot];
|
|
1146
|
-
if (sc) { const [a, b] = sc(mm.k); ksx *= a; ksy *= b; }
|
|
1147
|
-
}
|
|
1332
|
+
const { ksx, ksy, act } = this.tickMorphs(t, rm);
|
|
1148
1333
|
if (act.shape && !act.shape.ready) this.startShape(act.shape);
|
|
1149
1334
|
const shapeK = act.shape?.ready ? act.shape.k : 0;
|
|
1150
1335
|
this.bodyU.uMorph.value.set(act.melt?.k || 0, rm ? 0 : act.jelly?.k || 0, shapeK, 0);
|
|
@@ -1282,7 +1467,42 @@ export class Character3D {
|
|
|
1282
1467
|
hnd.scale.setScalar(Math.max(0.01, mhd.k * (p.s || 1)) * (type === 'open' ? 1 : 0.9));
|
|
1283
1468
|
}
|
|
1284
1469
|
|
|
1285
|
-
|
|
1470
|
+
const blush = this.tickEffects(t, rm);
|
|
1471
|
+
this.bodyU.uBlush.value = blush;
|
|
1472
|
+
this.fxg.position.copy(rig.position);
|
|
1473
|
+
this.fxg.rotation.y = this.yaw;
|
|
1474
|
+
|
|
1475
|
+
for (const f of this.furs) f.u.uTime.value = sec;
|
|
1476
|
+
for (const a of this.anims) a(rm ? 0 : sec);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** Pinchos: conos instanciados en el contorno (sin la base) y en la espalda. */
|
|
1480
|
+
/** Fases de las metamorfosis como en 2D (entra con rebote, sale suave): { ksx, ksy, act }. */
|
|
1481
|
+
tickMorphs(t, rm) {
|
|
1482
|
+
let ksx = 1;
|
|
1483
|
+
let ksy = 1;
|
|
1484
|
+
const act = {};
|
|
1485
|
+
for (const [slot, mm] of this.morphs) {
|
|
1486
|
+
if (mm.phase !== 'out' && t >= mm.until) { mm.phase = 'out'; mm.outAt = t; mm.k0 = mm.k; }
|
|
1487
|
+
if (mm.phase === 'in') {
|
|
1488
|
+
const q = clamp((t - mm.start) / MORPH_IN);
|
|
1489
|
+
mm.k = rm ? 1 : mm.shape ? EASE.inOut(q) : EASE.backOut(q);
|
|
1490
|
+
if (q >= 1) mm.phase = 'on';
|
|
1491
|
+
} else if (mm.phase === 'on') mm.k = 1;
|
|
1492
|
+
else {
|
|
1493
|
+
const q = clamp((t - mm.outAt) / MORPH_OUT);
|
|
1494
|
+
mm.k = rm ? 0 : mm.k0 * (1 - EASE.inOut(q));
|
|
1495
|
+
if (q >= 1) { this.morphs.delete(slot); this.endMorph(slot); continue; }
|
|
1496
|
+
}
|
|
1497
|
+
act[slot] = mm;
|
|
1498
|
+
const sc = SCALE_MORPH[slot];
|
|
1499
|
+
if (sc) { const [a, b] = sc(mm.k); ksx *= a; ksy *= b; }
|
|
1500
|
+
}
|
|
1501
|
+
return { ksx, ksy, act };
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** Avanza los efectos; devuelve el rubor que piden. */
|
|
1505
|
+
tickEffects(t, rm) {
|
|
1286
1506
|
let blush = 0;
|
|
1287
1507
|
if (this.fxs.length) {
|
|
1288
1508
|
const c = this.fxc;
|
|
@@ -1306,15 +1526,9 @@ export class Character3D {
|
|
|
1306
1526
|
return true;
|
|
1307
1527
|
});
|
|
1308
1528
|
}
|
|
1309
|
-
|
|
1310
|
-
this.fxg.position.copy(rig.position);
|
|
1311
|
-
this.fxg.rotation.y = this.yaw;
|
|
1312
|
-
|
|
1313
|
-
for (const f of this.furs) f.u.uTime.value = sec;
|
|
1314
|
-
for (const a of this.anims) a(rm ? 0 : sec);
|
|
1529
|
+
return blush;
|
|
1315
1530
|
}
|
|
1316
1531
|
|
|
1317
|
-
/** Pinchos: conos instanciados en el contorno (sin la base) y en la espalda. */
|
|
1318
1532
|
makeSpikes(color) {
|
|
1319
1533
|
const L = this.L;
|
|
1320
1534
|
const list = [];
|
|
@@ -1412,7 +1626,7 @@ export class Character3D {
|
|
|
1412
1626
|
|
|
1413
1627
|
/** Limpia lo que deja una metamorfosis al terminar. */
|
|
1414
1628
|
endMorph(slot) {
|
|
1415
|
-
if (slot === 'shape') {
|
|
1629
|
+
if (slot === 'shape' && this.B) {
|
|
1416
1630
|
for (const b of this.bodyMeshes) b.geometry = this.B.geometry;
|
|
1417
1631
|
this.shapeGeo?.dispose();
|
|
1418
1632
|
this.shapeGeo = null;
|
|
@@ -1691,3 +1905,5 @@ export class Character3D {
|
|
|
1691
1905
|
this.root.removeFromParent();
|
|
1692
1906
|
}
|
|
1693
1907
|
}
|
|
1908
|
+
|
|
1909
|
+
Object.assign(Character3D.prototype, { buildModel, tickModel, modelGesture });
|
package/src/character3d/index.js
CHANGED
|
@@ -93,6 +93,7 @@ export function createCharacter3D(el, spec, opts = {}) {
|
|
|
93
93
|
const ch = new Character3D(spec, { quality: opts.quality || 'high', material: opts.material || 'plush', moodFx: opts.moodFx });
|
|
94
94
|
scene.add(ch.root);
|
|
95
95
|
let aspect = 0;
|
|
96
|
+
let rev = ch.boundsRev;
|
|
96
97
|
const frame = () => {
|
|
97
98
|
const b = ch.bounds;
|
|
98
99
|
const size = b.getSize(new THREE.Vector3());
|
|
@@ -114,8 +115,9 @@ export function createCharacter3D(el, spec, opts = {}) {
|
|
|
114
115
|
bleed: opts.bleed || 0,
|
|
115
116
|
tick(t, dt) {
|
|
116
117
|
const a = this.rect.width / this.rect.height;
|
|
117
|
-
if (Math.abs(a - aspect) > 1e-3) {
|
|
118
|
+
if (Math.abs(a - aspect) > 1e-3 || rev !== ch.boundsRev) {
|
|
118
119
|
aspect = a;
|
|
120
|
+
rev = ch.boundsRev; // un mod de cuerpo completo cambia la caja al cargar su modelo
|
|
119
121
|
camera.aspect = a;
|
|
120
122
|
frame();
|
|
121
123
|
}
|
|
@@ -239,6 +241,10 @@ export function createCrowd3D(el, opts = {}) {
|
|
|
239
241
|
camera.aspect = a;
|
|
240
242
|
fit();
|
|
241
243
|
}
|
|
244
|
+
if (chars.some((c) => c.boundsRev !== c.seenRev)) {
|
|
245
|
+
for (const c of chars) c.seenRev = c.boundsRev;
|
|
246
|
+
fit();
|
|
247
|
+
}
|
|
242
248
|
for (const c of chars) {
|
|
243
249
|
c.tick(t, dt);
|
|
244
250
|
c.lod(pixelHeight(this, c));
|