mirror of https://github.com/hnmr293/posex.git
implement save/load
parent
cac16b60ed
commit
a054e8f78d
|
|
@ -1,3 +1,4 @@
|
|||
__pycache__
|
||||
.vscode
|
||||
venv
|
||||
.vscode/
|
||||
venv/
|
||||
saved_poses/
|
||||
120
app.py
120
app.py
|
|
@ -2,12 +2,130 @@ if __name__ == '__main__':
|
|||
import mimetypes
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
|
||||
from flask import Flask
|
||||
import os, sys, importlib, glob, json, base64, traceback, re
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
if importlib.util.find_spec('PIL') is not None:
|
||||
from PIL import Image, PngImagePlugin
|
||||
else:
|
||||
import subprocess
|
||||
try:
|
||||
print('-' * 80, file=sys.stderr)
|
||||
print('| installing PIL (pillow) ...')
|
||||
print('-' * 80, file=sys.stderr)
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", 'pillow'],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr
|
||||
)
|
||||
except Exception as e:
|
||||
msg = ''.join(traceback.TracebackException.from_exception(e).format())
|
||||
print(msg, file=sys.stderr)
|
||||
print('-' * 80, file=sys.stderr)
|
||||
print('| failed to install PIL (pillow). exit...', file=sys.stderr)
|
||||
print('-' * 80, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
app = Flask(__name__, static_folder='.', static_url_path='')
|
||||
|
||||
def atoi(text):
|
||||
return int(text) if text.isdigit() else text
|
||||
def natural_keys(text):
|
||||
return [ atoi(c) for c in re.split(r'(\d+)', text) ]
|
||||
def sorted_glob(path):
|
||||
return sorted(glob.glob(path), key=natural_keys)
|
||||
|
||||
def get_saved_poses():
|
||||
dirpath = os.path.join(app.static_folder, 'saved_poses')
|
||||
for path in sorted_glob(f'{dirpath}/*.png'):
|
||||
yield Image.open(path)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
def api_try(fn):
|
||||
@wraps(fn)
|
||||
def f(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
msg = ''.join(traceback.TracebackException.from_exception(e).format())
|
||||
print(msg, file=sys.stderr)
|
||||
return jsonify(result=str(e), ok=False)
|
||||
return f
|
||||
|
||||
@app.route('/pose/all')
|
||||
@api_try
|
||||
def saved_poses():
|
||||
result = []
|
||||
for img in get_saved_poses():
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format='png')
|
||||
|
||||
if not hasattr(img, 'text'):
|
||||
continue
|
||||
|
||||
result.append({
|
||||
'name': img.text['name'],
|
||||
'image': base64.b64encode(buffer.getvalue()).decode('ascii'),
|
||||
'screen': json.loads(img.text['screen']),
|
||||
'camera': json.loads(img.text['camera']),
|
||||
'joints': json.loads(img.text['joints']),
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
def name2path(name: str):
|
||||
if not isinstance(name, str):
|
||||
raise ValueError(f'str object expected, but {type(name)}')
|
||||
if len(name) == 0:
|
||||
raise ValueError(f'empty name')
|
||||
if '.' in name or '/' in name or '\\' in name:
|
||||
raise ValueError(f'invalid name: {name}')
|
||||
path = os.path.realpath(os.path.join(app.static_folder, 'saved_poses', f'{name}.png'))
|
||||
prefix = os.path.realpath(os.path.join(app.static_folder, 'saved_poses'))
|
||||
if not path.startswith(prefix):
|
||||
raise ValueError(f'invalid name: {name}')
|
||||
return path
|
||||
|
||||
@app.route('/pose/save', methods=['POST'])
|
||||
@api_try
|
||||
def save_pose():
|
||||
data = request.json
|
||||
|
||||
name = data['name']
|
||||
screen = data['screen']
|
||||
camera = data['camera']
|
||||
joints = data['joints']
|
||||
|
||||
info = PngImagePlugin.PngInfo()
|
||||
info.add_text('name', name)
|
||||
info.add_text('screen', json.dumps(screen))
|
||||
info.add_text('camera', json.dumps(camera))
|
||||
info.add_text('joints', json.dumps(joints))
|
||||
|
||||
filepath = name2path(name)
|
||||
|
||||
image = Image.open(BytesIO(base64.b64decode(data['image'][len('data:image/png;base64,'):])))
|
||||
unit = max(image.width, image.height)
|
||||
mx, my = (unit - image.width) // 2, (unit - image.height) // 2
|
||||
canvas = Image.new('RGB', (unit, unit), color=(68, 68, 68))
|
||||
canvas.paste(image, (mx, my))
|
||||
image = canvas.resize((canvas.width//4, canvas.height//4))
|
||||
|
||||
image.save(filepath, pnginfo=info)
|
||||
|
||||
return jsonify(result='pose saved', ok=True)
|
||||
|
||||
@app.route('/pose/delete', methods=['POST'])
|
||||
@api_try
|
||||
def delete_pose():
|
||||
data = request.json
|
||||
filepath = name2path(data['name'])
|
||||
os.remove(filepath)
|
||||
return jsonify(result='pose deleted', ok=True)
|
||||
|
||||
app.run(port=55502, debug=True)
|
||||
|
|
|
|||
54
css/main.css
54
css/main.css
|
|
@ -68,4 +68,58 @@
|
|||
position: absolute;
|
||||
outline: 1px solid gray;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#saved_poses {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.25em;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#saved_poses > * {
|
||||
margin: 0;
|
||||
outline: 1px solid gray;
|
||||
max-width: 128px;
|
||||
}
|
||||
|
||||
#saved_poses img {
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
}
|
||||
|
||||
#saved_poses figcaption {
|
||||
padding: 0 0.25em;
|
||||
overflow-wrap: anywhere; /* Opera Android may not be able to interpret `anywhere` keyword. */
|
||||
}
|
||||
|
||||
#saved_poses .close {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
border: 1px solid gray;
|
||||
background-color: white;
|
||||
opacity: 0.5;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#saved_poses .close:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
#saved_poses .close2 {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 1.5em;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#saved_poses .close:hover .close2 {
|
||||
display: block;
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -52,8 +52,12 @@
|
|||
</div>
|
||||
<div id="body_indicator2"></div>
|
||||
<div id="body_indicator1"></div>
|
||||
<a id="save_button" class="box" href="#">💾 Save</a>
|
||||
<a id="copy_button" class="box" href="#">📋 Copy to clipboard</a>
|
||||
<div style="display: flex; flex-direction: row; gap: 0 0.25em;">
|
||||
<a id="save_button" class="box" style="flex: 1 1 0;" href="#">💾 Save Image</a>
|
||||
<a id="copy_button" class="box" style="flex: 1 1 0;" href="#">📋 Copy to clipboard</a>
|
||||
</div>
|
||||
<button id="save_pose" class="box">💾🧍 Save Pose</button>
|
||||
<div id="saved_poses"></div>
|
||||
</div>
|
||||
<p id="notation"></p>
|
||||
<div id="notifications"></div>
|
||||
|
|
|
|||
129
js/app.js
129
js/app.js
|
|
@ -1,5 +1,80 @@
|
|||
const POSES = new Map();
|
||||
|
||||
function notify(str, type) {
|
||||
if (type === undefined) type = 'success';
|
||||
|
||||
switch (type) {
|
||||
case 'success': console.log(str); break;
|
||||
case 'info': console.log(str); break;
|
||||
case 'warn': console.warn(str); break;
|
||||
case 'error': console.error(str); break;
|
||||
}
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = str;
|
||||
p.classList.add('item', type);
|
||||
const cont = document.querySelector('#notifications');
|
||||
cont.appendChild(p);
|
||||
setTimeout(() => cont.removeChild(p), 3000);
|
||||
}
|
||||
|
||||
async function save_pose(obj) {
|
||||
const res = await fetch('/pose/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(obj),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.ok) reload_poses();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function delete_pose(name) {
|
||||
const res = await fetch('/pose/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const result = await res.json();
|
||||
notify(result.result, result.ok ? 'success' : 'error');
|
||||
if (result.ok) reload_poses();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function reload_poses() {
|
||||
POSES.clear();
|
||||
|
||||
const res = await fetch('/pose/all');
|
||||
const cont = document.querySelector('#saved_poses');
|
||||
cont.innerHTML = '';
|
||||
const df = document.createDocumentFragment();
|
||||
for (let data of await res.json()) {
|
||||
POSES.set(data.name, data);
|
||||
|
||||
const fig = document.createElement('figure')
|
||||
const img = document.createElement('img');
|
||||
const cap = document.createElement('figcaption');
|
||||
const clo = document.createElement('div');
|
||||
const clo2 = document.createElement('span');
|
||||
fig.dataset.poseName = data.name;
|
||||
cap.textContent = data.name;
|
||||
clo.textContent = 'x';
|
||||
clo.classList.add('close');
|
||||
clo2.classList.add('close2');
|
||||
clo2.textContent = 'delete';
|
||||
clo.appendChild(clo2);
|
||||
|
||||
img.src = 'data:image/png;base64,' + data.image;
|
||||
img.title = data.name;
|
||||
fig.append(clo, img, cap);
|
||||
|
||||
df.appendChild(fig);
|
||||
}
|
||||
cont.appendChild(df);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const { init, init_3d } = await import('posex');
|
||||
|
||||
const ui = {
|
||||
container: document.querySelector('#cont'),
|
||||
canvas: document.querySelector('#main_canvas'),
|
||||
|
|
@ -17,21 +92,49 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
reset_bg: document.querySelector('#reset_bg'),
|
||||
save: document.querySelector('#save_button'),
|
||||
copy: document.querySelector('#copy_button'),
|
||||
notify: function () {
|
||||
function notify(str, type) {
|
||||
if (type === undefined) type = 'success';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = str;
|
||||
p.classList.add('item', type);
|
||||
const cont = document.querySelector('#notifications');
|
||||
cont.appendChild(p);
|
||||
setTimeout(() => cont.removeChild(p), 3000);
|
||||
}
|
||||
},
|
||||
save_pose: document.querySelector('#save_pose'),
|
||||
save_pose_callback: save_pose,
|
||||
notify: notify,
|
||||
};
|
||||
|
||||
document.addEventListener('poseload', e => {
|
||||
const obj = POSES.get(e.detail.name);
|
||||
if (obj) ui.loadPose(obj);
|
||||
}, false);
|
||||
|
||||
const { init, init_3d } = await import('posex');
|
||||
|
||||
init(ui);
|
||||
const animate = init_3d(ui);
|
||||
animate();
|
||||
|
||||
await reload_poses();
|
||||
|
||||
}, false);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const get_name = ele => {
|
||||
while (ele && ele !== document) {
|
||||
if (ele.dataset && ele.dataset.poseName !== undefined)
|
||||
return ele.dataset.poseName;
|
||||
ele = ele.parentNode;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
document.querySelector('#saved_poses').addEventListener('click', e => {
|
||||
let target = e.target;
|
||||
if (target.tagName === 'IMG') target = target.parentNode;
|
||||
if (target.classList.contains('close2')) target = target.parentNode;
|
||||
if (target.tagName === 'FIGURE') {
|
||||
const name = get_name(target);
|
||||
const ev = new CustomEvent('poseload', { bubbles: true, detail: { name } });
|
||||
target.dispatchEvent(ev);
|
||||
} else if (target.classList.contains('close')) {
|
||||
const name = get_name(target);
|
||||
if (name.length != 0) {
|
||||
delete_pose(name);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
}, false);
|
||||
|
|
|
|||
77
js/posex.js
77
js/posex.js
|
|
@ -233,6 +233,8 @@ function init_3d(ui) {
|
|||
bodies.set(name, body);
|
||||
scene.add(group);
|
||||
touchable_bodies.push(group);
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const remove_body = name => {
|
||||
|
|
@ -445,6 +447,53 @@ function init_3d(ui) {
|
|||
if (ui.reset_bg)
|
||||
ui.reset_bg.addEventListener('click', () => set_bg(null), false);
|
||||
|
||||
function get_pose_dict(obj3d) {
|
||||
return {
|
||||
position: obj3d.position.toArray(),
|
||||
rotation: obj3d.rotation.toArray(),
|
||||
scale: obj3d.scale.toArray(),
|
||||
up: obj3d.up.toArray(),
|
||||
};
|
||||
}
|
||||
|
||||
function set_pose_dict(obj3d, dict) {
|
||||
obj3d.position.set(...dict.position);
|
||||
obj3d.rotation.set(...dict.rotation);
|
||||
obj3d.scale.set(...dict.scale);
|
||||
obj3d.up.set(...dict.up);
|
||||
}
|
||||
|
||||
if (ui.save_pose && ui.save_pose_callback)
|
||||
ui.save_pose.addEventListener('click', async () => {
|
||||
const name = prompt('Input pose name.');
|
||||
if (name === undefined || name === null || name === '') return;
|
||||
|
||||
const screen = {
|
||||
width: width(),
|
||||
height: height(),
|
||||
}
|
||||
|
||||
const camera_ = get_pose_dict(camera);
|
||||
|
||||
const joints = [];
|
||||
for (let [name, body] of bodies) {
|
||||
joints.push({
|
||||
name,
|
||||
joints: body.joints.map(j => get_pose_dict(j)),
|
||||
group: get_pose_dict(body.group),
|
||||
x0: body.x0,
|
||||
y0: body.y0,
|
||||
z0: body.z0,
|
||||
});
|
||||
}
|
||||
|
||||
const image = await ui.getDataURL();
|
||||
|
||||
const data = { name, image, screen, camera: camera_, joints };
|
||||
const result = await ui.save_pose_callback(data);
|
||||
ui.notify(result.result, result.ok ? 'success' : 'error');
|
||||
}, false);
|
||||
|
||||
const onAnimateEndOneshot = [];
|
||||
|
||||
const animate = () => {
|
||||
|
|
@ -499,6 +548,34 @@ function init_3d(ui) {
|
|||
onAnimateEndOneshot.length = 0;
|
||||
};
|
||||
|
||||
ui.loadPose = function(data) {
|
||||
selected_body = null;
|
||||
touched_body = null;
|
||||
touchable_objects.length = 0;
|
||||
touchable_bodies.length = 0;
|
||||
object_to_body.clear();
|
||||
for (let name of bodies.keys()) {
|
||||
remove_body(name);
|
||||
}
|
||||
|
||||
// screen
|
||||
size_change(data.screen.width, data.screen.height);
|
||||
if (width_input) width_input.value = data.screen.width;
|
||||
if (height_input) height_input.value = data.screen.height;
|
||||
|
||||
// camera
|
||||
set_pose_dict(camera, data.camera);
|
||||
|
||||
// bodies
|
||||
for (let dict of data.joints) {
|
||||
const body = add_body(dict.name, dict.x0, dict.y0, dict.z0);
|
||||
for (let i = 0, e = Math.min(body.joints.length, dict.joints.length); i < e; ++i) {
|
||||
set_pose_dict(body.joints[i], dict.joints[i]);
|
||||
}
|
||||
set_pose_dict(body.group, dict.group);
|
||||
}
|
||||
};
|
||||
|
||||
ui.getDataURL = async function() {
|
||||
const pr = new Promise(resolve => {
|
||||
const current_bg = scene.background;
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
flask
|
||||
pillow
|
||||
|
|
|
|||
Loading…
Reference in New Issue