Add axis-angle rotation mode

This commit is contained in:
Misode
2023-02-16 01:22:16 +01:00
parent 9c2c1bbfd0
commit dd55fc5dc6
4 changed files with 96 additions and 24 deletions

View File

@@ -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>,

View File

@@ -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'
}

View File

@@ -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",

View File

@@ -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);
}