Skip to content

Instantly share code, notes, and snippets.

@oxyflour
Last active November 3, 2020 16:40
Show Gist options
  • Save oxyflour/85e65d851d361e7568b6fc9e8d94e0fd to your computer and use it in GitHub Desktop.
Save oxyflour/85e65d851d361e7568b6fc9e8d94e0fd to your computer and use it in GitHub Desktop.
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