Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/kiri-moto/CAM/ops.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ While the Trace op can do a lot, some specific use cases include:
- Outlining a part with a nonstandard offset
- Outlining only one part on the workspace

When selecting line segments for a Trace operation:
- Click a line segment to toggle its selection.
- **`Shift + click`** toggles all connected line segments at the same Z height.
- **`Ctrl + click`** (or **`Cmd + click`** on macOS) toggles all line segments at the same Z height.

### Pocket

<ImageCarousel base="/img/CAM/example/pocket/" images="pocket" />
Expand Down
155 changes: 132 additions & 23 deletions src/kiri/mode/cam/app/cl-trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,23 @@ export function traceAdd(ev) {
}
}

export let lastHoverData;

function onKeyChange(evt) {
// Re-evaluate hover highlight when Shift, Control, or Meta (macOS Command ⌘) modifier keys change
if (traceOn && lastHoverData && (evt.key === 'Shift' || evt.key === 'Control' || evt.key === 'Meta')) {
traceHover({ ...lastHoverData, event: evt });
}
}

window.addEventListener('keydown', onKeyChange);
window.addEventListener('keyup', onKeyChange);

export function traceDone() {
if (!traceOn) {
return;
}
lastHoverData = null;
env.func.unpop();
traceOn.classList.remove("editing");
traceOn = false;
Expand All @@ -158,42 +171,138 @@ export function traceDone() {
}

export function traceHover(data) {
// Un-highlight previously hovered trace(s)
if (lastTrace) {
let { color, colorSave } = lastTrace.material[0] || lastTrace.material;
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
let list = Array.isArray(lastTrace) ? lastTrace : [ lastTrace ];
for (let obj of list) {
let material = obj.material[0] || obj.material;
if (material) {
let { color, colorSave } = material;
if (colorSave) {
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
}
}
}
}
lastTrace = null;
if (data.type === 'platform') {

if (data.type === 'platform' || !data.int || !data.int.object || !data.int.object.trace) {
lastHoverData = null;
return;
}
if (!data.int.object.trace) {
return;

lastHoverData = data;
let object = data.int.object;
let ev = data.event || {};

// Determine target set of trace meshes to highlight based on modifier keys
let targets;
if (ev.shiftKey) {
targets = getConnectedTraces(object);
} else if (ev.metaKey || ev.ctrlKey) {
targets = getSameZTraces(object);
} else {
targets = [ object ];
}

lastTrace = targets;

// Apply hover highlight color to all target meshes
let hoverColorHex = isDark() ? 0x0066ff : 0x0000ff;
for (let obj of targets) {
let material = obj.material[0] || obj.material;
if (material) {
let color = material.color;
if (!material.colorSave) {
material.colorSave = color.clone();
}
color.setHex(hoverColorHex);
}
}
lastTrace = data.int.object;
if (lastTrace.selected) {
let event = data.event;
let target = event.target;
let { clientX, clientY } = event;
let { offsetWidth, offsetHeight } = target;
}
let material = lastTrace.material[0] || lastTrace.material;
let color = material.color;
material.colorSave = color.clone();
color.setHex(isDark() ? 0x0066ff : 0x0000ff);
}

/**
* Helper to find all connected trace objects at the same Z height as object
* @param {THREE.Mesh} object - target trace object
* @returns {THREE.Mesh[]} array of connected trace objects at the same Z height
*/
function getConnectedTraces(object) {
if (!object || !object.trace) return [ object ];
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
let candidates = widget.adds.filter(add => add.trace && add.trace.poly.onZ(avgZ));

// Build point-to-edges map for fast O(1) neighbor lookups
let pointToEdges = new Map();
function getPtKey(pt) {
return `${Math.round(pt.x * 100)},${Math.round(pt.y * 100)}`;
}
for (let add of candidates) {
for (let pt of add.trace.poly.points) {
let key = getPtKey(pt);
let list = pointToEdges.get(key);
if (!list) {
list = [];
pointToEdges.set(key, list);
}
list.push(add);
}
}

// BFS graph traversal to find all connected trace edges
let visited = new Set([ object ]);
let queue = [ object ];
let result = [ object ];
while (queue.length > 0) {
let curr = queue.shift();
for (let pt of curr.trace.poly.points) {
let key = getPtKey(pt);
let neighbors = pointToEdges.get(key);
if (neighbors) {
for (let neighbor of neighbors) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
result.push(neighbor);
}
}
}
}
}
return result;
}

/**
* Helper to find all trace objects at the same Z height as object
* @param {THREE.Mesh} object - target trace object
* @returns {THREE.Mesh[]} array of trace objects at the same Z height
*/
function getSameZTraces(object) {
if (!object || !object.trace) return [ object ];
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
return widget.adds.filter(add => add.trace && add.trace.poly.onZ(avgZ));
}

export function traceHoverUp(int, ev) {
if (!int) return;
let { object } = int;
traceToggle(object);
if (ev.metaKey || ev.ctrlKey) {
if (ev.shiftKey) {
// Shift+click: toggle all connected trace edges at the same Z height
let { selected } = object;
for (let add of getConnectedTraces(object)) {
if (add !== object && add.selected !== selected) {
traceToggle(add);
}
}
} else if (ev.metaKey || ev.ctrlKey) {
// Ctrl/Cmd+click: toggle all trace edges at the same Z height (connected or disconnected)
let { selected } = object;
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
for (let add of widget.adds) {
if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) {
for (let add of getSameZTraces(object)) {
if (add !== object && add.selected !== selected) {
traceToggle(add);
}
}
Expand Down