RedNode Excavation
Omconsole Machine Control · Task Intelligence
LIVE OPS · EXCAVATION NODE
J
Site Operator
Omconsole · Excavation Control
Sensor Fusion Snapshot
Lidar · Infrared · Grade / Depth
Simulated · tied to alerts & awareness
Lidar < 16ft 6 hits Closest: 9.0 ft
Infrared Hotspots 4 zones Max temp: 60.5 °C
Grade & Depth 0' 0" Slope: 7.4 ° · Depth: 0' 0"
Sensor readings are simulated offline, but tied to workflow events (alerts & awareness triggers).
Cluster Overview
Excavation Node Status
Post holes · Pipe runs · Stripping · No-go zones
Active Primary Tasks
0
No tasks yet
Awareness Mode
OFF
Secondary triggers disabled
Obstacles in Reach
6 objects
Closest: 9.0 ft
Live Site Map 0 shapes
Draw geometry · Select · Measure · Overlay · Simulate excavator
TOOL: AUTO
SCALE 1ft = px
Pointer
Selected
Machine idle
Overlay (image / 3D wire)
Dig radiance (depth-weighted)
Pile radiance (dumped fill)
Area tasks
Pipe / cable runs
Posts / points
No-go zones
Starts empty. Draw shapes first, then add tasks. Use Select to move/edit and Clear Map to restart. Units: feet (depth uses feet + inches).
3D / Image Overlays No overlay
Upload · Position · Rotate · Opacity
3D layers · Image overlays Upload · Place · Scale (ft) · Rotate · Layer
No overlay loaded
Workflow: Upload → choose asset → set width/scale → click on the map to drop. Use Select to move; use the panel below to fine-tune.
Drop in a site photo (image) or a lightweight 3D model (.GLB/.GLTF/.OBJ/.PLY). 3D is rendered as a top-down wireframe projection so it can be aligned to the map (offline, no external libs).
Show Overlay
Overlay stays locked to the map (pan/zoom).
0.55
X Y
Tip: Select an overlay on the map (Select tool) to edit it here. For images, only width is required (height stays locked to aspect ratio). Models use Scale (ft / unit) for size.
3D Overlay & Boom Preview GLTF / GLB / OBJ
Inline orbit · Popup · XYZ alignment
The 3D view orbits around the overlay anchor (X/Y/Z) and renders the excavator boom (turret + boom + arm + bucket) in real time. Use 3D View to dock inline or Pop-up for a larger view.
Machine Telemetry
idle
Mode
idle
Heading
Turret
Boom lift
60%
Reach
9.0ft
Bucket curl
45°
Bucket tilt
Bucket fill
0%
Action
Live kinematics are logged step-by-step (lower/raise/tilt/curl/slew/track).
Earthworks
Depth Radiance & Live Cut/Fill
Graded digging/piling streamed into the live site map
Radiance Map Baseline ready
Cyan = excavation · Amber/Red = fill
Deeper cut (excavation)
Raised fill / piling
Baseline grade
Depth radiance updates the 2D map overlay and this live preview whenever you cut or fill.
Digging & Piling Controls
Radiance graded by depth
Use the same world coordinates as the main map. Radiance is also rendered on the live site canvas so depth deltas stay in context with overlays, tasks, and the excavator rig.
Task Manager
Main & Secondary Jobs
Priority · Grade · Ground · Depth (ft/in) · Workflow
Main Tasks
Requires map geometry
ft in
Tip: choose Auto + draw the right geometry; then click Add Main Task.
Secondary Tasks
Awareness / follow-up
ft in
Secondary tasks can run as interruptions when Awareness is ON.
Enable Awareness Tasks
When on, the simulation can automatically insert secondary tasks based on alerts (obstacles, slope, no-go entry).
1.0×
Speed scales movement/operation timing. For more realistic pacing keep it near 1×.
Tasks are fully loggable: every step emits telemetry (track/slew/boom/bucket) + events + training rows.
Execution & Learning
Queue · Playback · Training Log
Sequential workflow · interruption handling · operator feedback
Task Queue
0 tasks queued
# Task P G Ground Depth Kind Status
Main tasks run in priority order. Awareness may insert secondary interrupts (track reset / leveling / bucket set).
Simulation Playback
track · slew · boom · bucket
Shows step-by-step actions like bucket lower/raise/tilt/curl + turret turn + track moves.
Adaptive Training Log
pos(ft) · kinematics · action
Captures telemetry per step for offline learning/replay.
Operator Feedback
per task completion
Situational Alerts
sensor-driven
No active alerts. Node is operating within envelopes.
Alerts can pause the sim or trigger secondary tasks when Awareness is enabled.
3D Overlay Orbit
GLB / GLTF / OBJ projected with excavator boom logic
function toLocal(worldPt) { const cx = overlay.posXFt, cy = overlay.posYFt; const rot = degToRadLocal(overlay.rotationDeg || 0); const dx = worldPt.xFt - cx; const dy = worldPt.yFt - cy; const c = Math.cos(-rot), s = Math.sin(-rot); return { x: dx * c - dy * s, y: dx * s + dy * c }; } function toWorld(local) { const cx = overlay.posXFt, cy = overlay.posYFt; const rot = degToRadLocal(overlay.rotationDeg || 0); const c = Math.cos(rot), s = Math.sin(rot); const x = local.x * c - local.y * s; const y = local.x * s + local.y * c; return { xFt: cx + x, yFt: cy + y }; } function handleLocalPositions(hx, hy) { const rotHandleOffsetFt = 6; return { nw: { x: -hx, y: -hy }, ne: { x: +hx, y: -hy }, se: { x: +hx, y: +hy }, sw: { x: -hx, y: +hy }, rot: { x: 0, y: -hy - rotHandleOffsetFt } }; } function hitTestOverlay(worldPt) { if (!overlay.enabled || !overlay.type) return null; const ext = overlayHalfExtentsFt(); if (!ext) return null; const { hx, hy } = ext; const tolPx = 12; const tolFt = tolPx / (view.pxPerFt * view.zoom); const local = toLocal(worldPt); const handles = handleLocalPositions(hx, hy); const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y); for (const key of ["nw", "ne", "se", "sw"]) { if (dist(local, handles[key]) <= tolFt) return { kind: "corner", handle: key, hx, hy }; } if (dist(local, handles.rot) <= tolFt) return { kind: "rotate", handle: "rot", hx, hy }; if (Math.abs(local.x) <= hx + tolFt && Math.abs(local.y) <= hy + tolFt) { return { kind: "inside", handle: "inside", hx, hy }; } return null; } // ------------------------------ // Gizmo renderer (drawn after normal redraw) // ------------------------------ function drawOverlayGizmo() { if (!state.canvasEdit) return; if (!overlay.enabled || !overlay.type) return; const ext = overlayHalfExtentsFt(); if (!ext) return; const { hx, hy } = ext; const canvas = document.getElementById("siteCanvas"); const ctx = canvas.getContext("2d"); const accent = cssVar("--accent-strong", "#ff6b8f"); const subtle = "rgba(255,255,255,0.55)"; const bg = "rgba(2,2,8,0.65)"; const corners = [ toWorld({ x: -hx, y: -hy }), toWorld({ x: +hx, y: -hy }), toWorld({ x: +hx, y: +hy }), toWorld({ x: -hx, y: +hy }) ]; const sp = corners.map(p => window.worldToScreenPx(p.xFt, p.yFt)); const center = window.worldToScreenPx(overlay.posXFt, overlay.posYFt); ctx.save(); ctx.lineWidth = 2; ctx.strokeStyle = state.selected ? accent : subtle; ctx.setLineDash(state.selected ? [] : [6, 4]); ctx.beginPath(); ctx.moveTo(sp[0].xPx, sp[0].yPx); for (let i = 1; i < sp.length; i++) ctx.lineTo(sp[i].xPx, sp[i].yPx); ctx.closePath(); ctx.stroke(); ctx.setLineDash([]); // center dot ctx.fillStyle = state.selected ? accent : subtle; ctx.beginPath(); ctx.arc(center.xPx, center.yPx, 4, 0, Math.PI * 2); ctx.fill(); // handles const handles = handleLocalPositions(hx, hy); const keys = ["nw", "ne", "se", "sw", "rot"]; const handleSize = 10; for (const k of keys) { const wp = toWorld(handles[k]); const hp = window.worldToScreenPx(wp.xFt, wp.yFt); ctx.fillStyle = bg; ctx.fillRect(hp.xPx - handleSize / 2, hp.yPx - handleSize / 2, handleSize, handleSize); ctx.strokeStyle = state.selected ? accent : subtle; ctx.lineWidth = 2; ctx.strokeRect(hp.xPx - handleSize / 2, hp.yPx - handleSize / 2, handleSize, handleSize); } // label ctx.font = `11px ${cssVar("--font-mono", "ui-monospace")}`; ctx.textBaseline = "bottom"; ctx.fillStyle = "rgba(249,251,255,0.92)"; ctx.fillText(`OVERLAY: ${overlay.name || overlay.type}`, center.xPx + 10, center.yPx - 10); ctx.restore(); } // Patch redrawCanvas to always draw the gizmo after the normal scene if (typeof window.redrawCanvas === "function") { const origRedraw = window.redrawCanvas; window.redrawCanvas = function(...args) { origRedraw.apply(this, args); try { drawOverlayGizmo(); } catch {} }; try { redrawCanvas = window.redrawCanvas; } catch {} } // ------------------------------ // Pointer interception (capture phase) // ------------------------------ const canvas = document.getElementById("siteCanvas"); function canvasLocalPx(evt) { const rect = canvas.getBoundingClientRect(); return { xPx: evt.clientX - rect.left, yPx: evt.clientY - rect.top }; } function screenToWorldFromEvent(evt) { const p = canvasLocalPx(evt); const w = window.screenToWorldFt(p.xPx, p.yPx); return { ...w, xPx: p.xPx, yPx: p.yPx }; } function startDrag(hit, pt, evt) { state.selected = true; state.pointerId = evt.pointerId; state.startPointerFt = { xFt: pt.xFt, yFt: pt.yFt }; state.startOverlay = { posXFt: overlay.posXFt, posYFt: overlay.posYFt, rotationDeg: overlay.rotationDeg || 0, imgWidthFt: overlay.img.widthFt, imgHeightFt: overlay.img.heightFt, modelScale: overlay.model.scaleFtPerUnit || 1, hx: hit.hx, hy: hit.hy, rotateRefRad: Math.atan2(pt.yFt - overlay.posYFt, pt.xFt - overlay.posXFt) }; if (hit.kind === "inside") state.dragMode = "move"; else if (hit.kind === "corner") { state.dragMode = "scale"; state.activeHandle = hit.handle; } else if (hit.kind === "rotate") { state.dragMode = "rotate"; state.activeHandle = "rot"; } canvas.setPointerCapture(evt.pointerId); redrawWithGizmo(); } function updateDrag(pt, evt) { if (!state.dragMode || state.pointerId !== evt.pointerId) return; if (!state.startPointerFt || !state.startOverlay) return; const base = state.startOverlay; const start = state.startPointerFt; if (state.dragMode === "move") { overlay.posXFt = base.posXFt + (pt.xFt - start.xFt); overlay.posYFt = base.posYFt + (pt.yFt - start.yFt); syncUIFromOverlay(); redrawWithGizmo(); return; } if (state.dragMode === "rotate") { const nowRad = Math.atan2(pt.yFt - base.posYFt, pt.xFt - base.posXFt); const deltaRad = nowRad - base.rotateRefRad; let deg = base.rotationDeg + radToDegLocal(deltaRad); // normalize (-180, 180] deg = ((deg + 180) % 360 + 360) % 360 - 180; if (evt.shiftKey && state.snapDeg > 0) { deg = Math.round(deg / state.snapDeg) * state.snapDeg; } overlay.rotationDeg = deg; syncUIFromOverlay(); redrawWithGizmo(); return; } if (state.dragMode === "scale") { const local = toLocal({ xFt: pt.xFt, yFt: pt.yFt }); const newHx = Math.max(0.5, Math.abs(local.x)); const newHy = Math.max(0.5, Math.abs(local.y)); const scaleFactor = Math.max(newHx / Math.max(0.5, base.hx), newHy / Math.max(0.5, base.hy)); if (overlay.type === "image") { if (state.lockAspect) { overlay.img.widthFt = Math.max(1, base.imgWidthFt * scaleFactor); overlay.img.heightFt = Math.max(1, base.imgHeightFt * scaleFactor); } else { overlay.img.widthFt = Math.max(1, newHx * 2); overlay.img.heightFt = Math.max(1, newHy * 2); } } else if (overlay.type === "model") { overlay.model.scaleFtPerUnit = Math.max(0.0001, base.modelScale * scaleFactor); } syncUIFromOverlay(); redrawWithGizmo(); return; } } function endDrag(evt) { if (state.pointerId !== evt.pointerId) return; state.dragMode = null; state.activeHandle = null; state.pointerId = null; state.startPointerFt = null; state.startOverlay = null; redrawWithGizmo(); } function onPointerDownCapture(evt) { if (!state.canvasEdit) return; if (!overlay.enabled || !overlay.type) return; const pt = screenToWorldFromEvent(evt); const hit = hitTestOverlay(pt); if (!hit) { // click outside overlay: deselect but allow normal tools if (state.selected) { state.selected = false; redrawWithGizmo(); } return; } evt.preventDefault(); evt.stopImmediatePropagation(); startDrag(hit, pt, evt); } function onPointerMoveCapture(evt) { if (!state.canvasEdit) return; if (!state.dragMode) return; const pt = screenToWorldFromEvent(evt); evt.preventDefault(); evt.stopImmediatePropagation(); updateDrag(pt, evt); } function onPointerUpCapture(evt) { if (!state.canvasEdit) return; if (!state.dragMode) return; evt.preventDefault(); evt.stopImmediatePropagation(); endDrag(evt); } function onWheelCapture(evt) { if (!state.canvasEdit) return; if (!state.selected) return; if (!overlay.enabled || !overlay.type) return; // ALT + wheel => scale overlay (doesn't steal normal map zoom unless ALT is pressed) if (!evt.altKey) return; evt.preventDefault(); evt.stopImmediatePropagation(); const factor = evt.deltaY > 0 ? 0.94 : 1.06; if (overlay.type === "image") { overlay.img.widthFt = Math.max(1, overlay.img.widthFt * factor); overlay.img.heightFt = Math.max(1, overlay.img.heightFt * factor); } else if (overlay.type === "model") { overlay.model.scaleFtPerUnit = Math.max(0.0001, overlay.model.scaleFtPerUnit * factor); } syncUIFromOverlay(); redrawWithGizmo(); } function onKeyDownCapture(evt) { if (!state.canvasEdit || !state.selected) return; // don't hijack typing const tag = (evt.target && evt.target.tagName) ? evt.target.tagName.toLowerCase() : ""; if (["input", "textarea", "select"].includes(tag)) return; const step = evt.shiftKey ? 2 : 0.5; if (evt.key === "Escape") { state.selected = false; redrawWithGizmo(); return; } if (evt.key.toLowerCase() === "r") { overlay.rotationDeg = 0; syncUIFromOverlay(); redrawWithGizmo(); evt.preventDefault(); return; } if (evt.key === "ArrowLeft") { overlay.posXFt -= step; syncUIFromOverlay(); redrawWithGizmo(); evt.preventDefault(); } else if (evt.key === "ArrowRight") { overlay.posXFt += step; syncUIFromOverlay(); redrawWithGizmo(); evt.preventDefault(); } else if (evt.key === "ArrowUp") { overlay.posYFt -= step; syncUIFromOverlay(); redrawWithGizmo(); evt.preventDefault(); } else if (evt.key === "ArrowDown") { overlay.posYFt += step; syncUIFromOverlay(); redrawWithGizmo(); evt.preventDefault(); } } if (canvas) { canvas.addEventListener("pointerdown", onPointerDownCapture, true); canvas.addEventListener("pointermove", onPointerMoveCapture, true); canvas.addEventListener("pointerup", onPointerUpCapture, true); canvas.addEventListener("pointercancel", onPointerUpCapture, true); canvas.addEventListener("wheel", onWheelCapture, { capture: true, passive: false }); } window.addEventListener("keydown", onKeyDownCapture, true); // Initial sync + repaint syncUIFromOverlay(); redrawWithGizmo(); if (typeof window.appendSimulationEvent === "function") { window.appendSimulationEvent("Overlay Tools Extension loaded (canvas edit + advanced model controls).", "info"); } })();