EmboDi / src /shared /draft.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
5.42 kB
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
};