From 3dcd2d681f398c24a17d9404948de5318ac187fb Mon Sep 17 00:00:00 2001 From: wangdequan Date: Tue, 1 Sep 2026 20:24:47 +0800 Subject: [PATCH] Fix repeated motion-id preview highlight --- web/react/src/XyzacToolPreview.tsx | 26 ++++++++++++++++++----- web/react/src/test_xyzac_tool_preview.mjs | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/web/react/src/XyzacToolPreview.tsx b/web/react/src/XyzacToolPreview.tsx index 0fe2340..182cc94 100644 --- a/web/react/src/XyzacToolPreview.tsx +++ b/web/react/src/XyzacToolPreview.tsx @@ -4,7 +4,7 @@ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { projectXyzacVisual, type XyzacVisualSource, type XyzacVisualState } from "./xyzac-visual-adapter"; type Point = Record; -type Segment = { id?: number; active?: boolean; kind?: string; motion_type?: number; kinematics_mode?: number; turn?: number; center?: Point; normal?: Point; samples?: Point[]; start?: Point; end?: Point }; +type Segment = { id?: number; source_line?: number | null; active?: boolean; kind?: string; motion_type?: number; kinematics_mode?: number; turn?: number; center?: Point; normal?: Point; samples?: Point[]; start?: Point; end?: Point }; type Props = { visual: XyzacVisualState; view?: "z" | "z2" | "x" | "y" | "y2" | "p"; zoom?: number; rotateMode?: boolean; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean }; const DEG = Math.PI / 180; @@ -82,13 +82,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa }; const completed = new THREE.LineBasicMaterial({ color: 0xd8dfe3, depthTest: false }); const rapid = new THREE.LineDashedMaterial({ color: 0xa85d5d, dashSize: .025, gapSize: .018, depthTest: false }); - const live = new THREE.LineBasicMaterial({ color: 0xffd166, depthTest: false }); + // LinuxCNC uses yellow for jog backplot. Coordinated program execution is + // red (live plot), while an unambiguous current program block is cyan. + const live = new THREE.LineBasicMaterial({ color: 0x00ffff, depthTest: false }); const liveHistory = new THREE.LineBasicMaterial({ color: 0xff6b4a, depthTest: false }); let frame = 0; let liveTick = -1; let liveTrace: THREE.Vector3[] = []; let pathSegments: Segment[] | null = null; - let segmentById = new Map(); + // Native TP active_id is the source line/motion id and may repeat (for + // example, each call of the same helix subroutine). Preserve every + // occurrence instead of letting a Map overwrite all but one geometry. + let segmentCandidatesBySourceLine = new Map(); let bounds = { center: new THREE.Vector3(), radius: .12 }; let playbackQueue: Array<{ tick: number; pose: Point; joints: number[]; activeSegmentId: number; activeProgress: number }> = []; let playbackStart = 0; @@ -203,7 +208,14 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa // segments on every animation frame causes visible execution stalls. if (segments !== pathSegments) { pathSegments = segments; - segmentById = new Map(segments.flatMap((segment) => Number.isSafeInteger(segment.id) ? [[Number(segment.id), segment] as [number, Segment]] : [])); + segmentCandidatesBySourceLine = new Map(); + for (const segment of segments) { + const sourceLine = Number(segment.source_line); + if (!Number.isSafeInteger(sourceLine) || sourceLine <= 0) continue; + const candidates = segmentCandidatesBySourceLine.get(sourceLine) ?? []; + candidates.push(segment); + segmentCandidatesBySourceLine.set(sourceLine, candidates); + } clearLines(pathGroup); clearLines(liveGroup); bounds = fit(segments); @@ -268,7 +280,11 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa const nextActiveKey = `${renderedTick}:${renderedActiveSegment}:${renderedProgress}`; if (nextActiveKey !== lastActiveKey) { if (activeLiveLine) { activeLiveLine.geometry.dispose(); liveGroup.remove(activeLiveLine); activeLiveLine = null; } - const activeSegment = segmentById.get(renderedActiveSegment) ?? segments.find((segment) => Boolean(segment.active)); + const candidates = segmentCandidatesBySourceLine.get(renderedActiveSegment) ?? []; + // A repeated source line has no unique preview geometry. LinuxCNC's + // current-line indicator remains authoritative in the program view; + // do not invent a yellow/cyan path by selecting an arbitrary repeat. + const activeSegment = candidates.length === 1 ? candidates[0] : null; if (activeSegment) { const start = activeSegment.start ?? {}; const end = activeSegment.end ?? {}; const points = Array.isArray(activeSegment.samples) && activeSegment.samples.length > 1 ? activeSegment.samples.map(sourcePoint) : [sourcePoint(start), sourcePoint(end)]; diff --git a/web/react/src/test_xyzac_tool_preview.mjs b/web/react/src/test_xyzac_tool_preview.mjs index 9560208..ae3ac17 100644 --- a/web/react/src/test_xyzac_tool_preview.mjs +++ b/web/react/src/test_xyzac_tool_preview.mjs @@ -16,7 +16,7 @@ assert.deepEqual(visual.pose, { x: 11, y: -4, z: 19, a: 31, b: 0, c: 44, u: 0, v assert.deepEqual(visual.livePose, visual.pose); assert.equal(visual.activeProgress, 0.375); assert.equal(visual.activeSegmentId, 3); -for (const token of ["xyzac-tool-preview", "xyzac-preview-toolpath", "xyzac-preview-live-toolpath", "xyzac-preview-tool", "xyzac-preview-tool-shaft", "xyzac-preview-tool-tip", "xyzac-origin-coordinate-system", "xyzac-origin-axis-${axis.label.toLowerCase()}", "xyzac-origin-label-${axis.label.toLowerCase()}", "OrbitControls", "enablePan", "enableRotate", "zoomIn", "panRight", "data-xyzac-controller=\"orbit\"", "data-xyzac-origin=\"0,0,0\"", "sourcePoint", "toolGroup.position.copy(pose)", "toolGroup.rotation.set(-Number(renderedPose.a ?? 0) * DEG, -Number(renderedPose.c ?? 0) * DEG, 0)", "const segments = state.toolpath as Segment[]", "segment.samples.map(sourcePoint)", "state.activeProgress", "state.livePose", "motionTrace", "playbackQueue", "data-xyzac-preview=\"toolpath\"", "data-xyzac-helix-count", "data-xyzac-sample-count", "data-xyzac-samples-source", "data-xyzac-live-source", "data-xyzac-live-pose", "preserveDrawingBuffer: true", "authoritative tool preview"]) assert.ok(source.includes(token), token); +for (const token of ["xyzac-tool-preview", "xyzac-preview-toolpath", "xyzac-preview-live-toolpath", "xyzac-preview-tool", "xyzac-preview-tool-shaft", "xyzac-preview-tool-tip", "xyzac-origin-coordinate-system", "xyzac-origin-axis-${axis.label.toLowerCase()}", "xyzac-origin-label-${axis.label.toLowerCase()}", "OrbitControls", "enablePan", "enableRotate", "zoomIn", "panRight", "data-xyzac-controller=\"orbit\"", "data-xyzac-origin=\"0,0,0\"", "sourcePoint", "toolGroup.position.copy(pose)", "toolGroup.rotation.set(-Number(renderedPose.a ?? 0) * DEG, -Number(renderedPose.c ?? 0) * DEG, 0)", "const segments = state.toolpath as Segment[]", "activeSegment.samples.map(sourcePoint)", "segmentCandidatesBySourceLine", "candidates.length === 1", "state.activeProgress", "state.livePose", "motionTrace", "playbackQueue", "data-xyzac-preview=\"toolpath\"", "data-xyzac-helix-count", "data-xyzac-sample-count", "data-xyzac-samples-source", "data-xyzac-live-source", "data-xyzac-live-pose", "preserveDrawingBuffer: true", "authoritative tool preview"]) assert.ok(source.includes(token), token); for (const forbidden of [/arcPoints/, /Math\.atan2/, /Quaternion/, /cross\(/]) assert.equal(forbidden.test(source), false, String(forbidden)); for (const forbidden of [/xyzac-machine/, /table-x/, /saddle-y/, /spindle-z/, /accept\s*\(/, /postMessage\s*\(/, /_sim_/, /_malloc/, /navigator\.storage/]) assert.equal(forbidden.test(source), false, String(forbidden)); console.log(JSON.stringify({ schema: "xyzac-tool-preview-rtcp-10b.v1", status: "verified", tick: visual.tick, state_hash: visual.stateHash, active_segment: visual.activeSegmentId, tool_pose: visual.pose, joints: visual.joints, machine_scene_nodes: 0, canonical_toolpath_segments: visual.toolpath.length, source: visual.source }));