Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/loot-assets
Submodule loot-assets added at 24e2d4
51 changes: 51 additions & 0 deletions src/components/BoneSelector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.bone-selector {
position: absolute;
display: flex;
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
height: 100vh; /* Full viewport height */
width: 100vw; /* Full viewport height */
pointer-events: none;
top:-30px;
}

.character-base {
/* position: relative;
display: inline-block;
width: 300px;
height: 600px;
display: flex;
justify-content: center;
text-align: center; */
user-select: none;

height: 70%;
position: relative;
display: inline-block;
}

.character-base img {
height: 100%;
/* width: 100%; */
}

.bone-dot {
pointer-events: auto;
user-select: none;
position: relative;
width: 0px;
height: 0px;
background-size: cover;

cursor: pointer;
}

.bone-dot img {
width: 30px;
height: 30px;
transform: translate(-50%, -50%); /* Center the dot */
}

.bone-dot-position {
position: absolute;
}
83 changes: 83 additions & 0 deletions src/components/BoneSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useState } from 'react';
import './BoneSelector.css'; // Import CSS for styling
import boneDot from "../images/humanoid_option.png"
import humanoidUI from "../images/humanoid_ui.png"

// Example CharacterBase component
const CharacterBase = ({}) => {
return (
<div className="character-base">
<img src={humanoidUI}/>
</div>
);
};

// Example BoneDot component
const BoneDot = ({ position, onSelect }) => {
return (
<div
className="bone-dot"
style={{ left: `${position.x}px`, top: `${-position.y}px` }}
onClick={() => onSelect(position)}>
<img
src={boneDot}
/>
</div>
);
};

// Example BoneSelector component
export const BoneSelector = ({onSelect}) => {
const [bonePositions, setBonePositions] = useState([
{ x: 0, y: 10, name:"hips" },

{ x: 0, y: 60, name:"spine" },
{ x: 0, y: 120, name:"chest" },
{ x: 0, y: 180, name:"upperChest" },
{ x: 0, y: 220, name:"neck" },
{ x: 0, y: 260, name:"head" },

{ x: -40, y: 180, name:"leftShoulder" },
{ x: 40, y: 180, name:"rightShoulder" },

{ x: -70, y: 160, name:"leftUpperArm" },
{ x: 70, y: 160, name:"rightUpperArm" },

{ x: -80, y: 80, name:"leftLowerArm" },
{ x: 80, y: 80, name:"rightLowerArm" },

{ x: -80, y: -20, name:"leftHand" },
{ x: 80, y: -20, name:"rightHand" },

{ x: -30, y: -20, name:"leftUpperLeg" },
{ x: 30, y: -20, name:"rightUpperLeg" },

{ x: -22, y: -120, name:"leftLowerLeg" },
{ x: 20, y: -120, name:"rightLowerLeg" },

{ x: -17, y: -260, name:"leftFoot" },
{ x: 17, y: -260, name:"rightFoot" },
// Add more positions as needed
]);

const handleSelect = (position) => {
console.log('Bone selected at:', position);
onSelect(position.name);
// Additional logic for selecting a bone can be added here
};

return (
<div className="bone-selector">
<CharacterBase />
<div className="bone-dot-position">
{bonePositions.map((pos, index) => (
<BoneDot
key={index}
position={pos}
onSelect={handleSelect}
/>
))}
</div>
</div>
);
};
121 changes: 121 additions & 0 deletions src/components/TransformInspector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react'
import styles from './TransformInspector.module.css'
import { SceneContext } from '../context/SceneContext'

export default function TransformInspector(){
const {
transformTarget,
applyTranslateDelta,
applyRotateDelta,
applyScaleDelta,
getBoneNames,
reparentToBone,
getAttachedBoneName,
copyTransform,
pasteTransform,
resetToBoneOrigin,
} = React.useContext(SceneContext)

const [bone, setBone] = React.useState('')
const bones = getBoneNames()
const [clipboard, setClipboard] = React.useState(null)
const deg = 180/Math.PI
const [pos, setPos] = React.useState({x:0,y:0,z:0})
const [rot, setRot] = React.useState({x:0,y:0,z:0}) // degrees
const [scl, setScl] = React.useState({x:1,y:1,z:1})
const [u, setU] = React.useState(1)

React.useEffect(()=>{
if (bones && bones.length && !bone) setBone(bones.includes('head')?'head':bones[0])
}, [bones])

React.useEffect(()=>{
if (!transformTarget) return
const p = transformTarget.position
const r = transformTarget.rotation
const s = transformTarget.scale
setPos({x:p.x,y:p.y,z:p.z})
setRot({x:r.x*deg,y:r.y*deg,z:r.z*deg})
setScl({x:s.x,y:s.y,z:s.z})
setU((s.x + s.y + s.z)/3)
}, [transformTarget])

if (!transformTarget) return null

return (
<div className={styles.panel}>
<div className={styles.header}>Trait Position</div>
<div className={styles.row}>
<label>X position</label>
<input type="range" min={-0.5} max={0.5} step={0.001} value={pos.x} onChange={(e)=>{ const v = Number(e.target.value); applyTranslateDelta(v-pos.x,0,0); setPos(prev=>({...prev,x:v})) }} />
<input className={styles.num} type="number" step={0.001} value={pos.x} onChange={(e)=>{ const v=Number(e.target.value); applyTranslateDelta(v-pos.x,0,0); setPos(prev=>({...prev,x:v})) }} />
</div>
<div className={styles.row}>
<label>Y position</label>
<input type="range" min={-0.5} max={0.5} step={0.001} value={pos.y} onChange={(e)=>{ const v = Number(e.target.value); applyTranslateDelta(0,v-pos.y,0); setPos(prev=>({...prev,y:v})) }} />
<input className={styles.num} type="number" step={0.001} value={pos.y} onChange={(e)=>{ const v=Number(e.target.value); applyTranslateDelta(0,v-pos.y,0); setPos(prev=>({...prev,y:v})) }} />
</div>
<div className={styles.row}>
<label>Z position</label>
<input type="range" min={-0.5} max={0.5} step={0.001} value={pos.z} onChange={(e)=>{ const v = Number(e.target.value); applyTranslateDelta(0,0,v-pos.z); setPos(prev=>({...prev,z:v})) }} />
<input className={styles.num} type="number" step={0.001} value={pos.z} onChange={(e)=>{ const v=Number(e.target.value); applyTranslateDelta(0,0,v-pos.z); setPos(prev=>({...prev,z:v})) }} />
</div>

<div className={styles.header}>Trait Rotation</div>
<div className={styles.row}>
<label>Pitch (X)</label>
<input type="range" min={-90} max={90} step={0.5} value={rot.x} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(v-rot.x,0,0); setRot(prev=>({...prev,x:v})) }} />
<input className={styles.num} type="number" step={0.5} value={rot.x} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(v-rot.x,0,0); setRot(prev=>({...prev,x:v})) }} />
</div>
<div className={styles.row}>
<label>Yaw (Y)</label>
<input type="range" min={-180} max={180} step={0.5} value={rot.y} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(0,v-rot.y,0); setRot(prev=>({...prev,y:v})) }} />
<input className={styles.num} type="number" step={0.5} value={rot.y} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(0,v-rot.y,0); setRot(prev=>({...prev,y:v})) }} />
</div>
<div className={styles.row}>
<label>Roll (Z)</label>
<input type="range" min={-90} max={90} step={0.5} value={rot.z} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(0,0,v-rot.z); setRot(prev=>({...prev,z:v})) }} />
<input className={styles.num} type="number" step={0.5} value={rot.z} onChange={(e)=>{ const v=Number(e.target.value); applyRotateDelta(0,0,v-rot.z); setRot(prev=>(({...prev,z:v})) ) }} />
</div>

<div className={styles.header}>Trait Scale</div>
<div className={styles.row}>
<label>Uniform</label>
<input type="range" min={0.1} max={3} step={0.01} value={u} onChange={(e)=>{ const v=Number(e.target.value); const dx=v-scl.x; const dy=v-scl.y; const dz=v-scl.z; applyScaleDelta(dx,dy,dz); setScl({x:v,y:v,z:v}); setU(v); }} />
<input className={styles.num} type="number" step={0.01} value={u} onChange={(e)=>{ const v=Number(e.target.value); const dx=v-scl.x; const dy=v-scl.y; const dz=v-scl.z; applyScaleDelta(dx,dy,dz); setScl({x:v,y:v,z:v}); setU(v); }} />
</div>
<div className={styles.row}>
<label>X Scale</label>
<input type="range" min={0.1} max={3} step={0.01} value={scl.x} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(v-scl.x,0,0); setScl(prev=>({...prev,x:v})); setU((v+scl.y+scl.z)/3); }} />
<input className={styles.num} type="number" step={0.01} value={scl.x} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(v-scl.x,0,0); setScl(prev=>({...prev,x:v})); setU((v+scl.y+scl.z)/3); }} />
</div>
<div className={styles.row}>
<label>Y Scale</label>
<input type="range" min={0.1} max={3} step={0.01} value={scl.y} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(0,v-scl.y,0); setScl(prev=>({...prev,y:v})); setU((scl.x+v+scl.z)/3); }} />
<input className={styles.num} type="number" step={0.01} value={scl.y} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(0,v-scl.y,0); setScl(prev=>({...prev,y:v})); setU((scl.x+v+scl.z)/3); }} />
</div>
<div className={styles.row}>
<label>Z Scale</label>
<input type="range" min={0.1} max={3} step={0.01} value={scl.z} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(0,0,v-scl.z); setScl(prev=>({...prev,z:v})); setU((scl.x+scl.y+v)/3); }} />
<input className={styles.num} type="number" step={0.01} value={scl.z} onChange={(e)=>{ const v=Number(e.target.value); applyScaleDelta(0,0,v-scl.z); setScl(prev=>({...prev,z:v})); setU((scl.x+scl.y+v)/3); }} />
</div>

<div className={styles.header}>Attach To Bone</div>
<div className={styles.row}>
<select className={styles.select} value={bone || getAttachedBoneName() || ''} onChange={(e)=>setBone(e.target.value)}>
{bones.map((b)=>(<option key={b} value={b}>{b}</option>))}
</select>
<button className={styles.btn} onClick={()=>reparentToBone(bone)}>Reattach</button>
</div>

<div className={styles.header}>Utilities</div>
<div className={styles.row}>
<button className={styles.btn} onClick={()=>{ setClipboard(copyTransform()) }}>Copy</button>
<button className={styles.btn} onClick={()=>pasteTransform(clipboard)} disabled={!clipboard}>Paste</button>
<button className={styles.btn} onClick={resetToBoneOrigin}>Reset</button>
</div>
</div>
)
}


42 changes: 42 additions & 0 deletions src/components/TransformInspector.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.panel{
position: absolute;
top: 80px;
right: 24px;
width: 260px;
padding: 12px;
border-radius: 12px;
background: rgba(5,11,14,0.8);
color: #fff;
backdrop-filter: blur(22.5px);
z-index: 1001;
}
.header{
font-weight: 800;
letter-spacing: 1px;
margin: 8px 0;
}
.row{
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
label{ width: 96px; font-size: 12px; color: #ddd; }
.select{ flex:1; }
.num{
width: 48px;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #fff;
}
.btn{
padding: 6px 10px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: #fff;
border-radius: 8px;
}


94 changes: 94 additions & 0 deletions src/components/TransformToolbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react'
import styles from './TransformToolbar.module.css'
import { SceneContext } from '../context/SceneContext'

export default function TransformToolbar(){
const {
transformMode,
setTransformMode,
transformSnap,
setTransformSnap,
transformTarget,
detachTransformTarget,
applyTranslateDelta,
applyRotateDelta,
applyScaleDelta,
} = React.useContext(SceneContext)

const [plane, setPlane] = React.useState('XY') // for translate only

const onSnapChange = (key) => (e) => {
const v = Number(e.target.value)
setTransformSnap({ ...transformSnap, [key]: isNaN(v) ? 0 : v })
}

if (!transformTarget) return null

const stepValue = transformMode === 'translate' ? transformSnap.t : transformMode === 'rotate' ? transformSnap.r : transformSnap.s
const setStepValue = (v) => {
const num = Number(v)
if (isNaN(num)) return
if (transformMode === 'translate') setTransformSnap({ ...transformSnap, t: num })
else if (transformMode === 'rotate') setTransformSnap({ ...transformSnap, r: num })
else setTransformSnap({ ...transformSnap, s: num })
}

const onArrow = (dir) => () => {
if (transformMode === 'translate'){
const s = transformSnap.t
if (plane === 'XY'){
if (dir==='up') return applyTranslateDelta(0, s, 0)
if (dir==='down') return applyTranslateDelta(0, -s, 0)
} else {
if (dir==='up') return applyTranslateDelta(0, 0, s)
if (dir==='down') return applyTranslateDelta(0, 0, -s)
}
if (dir==='left') return applyTranslateDelta(-s, 0, 0)
if (dir==='right') return applyTranslateDelta(s, 0, 0)
}
if (transformMode === 'rotate'){
const r = transformSnap.r
if (dir==='up') return applyRotateDelta(r, 0, 0)
if (dir==='down') return applyRotateDelta(-r, 0, 0)
if (dir==='left') return applyRotateDelta(0, -r, 0)
if (dir==='right') return applyRotateDelta(0, r, 0)
}
if (transformMode === 'scale'){
const s = transformSnap.s
const d = (dir==='up' || dir==='right') ? s : -s
return applyScaleDelta(d, d, d)
}
}

return (
<div className={styles.toolbar}>
<div className={styles.seg}>
<button className={`${styles.segBtn} ${transformMode==='translate'?styles.active:''}`} onClick={()=>setTransformMode('translate')}>T</button>
<button className={`${styles.segBtn} ${transformMode==='rotate'?styles.active:''}`} onClick={()=>setTransformMode('rotate')}>R</button>
<button className={`${styles.segBtn} ${transformMode==='scale'?styles.active:''}`} onClick={()=>setTransformMode('scale')}>S</button>
</div>
{transformMode==='translate' && (
<div className={styles.seg}>
<button className={`${styles.segBtn} ${plane==='XY'?styles.active:''}`} onClick={()=>setPlane('XY')}>XY</button>
<button className={`${styles.segBtn} ${plane==='XZ'?styles.active:''}`} onClick={()=>setPlane('XZ')}>XZ</button>
</div>
)}
<div className={styles.dpad}>
<button className={styles.arrow} aria-label="up" onClick={onArrow('up')}>↑</button>
<div className={styles.row}>
<button className={styles.arrow} aria-label="left" onClick={onArrow('left')}>←</button>
<button className={styles.center} aria-label="center" onClick={()=>{}}></button>
<button className={styles.arrow} aria-label="right" onClick={onArrow('right')}>→</button>
</div>
<button className={styles.arrow} aria-label="down" onClick={onArrow('down')}>↓</button>
</div>
<div className={styles.compact}>
<label className={styles.label}>Step</label>
<input className={styles.inputSmall} type="number" step={transformMode==='rotate'?1:0.01} value={stepValue} onChange={(e)=>setStepValue(e.target.value)} />
<button className={styles.btn} onClick={detachTransformTarget}>Done</button>
</div>
</div>
)
}


Loading
Loading