mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Add axis-angle rotation mode
This commit is contained in:
@@ -3,6 +3,7 @@ export const Octicon = {
|
||||
archive: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 2.5a.25.25 0 00-.25.25v1.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-1.5a.25.25 0 00-.25-.25H1.75zM0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0114.25 6H1.75A1.75 1.75 0 010 4.25v-1.5zM1.75 7a.75.75 0 01.75.75v5.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-5.5a.75.75 0 111.5 0v5.5A1.75 1.75 0 0113.25 15H2.75A1.75 1.75 0 011 13.25v-5.5A.75.75 0 011.75 7zm4.5 1a.75.75 0 000 1.5h3.5a.75.75 0 100-1.5h-3.5z"></path></svg>,
|
||||
arrow_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>,
|
||||
arrow_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>,
|
||||
arrow_switch: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M5.22 14.78a.75.75 0 0 0 1.06-1.06L4.56 12h8.69a.75.75 0 0 0 0-1.5H4.56l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3a.75.75 0 0 0 0 1.06l3 3Zm5.56-6.5a.75.75 0 1 1-1.06-1.06l1.72-1.72H2.75a.75.75 0 0 1 0-1.5h8.69L9.72 2.28a.75.75 0 0 1 1.06-1.06l3 3a.75.75 0 0 1 0 1.06l-3 3Z"></path></svg>,
|
||||
check: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_down: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"></path></svg>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Matrix3, Matrix4, Mesh, Quad, Renderer, ShaderProgram, Vector, Vertex } from 'deepslate'
|
||||
import { mat4, quat } from 'gl-matrix'
|
||||
import { mat4, quat, vec3 } from 'gl-matrix'
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { Footer, NumberInput, Octicon, RangeInput } from '../components/index.js'
|
||||
import { InteractiveCanvas3D } from '../components/previews/InteractiveCanvas3D.jsx'
|
||||
@@ -12,6 +12,11 @@ import { composeMatrix, svdDecompose } from '../Utils.js'
|
||||
const XYZ = ['x', 'y', 'z'] as const
|
||||
type XYZ = typeof XYZ[number]
|
||||
|
||||
const XYZW = ['x', 'y', 'z', 'w'] as const
|
||||
|
||||
const RotationModes = ['quaternion', 'axis_angle'] as const
|
||||
type RotationMode = typeof RotationModes[number]
|
||||
|
||||
interface Props {
|
||||
path?: string,
|
||||
}
|
||||
@@ -104,8 +109,41 @@ export function Transformation({}: Props) {
|
||||
const copy = quat.clone(rightRotation)
|
||||
copy[i] = value
|
||||
if (normalizeRight) quat.normalize(copy, copy)
|
||||
setRightRotation(copy)
|
||||
setMatrix(composeMatrix(translation, leftRotation, scale, rightRotation))
|
||||
updateRightRotation(copy)
|
||||
}, [rightRotation, normalizeRight, updateRightRotation])
|
||||
|
||||
const [rotationMode, setRotationMode] = useState<RotationMode>('quaternion')
|
||||
|
||||
const leftRotationAxisAngle = useMemo(() => {
|
||||
const axis = vec3.create()
|
||||
const angle = quat.getAxisAngle(axis, leftRotation)
|
||||
return { axis, angle }
|
||||
}, [leftRotation])
|
||||
|
||||
const changeLeftRotationAxisAngle = useCallback((i: number, value: number) => {
|
||||
const axisCopy = vec3.clone(leftRotationAxisAngle.axis)
|
||||
if (i < 3) axisCopy[i] = value
|
||||
else leftRotationAxisAngle.angle = value
|
||||
if (normalizeLeft) vec3.normalize(axisCopy, axisCopy)
|
||||
const copy = quat.setAxisAngle(quat.create(), axisCopy, leftRotationAxisAngle.angle)
|
||||
if (normalizeLeft) quat.normalize(copy, copy)
|
||||
updateLeftRotation(copy)
|
||||
}, [leftRotation, normalizeLeft, updateLeftRotation])
|
||||
|
||||
const rightRotationAxisAngle = useMemo(() => {
|
||||
const axis = vec3.create()
|
||||
const angle = quat.getAxisAngle(axis, rightRotation)
|
||||
return { axis, angle }
|
||||
}, [rightRotation])
|
||||
|
||||
const changeRightRotationAxisAngle = useCallback((i: number, value: number) => {
|
||||
const axisCopy = vec3.clone(rightRotationAxisAngle.axis)
|
||||
if (i < 3) axisCopy[i] = value
|
||||
else rightRotationAxisAngle.angle = value
|
||||
if (normalizeRight) vec3.normalize(axisCopy, axisCopy)
|
||||
const copy = quat.setAxisAngle(quat.create(), axisCopy, rightRotationAxisAngle.angle)
|
||||
if (normalizeRight) quat.normalize(copy, copy)
|
||||
updateRightRotation(copy)
|
||||
}, [rightRotation, normalizeRight, updateRightRotation])
|
||||
|
||||
const renderer = useRef<MeshRenderer>()
|
||||
@@ -143,42 +181,50 @@ export function Transformation({}: Props) {
|
||||
<button class="tooltipped tip-se" aria-label={locale('reset')} onClick={() => updateTranslation(new Vector(0, 0, 0))}>{Octicon['history']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={locale('transformation.copy_decomposed')} onClick={onCopyDecomposed}>{Octicon[copiedDecomposed ? 'check' : 'clippy']}</button>
|
||||
</div>
|
||||
{XYZ.map((c) => <div class="transformation-input">
|
||||
<NumberInput value={translation[c].toFixed(3)} onChange={v => changeTranslation(c, v)} />
|
||||
<RangeInput min={-1} max={1} step={0.01} value={translation[c]} onChange={v => changeTranslation(c, v)} />
|
||||
</div>)}
|
||||
{XYZ.map((c) =>
|
||||
<Slider label={c} value={translation[c]} onChange={v => changeTranslation(c, v)} />
|
||||
)}
|
||||
</div>
|
||||
<div class="transformation-section">
|
||||
<div class="transformation-title">
|
||||
<span>{locale('transformation.left_rotation')}</span>
|
||||
<button class="tooltipped tip-se" aria-label={locale('reset')} onClick={() => updateLeftRotation(quat.create())}>{Octicon['history']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={locale('normalize')} onClick={() => setNormalizeLeft(!normalizeLeft)}>{Octicon[normalizeLeft ? 'lock' : 'unlock']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={locale('transformation.rotation_mode', locale(`transformation.rotation_mode.${rotationMode}`))} onClick={() => setRotationMode(rotationMode === 'quaternion' ? 'axis_angle' : 'quaternion')}>{Octicon['arrow_switch']}</button>
|
||||
</div>
|
||||
{Array(4).fill(0).map((_, i) => <div class="transformation-input">
|
||||
<NumberInput value={leftRotation[i].toFixed(3)} onChange={v => changeLeftRotation(i, v)} />
|
||||
<RangeInput min={-1} max={1} step={0.01} value={leftRotation[i]} onChange={v => changeLeftRotation(i, v)} />
|
||||
</div>)}
|
||||
{rotationMode === 'quaternion'
|
||||
? XYZW.map((c, i) =>
|
||||
<Slider label={c} value={leftRotation[i]} onChange={v => changeLeftRotation(i, v)} />)
|
||||
: <>
|
||||
{XYZ.map((c, i) =>
|
||||
<Slider label={c} value={leftRotationAxisAngle.axis[i]} onChange={v => changeLeftRotationAxisAngle(i, v)} />)}
|
||||
<Slider label="θ" value={leftRotationAxisAngle.angle} min={0} max={Math.PI*2} onChange={v => changeLeftRotationAxisAngle(3, v)} />
|
||||
</>}
|
||||
</div>
|
||||
<div class="transformation-section">
|
||||
<div class="transformation-title">
|
||||
<span>{locale('transformation.scale')}</span>
|
||||
<button class="tooltipped tip-se" aria-label={locale('reset')} onClick={() => updateScale(new Vector(1, 1, 1))}>{Octicon['history']}</button>
|
||||
</div>
|
||||
{XYZ.map((c) => <div class="transformation-input">
|
||||
<NumberInput value={scale[c].toFixed(3)} onChange={v => changeScale(c, v)} />
|
||||
<RangeInput min={-1} max={1} step={0.01} value={scale[c]} onChange={v => changeScale(c, v)} />
|
||||
</div>)}
|
||||
{XYZ.map((c) =>
|
||||
<Slider label={c} value={scale[c]} onChange={v => changeScale(c, v)} />
|
||||
)}
|
||||
</div>
|
||||
<div class="transformation-section">
|
||||
<div class="transformation-title">
|
||||
<span>{locale('transformation.right_rotation')}</span>
|
||||
<button class="tooltipped tip-se" aria-label={locale('reset')} onClick={() => updateRightRotation(quat.create())}>{Octicon['history']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={locale('normalize')} onClick={() => setNormalizeRight(!normalizeRight)}>{Octicon[normalizeRight ? 'lock' : 'unlock']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={locale('transformation.rotation_mode', locale(`transformation.rotation_mode.${rotationMode}`))} onClick={() => setRotationMode(rotationMode === 'quaternion' ? 'axis_angle' : 'quaternion')}>{Octicon['arrow_switch']}</button>
|
||||
</div>
|
||||
{Array(4).fill(0).map((_, i) => <div class="transformation-input">
|
||||
<NumberInput value={rightRotation[i].toFixed(3)} onChange={v => changeRightRotation(i, v)} />
|
||||
<RangeInput min={-1} max={1} step={0.01} value={rightRotation[i]} onChange={v => changeRightRotation(i, v)} />
|
||||
</div>)}
|
||||
{rotationMode === 'quaternion'
|
||||
? XYZW.map((c, i) =>
|
||||
<Slider label={c} value={rightRotation[i]} onChange={v => changeRightRotation(i, v)} />)
|
||||
: <>
|
||||
{XYZ.map((c, i) =>
|
||||
<Slider label={c} value={rightRotationAxisAngle.axis[i]} onChange={v => changeRightRotationAxisAngle(i, v)}/>)}
|
||||
<Slider label="θ" value={rightRotationAxisAngle.angle} min={0} max={Math.PI*2} onChange={v => changeRightRotationAxisAngle(3, v)} />
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="transformation-matrix">
|
||||
@@ -189,10 +235,9 @@ export function Transformation({}: Props) {
|
||||
<button class="tooltipped tip-se" aria-label={locale('transformation.copy_composed')} onClick={onCopyComposed}>{Octicon[copiedComposed ? 'check' : 'clippy']}</button>
|
||||
<button class="tooltipped tip-se" aria-label={`${useMatrixOverride ? 'Expected' : 'Current'} behavior (see MC-259853)`} onClick={() => setUseMatrixOverride(!useMatrixOverride)}>{Octicon['info']}</button>
|
||||
</div>
|
||||
{Array(16).fill(0).map((_, i) => <div class="transformation-input">
|
||||
<NumberInput value={matrix.data[i].toFixed(3)} onChange={v => changeMatrix(i, v)} />
|
||||
<RangeInput min={-1} max={1} step={0.01} value={matrix.data[i]} onChange={v => changeMatrix(i, v)} />
|
||||
</div>)}
|
||||
{Array(16).fill(0).map((_, i) =>
|
||||
<Slider value={matrix.data[i]} onChange={v => changeMatrix(i, v)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,6 +250,21 @@ export function Transformation({}: Props) {
|
||||
</main>
|
||||
}
|
||||
|
||||
interface SliderProps {
|
||||
label?: string
|
||||
value: number
|
||||
onChange?: (value: number) => void
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
function Slider({ label, value, onChange, min, max }: SliderProps) {
|
||||
return <div class="transformation-input">
|
||||
{label && <label>{label}</label>}
|
||||
<NumberInput value={value.toFixed(3)} onChange={onChange} />
|
||||
<RangeInput min={min ?? -1} max={max ?? 1} step={0.01} value={value} onChange={onChange} />
|
||||
</div>
|
||||
}
|
||||
|
||||
function formatFloat(x: number) {
|
||||
return x.toFixed(3).replace(/\.?0+$/, '') + 'f'
|
||||
}
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"transformation.right_rotation": "Right rotation",
|
||||
"transformation.copy_decomposed": "Copy decomposed format",
|
||||
"transformation.copy_composed": "Copy matrix format",
|
||||
"transformation.rotation_mode": "Format: %0%",
|
||||
"transformation.rotation_mode.quaternion": "Quaternion",
|
||||
"transformation.rotation_mode.axis_angle": "Axis-angle",
|
||||
"trim_material": "Trim material",
|
||||
"trim_pattern": "Trim pattern",
|
||||
"pack_mcmeta": "Pack.mcmeta",
|
||||
|
||||
@@ -584,13 +584,21 @@ main.has-project {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.transformation-input > * + * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.transformation-input label {
|
||||
color: var(--text-3);
|
||||
font-family: consolas;
|
||||
}
|
||||
|
||||
.transformation-input input[type=number] {
|
||||
width: 100px;
|
||||
padding: 3px 6px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user