File size: 5,419 Bytes
970fc3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import { smoothCurve, smoothingProfile, stabilizedPoint } from "./curve-smoothing.mjs";
const pointOK = (p) => Array.isArray(p) && p.length === 3 && p.every((n) => Number.isFinite(n) && Math.abs(n) <= 100);
const distance = (a, b) => Math.hypot(...a.map((v, i) => v - b[i]));
const DRAFT_LIMITS = Object.freeze({ spacing: 0.025, minLength: 0.15, maxLength: 30, maxPoints: 512, maxGap: 0.75, maxStep: 0.15 });
function pathLength(points) {
return points.slice(1).reduce((sum, p, i) => sum + distance(p, points[i]), 0);
}
function createDraft({ mode = "floor2d", smoothing = "off", validatePath = () => true } = {}) {
smoothingProfile(smoothing);
let points = [], rawPoints = [], stroke = null, rawStroke = null, rawLength = 0, history = [], version = 0, smoothingFallback = false, message = "Hold the right trigger to draw on the floor. Release to finish.";
const changed = () => version++;
function interrupt(reason = "Stroke cancelled. Previous path retained.") {
if (stroke) {
stroke = rawStroke = null;
message = reason;
changed();
}
}
function processed(raw) {
const candidate = smoothCurve(raw, smoothing);
const fallback = smoothing !== "off" && candidate.length > 1 && !validatePath(candidate);
return { points: fallback ? structuredClone(raw) : candidate, fallback };
}
function restore(raw) {
const result = processed(raw);
rawPoints = raw;
const tooShort = raw.length > 1 && pathLength(result.points) < DRAFT_LIMITS.minLength;
points = tooShort ? structuredClone(raw) : result.points;
smoothingFallback = result.fallback || tooShort;
message = smoothingFallback ? "Smoothing did not fit this path. Original stroke retained." : "Draft retained for this edit 路 Redraw or undo";
}
return {
setMode(next) {
if (!["floor2d", "space3d"].includes(next)) throw Error("Invalid brush mode");
mode = next;
},
setSmoothing(next) {
smoothingProfile(next);
if (stroke) throw Error("Release the trigger before adjusting smoothing");
smoothing = next;
restore(rawPoints);
changed();
},
begin(point) {
if (!pointOK(point)) throw new Error("Point at a visible floor");
if (stroke) throw new Error("Release the trigger first");
stroke = [[...point]];
rawStroke = [[...point]];
rawLength = 0;
message = "Drawing 路 Release to finish 路 A to cancel";
changed();
},
sample(point) {
if (!stroke) return false;
if (!pointOK(point)) {
interrupt("Left the visible floor. Stroke discarded; release and try again.");
return false;
}
const last = rawStroke.at(-1), step = distance(point, last);
if (step > DRAFT_LIMITS.maxGap || mode === "floor2d" && Math.abs(point[1] - last[1]) > DRAFT_LIMITS.maxStep) {
interrupt("Stroke contains a large jump. Release and redraw continuously along the floor.");
return false;
}
if (step < DRAFT_LIMITS.spacing) return true;
if (rawStroke.length >= DRAFT_LIMITS.maxPoints || rawLength + step > DRAFT_LIMITS.maxLength) {
interrupt("Stroke too long. Release and draw a shorter path.");
return false;
}
rawStroke.push([...point]);
rawLength += step;
const tip = stabilizedPoint(stroke.at(-1), point, smoothing);
if (distance(tip, stroke.at(-1)) > 1e-6) stroke.push(tip);
changed();
return true;
},
finish() {
if (!stroke) return false;
const result = processed(rawStroke);
if (pathLength(result.points) < DRAFT_LIMITS.minLength) {
interrupt("Stroke too short. Draw a forward path again.");
return false;
}
history.push(rawPoints);
if (history.length > 20) history.shift();
rawPoints = rawStroke;
points = result.points;
smoothingFallback = result.fallback;
stroke = rawStroke = null;
message = result.fallback ? "Smoothed curve crossed the floor edge. Original stroke retained." : "Draft retained for this edit 路 Redraw or undo";
changed();
return true;
},
interrupt,
undo() {
if (stroke) {
interrupt();
return;
}
if (history.length) {
restore(history.pop());
if (!smoothingFallback) message = "Last stroke undone";
changed();
}
},
load(saved) {
points = structuredClone(saved || []);
rawPoints = structuredClone(points);
stroke = rawStroke = null;
history = [];
smoothingFallback = false;
changed();
},
reset() {
points = [];
rawPoints = [];
stroke = rawStroke = null;
history = [];
smoothingFallback = false;
message = "Hold the right trigger to draw on the floor. Release to finish.";
changed();
},
lastPoint: () => rawStroke?.length ? [...rawStroke.at(-1)] : null,
summary: () => ({ mode, smoothing, smoothingLabel: smoothingProfile(smoothing).label, smoothingFallback, version, drawing: !!stroke, pointCount: (stroke || points).length, length: pathLength(stroke || points), canUndo: !!stroke || history.length > 0, message }),
snapshot: () => ({ schema: "vrbuild-draft/1", mode, smoothing, points: structuredClone(points), rawPoints: structuredClone(rawPoints), stroke: stroke ? structuredClone(stroke) : null })
};
}
export {
DRAFT_LIMITS,
createDraft,
pathLength
};
|