EmboDi / src /client /tools /draft-tool.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
7.22 kB
import { UI_THEME as T } from "../../shared/ui-theme.mjs";
import * as THREE from "three";
import { createDraft } from "../../shared/draft.mjs";
function createDraftTool({ world, pickGround, smoothing = "off" }) {
const model = createDraft({ smoothing, validatePath: supportedPath }), group = new THREE.Group();
group.visible = false;
world.add(group);
const material = new THREE.MeshBasicMaterial({ toneMapped: false, color: T.accent, depthTest: false });
const line = new THREE.Mesh(new THREE.BufferGeometry(), material);
line.renderOrder = 14;
group.add(line);
const halo = new THREE.Mesh(new THREE.BufferGeometry(), new THREE.MeshBasicMaterial({ toneMapped: false, color: T.panel, depthTest: false }));
halo.renderOrder = 13;
group.add(halo);
const pending = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineDashedMaterial({ toneMapped: false, color: T.panel, dashSize: 0.035, gapSize: 0.09, depthTest: false }));
pending.renderOrder = 15;
group.add(pending);
const dotMaterial = new THREE.MeshBasicMaterial({ toneMapped: false, color: T.accent, depthTest: false });
const brush = new THREE.Mesh(new THREE.RingGeometry(0.04, 0.065, 20), new THREE.MeshBasicMaterial({ toneMapped: false, color: T.panel, side: THREE.DoubleSide, depthTest: false }));
brush.rotation.x = -Math.PI / 2;
brush.renderOrder = 15;
group.add(brush);
const start = new THREE.Mesh(new THREE.SphereGeometry(0.065, 10, 8), dotMaterial), end = new THREE.Mesh(start.geometry, dotMaterial);
start.renderOrder = end.renderOrder = 15;
group.add(start, end);
const arrow = new THREE.ArrowHelper(new THREE.Vector3(0, 0, 1), new THREE.Vector3(), 0.3, T.accent, 0.12, 0.09);
arrow.line.material.toneMapped = arrow.cone.material.toneMapped = false;
arrow.line.material.depthTest = arrow.cone.material.depthTest = false;
arrow.line.renderOrder = arrow.cone.renderOrder = 15;
group.add(arrow);
let owner = null, version = -1, enabled = false, mode = "floor2d", depth = 0.6;
const point = (ray) => mode === "space3d" ? world.worldToLocal(ray.ray.at(depth, new THREE.Vector3())).toArray() : pickGround(ray)?.toArray() || null;
const probe = new THREE.Raycaster();
function supportedPath(points) {
if (mode === "space3d") return true;
world.updateMatrixWorld(true);
for (let n = 1; n < points.length; n++) {
const a = new THREE.Vector3().fromArray(points[n - 1]), b = new THREE.Vector3().fromArray(points[n]), steps = Math.max(1, Math.ceil(a.distanceTo(b) / 0.04));
for (let i = 0; i <= steps; i++) {
const p = a.clone().lerp(b, i / steps), origin = world.localToWorld(p.clone().add(new THREE.Vector3(0, 0.2, 0)));
probe.set(origin, new THREE.Vector3(0, -1, 0).transformDirection(world.matrixWorld));
const ground = pickGround(probe);
if (!ground || Math.abs(ground.y - p.y) > 0.05) return false;
}
}
return true;
}
function continuousSurface(next) {
if (mode === "space3d") return next;
const last = model.lastPoint();
if (!last || !next) return next;
const a = new THREE.Vector3().fromArray(last), b = new THREE.Vector3().fromArray(next), steps = Math.ceil(a.distanceTo(b) / 0.08);
for (let i = 1; i < Math.min(steps, 12); i++) {
const p = a.clone().lerp(b, i / steps), origin = p.clone().add(new THREE.Vector3(0, 0.2, 0));
world.localToWorld(origin);
probe.set(origin, new THREE.Vector3(0, -1, 0).transformDirection(world.matrixWorld));
const ground = pickGround(probe);
if (!ground || Math.abs(ground.y - p.y) > 0.12) return null;
}
return next;
}
function paint() {
const next = model.summary();
if (next.version === version) return;
version = next.version;
const data = model.snapshot(), points = (data.stroke || data.points).map((p) => new THREE.Vector3(p[0], p[1] + (mode === "floor2d" ? 0.025 : 0), p[2]));
line.geometry.dispose();
line.visible = points.length > 1;
const path = new THREE.CurvePath();
for (let i = 1; i < points.length; i++) path.add(new THREE.LineCurve3(points[i - 1], points[i]));
line.geometry = points.length > 1 ? new THREE.TubeGeometry(path, Math.max(8, points.length * 2), 0.012, 6, false) : new THREE.BufferGeometry();
halo.geometry.dispose();
halo.visible = line.visible;
halo.geometry = points.length > 1 ? new THREE.TubeGeometry(path, Math.max(8, points.length * 2), 0.022, 6, false) : new THREE.BufferGeometry();
pending.geometry.dispose();
pending.geometry = new THREE.BufferGeometry().setFromPoints(points);
pending.computeLineDistances();
pending.visible = !!data.stroke && points.length > 1;
start.visible = end.visible = points.length > 0;
arrow.visible = points.length > 1;
if (points.length) {
start.position.copy(points[0]);
end.position.copy(points.at(-1));
}
if (points.length > 1) {
const direction = points.at(-1).clone().sub(points.at(-2));
direction.y = 0;
if (direction.lengthSq() > 1e-5) {
arrow.position.copy(points.at(-1));
arrow.setDirection(direction.normalize());
} else arrow.visible = false;
}
}
return {
open(points = [], options = {}) {
mode = options.mode || "floor2d";
depth = options.depth || 0.6;
model.setMode(mode);
enabled = true;
group.visible = true;
owner = null;
model.load(points);
paint();
},
close() {
enabled = false;
group.visible = false;
owner = null;
model.reset();
paint();
},
depth(delta) {
if (owner && owner !== "mouse") return;
depth = THREE.MathUtils.clamp(depth + delta, 0.2, 5);
},
cycleSmoothing() {
const levels = ["off", "standard", "strong", "extreme"], current = model.summary().smoothing;
model.setSmoothing(levels[(levels.indexOf(current) + 1) % levels.length]);
paint();
},
active: () => enabled,
begin(ray, input) {
if (!enabled || owner) return false;
model.begin(point(ray));
owner = input;
paint();
return true;
},
update(ray, input) {
if (!enabled) return;
const p = point(ray);
brush.visible = !!p;
if (p) brush.position.set(p[0], p[1] + 0.03, p[2]);
if (owner === input) model.sample(continuousSurface(p));
paint();
},
release(input) {
if (owner !== input) return false;
owner = null;
const result = model.finish();
paint();
return result;
},
suspend(reason) {
owner = null;
model.interrupt(reason);
brush.visible = false;
paint();
},
clear() {
model.reset();
paint();
},
undo() {
owner = null;
model.undo();
paint();
},
summary: () => ({ ...model.summary(), depth, message: mode === "space3d" ? model.summary().message.replace("Hold the right trigger to draw on the floor. Release to finish.", "Spatial brush · Hold right trigger and move your hand · Y Save menu") : model.summary().message }),
snapshot: () => ({ ...model.snapshot(), active: enabled, owner: owner === "mouse" ? "mouse" : owner ? "controller" : null })
};
}
export {
createDraftTool
};