hm
This commit is contained in:
parent
7455d630fc
commit
4d3fc2cd47
84 changed files with 3429 additions and 3480 deletions
|
|
@ -51,17 +51,252 @@
|
|||
}
|
||||
function pointSegmentDistance(p, seg) {
|
||||
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0;
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const lenSq = dx * dx + dy * dy;
|
||||
if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1);
|
||||
const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq));
|
||||
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||
return global.TarinaiGeometry.pointSegmentDistance(p.x, p.y, seg[0], seg[1], seg[2], seg[3]);
|
||||
}
|
||||
|
||||
function canvasLocalPoint(canvas, e, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0) {
|
||||
if (!canvas || !e) return { x: 0, y: 0 };
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
const sy = canvas.height / Math.max(1, rect.height);
|
||||
return { x: (e.clientX - rect.left) * sx - cx, y: (e.clientY - rect.top) * sy - cy };
|
||||
}
|
||||
|
||||
function cleanSegments(segments = [], opts = {}) {
|
||||
const limit = Math.max(1, Number(opts.limit || 96) || 96);
|
||||
const bound = Math.max(1, Number(opts.bound || 420) || 420);
|
||||
const minLength = Math.max(0, Number(opts.minLength || 4) || 4);
|
||||
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
|
||||
const out = [];
|
||||
for (const seg of segments || []) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
const x1 = Math.max(-bound, Math.min(bound, Number(seg[0]) || 0));
|
||||
const y1 = Math.max(-bound, Math.min(bound, Number(seg[1]) || 0));
|
||||
const x2 = Math.max(-bound, Math.min(bound, Number(seg[2]) || 0));
|
||||
const y2 = Math.max(-bound, Math.min(bound, Number(seg[3]) || 0));
|
||||
if (Math.hypot(x2 - x1, y2 - y1) >= minLength) out.push([x1, y1, x2, y2]);
|
||||
if (out.length >= limit) break;
|
||||
}
|
||||
if (!out.length) out.push(fallback.slice(0, 4));
|
||||
return out;
|
||||
}
|
||||
|
||||
function drawEditorBase(ctx, canvas, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0) {
|
||||
if (!ctx || !canvas) return false;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#f7f3ea";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.translate(cx, cy);
|
||||
ctx.strokeStyle = "rgba(100,80,60,0.16)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = -cx; x <= cx; x += 40) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); }
|
||||
for (let y = -cy; y <= cy; y += 40) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); }
|
||||
ctx.strokeStyle = "rgba(70,60,50,0.38)";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke();
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawSegments(ctx, segments = [], opts = {}) {
|
||||
if (!ctx) return false;
|
||||
const lineWidth = Math.max(1, Number(opts.lineWidth || 12) || 12);
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = opts.strokeStyle || "rgba(80,150,205,0.92)";
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
for (const seg of segments || []) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); }
|
||||
ctx.stroke();
|
||||
if (opts.highlight !== false) {
|
||||
ctx.strokeStyle = opts.highlightStyle || "rgba(255,255,255,0.58)";
|
||||
ctx.lineWidth = Math.max(1.3, lineWidth * 0.20);
|
||||
ctx.beginPath();
|
||||
for (const seg of segments || []) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); }
|
||||
ctx.stroke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function eraseSegmentsAt(segments = [], point, radius) {
|
||||
const before = segments.length;
|
||||
const next = (segments || []).filter(seg => pointSegmentDistance(point, seg) > radius);
|
||||
return { segments: next, changed: next.length !== before };
|
||||
}
|
||||
|
||||
function syncInputPair(a, b, onChange = null) {
|
||||
if (!a || !b) return false;
|
||||
const sync = (from, to) => {
|
||||
to.value = from.value;
|
||||
if (typeof onChange === "function") onChange();
|
||||
};
|
||||
a.oninput = () => sync(a, b);
|
||||
b.oninput = () => sync(b, a);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function drawDraftSegment(ctx, draft, lineWidth, opts = {}) {
|
||||
if (!ctx || !draft) return false;
|
||||
ctx.setLineDash(opts.dash || [7, 5]);
|
||||
ctx.strokeStyle = opts.strokeStyle || "rgba(60,130,210,0.85)";
|
||||
ctx.lineWidth = Math.max(2, Number(lineWidth || 12) * 0.45);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(draft[0], draft[1]);
|
||||
ctx.lineTo(draft[2], draft[3]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawErasePreview(ctx, cx, cy, point, radius, opts = {}) {
|
||||
if (!ctx || !point) return false;
|
||||
ctx.save();
|
||||
ctx.translate(cx || 0, cy || 0);
|
||||
ctx.globalAlpha = opts.alpha ?? 0.42;
|
||||
ctx.strokeStyle = opts.strokeStyle || "rgba(210,70,60,0.86)";
|
||||
ctx.lineWidth = opts.lineWidth || 1.6;
|
||||
ctx.setLineDash(opts.dash || [4, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
function createSegmentEditorController(opts = {}) {
|
||||
const dialog = opts.dialog;
|
||||
const canvas = opts.canvas;
|
||||
const ctx = opts.ctx || canvas?.getContext?.("2d");
|
||||
if (!dialog || !canvas || !ctx) return null;
|
||||
const prefix = opts.prefix || "rotator";
|
||||
const cx = opts.cx ?? canvas.width / 2;
|
||||
const cy = opts.cy ?? canvas.height / 2;
|
||||
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
|
||||
let mode = "line";
|
||||
let segments = (opts.segments || []).map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4);
|
||||
let draft = null;
|
||||
let drawing = false;
|
||||
|
||||
const lineWidth = () => Math.max(1, Number(typeof opts.lineWidth === "function" ? opts.lineWidth() : opts.lineWidth || 12) || 12);
|
||||
const strokeStyle = () => typeof opts.strokeStyle === "function" ? opts.strokeStyle() : opts.strokeStyle;
|
||||
const clean = () => { segments = cleanSegments(segments, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength }); };
|
||||
const toLocal = (e) => canvasLocalPoint(canvas, e, cx, cy);
|
||||
|
||||
const draw = () => {
|
||||
drawEditorBase(ctx, canvas, cx, cy);
|
||||
if (typeof opts.drawBeforeSegments === "function") opts.drawBeforeSegments(ctx, { cx, cy, lineWidth: lineWidth(), segments: segments.slice() });
|
||||
const lw = lineWidth();
|
||||
drawSegments(ctx, segments, { lineWidth: lw, strokeStyle: strokeStyle() || "rgba(80,150,205,0.92)" });
|
||||
drawDraftSegment(ctx, draft, lw, opts.draft || {});
|
||||
if (typeof opts.drawCenter === "function") opts.drawCenter(ctx, { cx, cy, lineWidth: lw, segments: segments.slice() });
|
||||
ctx.restore();
|
||||
return true;
|
||||
};
|
||||
|
||||
const setMode = (next) => {
|
||||
mode = next === "free" || next === "erase" ? next : "line";
|
||||
for (const btn of dialog.querySelectorAll(`[data-${prefix}-mode]`)) btn.classList.toggle("active", btn.dataset[`${prefix}Mode`] === mode);
|
||||
canvas.style.cursor = mode === "erase" ? "cell" : "crosshair";
|
||||
return mode;
|
||||
};
|
||||
|
||||
const eraseAt = (p) => {
|
||||
const radius = Math.max(1, typeof opts.eraseRadius === "function" ? opts.eraseRadius(lineWidth()) : Number(opts.eraseRadius || 24) || 24);
|
||||
const erased = eraseSegmentsAt(segments, p, radius);
|
||||
segments = erased.segments;
|
||||
if (erased.changed) { draft = null; draw(); return true; }
|
||||
draw();
|
||||
drawErasePreview(ctx, cx, cy, p, radius, opts.erasePreview || {});
|
||||
return false;
|
||||
};
|
||||
|
||||
const setTemplate = (name) => {
|
||||
const shared = templateSegments(name);
|
||||
if (shared) segments = shared.map(seg => seg.slice(0, 4));
|
||||
clean();
|
||||
draw();
|
||||
};
|
||||
|
||||
dialog.querySelectorAll(`[data-${prefix}-mode]`).forEach(btn => { btn.onclick = () => setMode(btn.dataset[`${prefix}Mode`]); });
|
||||
dialog.querySelectorAll(`[data-${prefix}-template]`).forEach(btn => { btn.onclick = () => setTemplate(btn.dataset[`${prefix}Template`]); });
|
||||
|
||||
canvas.onpointerdown = (e) => {
|
||||
canvas.setPointerCapture?.(e.pointerId);
|
||||
const p = toLocal(e);
|
||||
drawing = true;
|
||||
if (mode === "erase") { draft = null; eraseAt(p); return; }
|
||||
draft = [p.x, p.y, p.x, p.y];
|
||||
if (mode === "free") segments.push([p.x, p.y, p.x, p.y]);
|
||||
draw();
|
||||
};
|
||||
canvas.onpointermove = (e) => {
|
||||
if (!drawing) return;
|
||||
const p = toLocal(e);
|
||||
if (mode === "erase") { eraseAt(p); return; }
|
||||
if (!draft) return;
|
||||
if (mode === "free") {
|
||||
const last = segments[segments.length - 1];
|
||||
if (!last) return;
|
||||
const lx = last[2], ly = last[3];
|
||||
if (Math.hypot(p.x - lx, p.y - ly) >= 5) {
|
||||
last[2] = p.x; last[3] = p.y;
|
||||
segments.push([p.x, p.y, p.x, p.y]);
|
||||
}
|
||||
} else {
|
||||
draft[2] = p.x; draft[3] = p.y;
|
||||
}
|
||||
draw();
|
||||
};
|
||||
canvas.onpointerup = (e) => {
|
||||
if (!drawing) return;
|
||||
drawing = false;
|
||||
const p = toLocal(e);
|
||||
if (mode === "erase") { draft = null; draw(); return; }
|
||||
if (!draft) return;
|
||||
if (mode === "line") {
|
||||
const seg = [draft[0], draft[1], p.x, p.y];
|
||||
if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= 4) segments.push(seg);
|
||||
}
|
||||
draft = null;
|
||||
clean();
|
||||
draw();
|
||||
};
|
||||
|
||||
const undo = dialog.querySelector(`#${prefix}Undo`);
|
||||
const clear = dialog.querySelector(`#${prefix}Clear`);
|
||||
if (undo) undo.onclick = () => { segments.pop(); clean(); draw(); };
|
||||
if (clear) clear.onclick = () => { segments = []; draft = null; draw(); };
|
||||
|
||||
const close = () => {
|
||||
canvas.style.cursor = "";
|
||||
canvas.onpointerdown = canvas.onpointermove = canvas.onpointerup = null;
|
||||
};
|
||||
|
||||
setMode(opts.initialMode || "line");
|
||||
return {
|
||||
cleanSegments: clean,
|
||||
close,
|
||||
draw,
|
||||
getSegments: () => segments.map(seg => seg.slice(0, 4)),
|
||||
setMode,
|
||||
setSegments(next = []) { segments = next.map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4); draft = null; draw(); },
|
||||
};
|
||||
}
|
||||
|
||||
global.TarinaiPhysicsShapeEditorSystem = Object.freeze({
|
||||
decorateToolbar,
|
||||
templateSegments,
|
||||
pointSegmentDistance,
|
||||
canvasLocalPoint,
|
||||
cleanSegments,
|
||||
drawEditorBase,
|
||||
drawSegments,
|
||||
eraseSegmentsAt,
|
||||
syncInputPair,
|
||||
drawDraftSegment,
|
||||
drawErasePreview,
|
||||
createSegmentEditorController,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue