Last active
November 3, 2020 16:40
-
-
Save oxyflour/85e65d851d361e7568b6fc9e8d94e0fd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useState } from 'react' | |
import ReactDOM from 'react-dom' | |
/** @type { (fcs: number[][]) => number[][] } */ | |
function getLoops(fcs) { | |
/** @type { { [k: string]: number } } */ | |
const faceNum = { } | |
for (const [a, b, c] of fcs) { | |
for (const [i, j] of [[a, b], [b, c], [c, a]]) { | |
const e = [i, j].sort().join('/') | |
faceNum[e] = (faceNum[e] || 0) + 1 | |
} | |
} | |
return Object.keys(faceNum) | |
.filter(key => faceNum[key] === 1) | |
.map(ab => ab.split('/').map(parseFloat)) | |
} | |
/** @type { (loops: number[][]) => number[][] } */ | |
function getSegments(loops) { | |
/** @type {{ [k: string]: number[] }} */ | |
const conns = { } | |
for (const [i, j] of loops) { | |
for (const [a, b] of [[i, j], [j, i]]) { | |
(conns[a] || (conns[a] = [])).push(b) | |
} | |
} | |
/** @type { (start: number, prev?: number, ret?: number[]) => number[] } */ | |
function findConnected(start, prev = undefined, ret = []) { | |
if (!ret.includes(start)) { | |
ret.push(start) | |
for (const next of conns[start].filter(idx => idx !== prev)) { | |
findConnected(next, start, ret) | |
} | |
} | |
return ret | |
} | |
/** @type { number[][] } */ | |
const segments = [] | |
while (Object.keys(conns).length) { | |
const [start] = Object.keys(conns), | |
segment = findConnected(parseInt(start)) | |
for (const idx of segment) { | |
delete conns[idx] | |
} | |
segments.push(segment) | |
} | |
return segments | |
} | |
function withMouseDown(onMove, onUp) { | |
function move(evt) { | |
onMove(evt) | |
} | |
function up(evt) { | |
onUp && onUp(evt) | |
document.body.removeEventListener('mousemove', move) | |
document.body.removeEventListener('mouseup', up) | |
} | |
document.body.addEventListener('mousemove', move) | |
document.body.addEventListener('mouseup', up) | |
} | |
/** @type { (props: { fcs: number[][], pts: number[][] }) => JSX.Element } */ | |
function App(props) { | |
const [{ fcs, pts }, setMesh] = useState(/** @type {{ fcs: number[][], pts: number[][] }} */({ fcs: [], pts: [] })), | |
[fixed, setFixed] = useState(/** @type { boolean[] } */({ })), | |
[loops, setLoops] = useState(/** @type { number[][] } */([ ])), | |
[history, setHistory] = useState(/** @type { { value: 1, fixed: boolean[], select: number, delta: number[] }[] } */([ ])) | |
useEffect(() => { | |
setMesh(props) | |
const loops = getLoops(props.fcs) | |
setLoops(loops) | |
setFixed(loops.map(() => true)) | |
}, [props.fcs]) | |
function getPtsFromDelta(pts, segment, unfixed, dx, dy) { | |
const delta = pts.map(() => [0, 0]) | |
for (const i of Array.from(new Set(segment))) { | |
delta[i][0] = dx | |
delta[i][1] = dy | |
} | |
for (let i = 0, loss = 1; i < 100 && loss > 1e-3; i ++) { | |
loss = 0 | |
for (const { idx, arr } of unfixed) { | |
let d = delta[idx] | |
d[0] = d[1] = 0 | |
for (const i of arr) { | |
d[0] += delta[i][0] | |
d[1] += delta[i][1] | |
} | |
d[0] /= arr.length | |
d[1] /= arr.length | |
const value = d[0] * d[0] + d[1] * d[1] | |
loss = Math.max(Math.abs((d.lastValue || 0) - value), loss) | |
d.lastValue = value | |
} | |
} | |
return delta.map((d, i) => [pts[i][0] + d[0], pts[i][1] + d[1]]) | |
} | |
function moveSegment(idx, evt) { | |
const segments = getSegments(loops.filter((_, idx) => fixed[idx])), | |
[i, j] = loops[idx], | |
segment = segments.find(segment => segment.includes(i) && segment.includes(j)), | |
{ clientX: sx, clientY: sy } = evt | |
if (segment) { | |
const conns = pts.map((_, idx) => ({ idx, fixed: false, arr: [] })) | |
for (const [idx, [i, j]] of loops.entries()) { | |
conns[i].arr.push(j) | |
conns[j].arr.push(i) | |
if (fixed[idx]) { | |
conns[i].fixed = conns[j].fixed = true | |
} | |
} | |
const unfixed = conns.filter(conn => !conn.fixed) | |
withMouseDown(({ clientX: px, clientY: py }) => { | |
if (Math.abs(sx - px) > 3 && Math.abs(sy - py) > 3) { | |
setMesh({ fcs, pts: getPtsFromDelta(pts, segment, unfixed, px - sx, py - sy) }) | |
} | |
}, ({ clientX: px, clientY: py }) => { | |
if (Math.abs(sx - px) < 3 && Math.abs(sy - py) < 3) { | |
setFixed(Object.assign(fixed.slice(), { [idx]: !fixed[idx] })) | |
} else { | |
setHistory(history.concat({ value: 1, fixed: fixed.slice(), select: idx, delta: [px - sx, py - sy] })) | |
} | |
}) | |
} else { | |
withMouseDown(() => { | |
}, ({ clientX: px, clientY: py }) => { | |
if (Math.abs(sx - px) < 3 && Math.abs(sy - py) < 3) { | |
setFixed(Object.assign(fixed.slice(), { [idx]: !fixed[idx] })) | |
} | |
}) | |
} | |
} | |
function updateHistory(idx, value) { | |
const list = Object.assign(history.slice(), { [idx]: { ...history[idx], value } }) | |
setHistory(list) | |
refreshDelta(list) | |
setFixed(loops.map(() => true)) | |
} | |
function removeHistory(idx) { | |
const list = history.filter((_, i) => i !== idx) | |
setHistory(list) | |
refreshDelta(list) | |
setFixed(loops.map(() => true)) | |
} | |
function refreshDelta(history) { | |
let pts = props.pts | |
for (const { fixed, delta: [dx, dy], select: idx, value } of history) { | |
const segments = getSegments(loops.filter((_, idx) => fixed[idx])), | |
[i, j] = loops[idx], | |
segment = segments.find(segment => segment.includes(i) && segment.includes(j)), | |
conns = pts.map((_, idx) => ({ idx, fixed: false, arr: [] })) | |
for (const [idx, [i, j]] of loops.entries()) { | |
conns[i].arr.push(j) | |
conns[j].arr.push(i) | |
if (fixed[idx]) { | |
conns[i].fixed = conns[j].fixed = true | |
} | |
} | |
const unfixed = conns.filter(conn => !conn.fixed) | |
pts = getPtsFromDelta(pts, segment, unfixed, dx * value, dy * value) | |
} | |
setMesh({ fcs, pts }) | |
} | |
return <> | |
<div style={{ position: 'absolute', right: 10, top: 10 }}> | |
{ | |
history.map(({ value }, idx) => <div key={ idx }> | |
var { idx }: <input type="range" min={ 0 } max={ 1 } step={ 0.001 } | |
value={ value } | |
onChange={ evt => updateHistory(idx, parseFloat(evt.target.value)) } | |
/> <button | |
onClick={ () => removeHistory(idx) }> | |
x | |
</button> | |
</div>) | |
} | |
</div> | |
<svg width={ window.innerWidth } height={ window.innerHeight }> | |
{ | |
fcs.map((ijk, idx) => | |
<polygon key={ idx } | |
points={ ijk.map(i => pts[i].join(',')).join(' ') } fill="gray" />) | |
} | |
{ | |
loops.map(([i, j], idx) => | |
<line key={ idx } | |
style={{ cursor: 'pointer' }} | |
strokeWidth={ 3 } stroke={ fixed[idx] ? 'blue' : 'transparent' } | |
onMouseDown={ evt => moveSegment(idx, evt) } | |
x1={ pts[i][0] } y1={ pts[i][1] } | |
x2={ pts[j][0] } y2={ pts[j][1] } | |
/>) | |
} | |
</svg> | |
</> | |
} | |
document.body.style.margin = document.body.style.padding = '0' | |
const div = document.createElement('div') | |
document.body.append(div) | |
ReactDOM.render(<App | |
pts={ [[100, 100], [200, 100], [100, 200], [200, 200], [100, 300], [200, 300], [100, 400], [200, 400], [300, 200], [300, 300]] } | |
fcs={ [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [3, 8, 5], [8, 5, 9]] } | |
/>, div) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment