Compare commits
6 Commits
6e1781926e
...
2b1747041b
| Author | SHA1 | Date |
|---|---|---|
|
|
2b1747041b | |
|
|
35155e6597 | |
|
|
32e5e9c50b | |
|
|
13a8403d6d | |
|
|
f2e6464c36 | |
|
|
faed282135 |
|
|
@ -50,7 +50,7 @@ import {
|
|||
seatOnDrop,
|
||||
snapPositionToBreadboard,
|
||||
} from '../../utils/breadboardSnap';
|
||||
import { snapBoardToSocket } from '../../utils/socketSnap';
|
||||
import { snapBoardToSocket, isBoardSeated } from '../../utils/socketSnap';
|
||||
import {
|
||||
findWireNearPoint,
|
||||
findSegmentNearPoint,
|
||||
|
|
@ -138,8 +138,7 @@ function carrySeatedBoards(
|
|||
for (const b of st.boards) {
|
||||
// Restricting the candidate list to the dragged component asks the
|
||||
// narrow question: is this board seated on THIS socket?
|
||||
const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [dragged]);
|
||||
if (seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5) {
|
||||
if (isBoardSeated(b.id, b.boardKind, b.x, b.y, [dragged])) {
|
||||
st.setBoardPosition({ x: b.x + dx, y: b.y + dy }, b.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -371,6 +370,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
// Which drag session already raised its item (drag-to-front fires once per
|
||||
// drag, on the first mousemove).
|
||||
const raisedThisDragRef = useRef<string | null>(null);
|
||||
/**
|
||||
* The socket a dragged board was plugged into when the drag STARTED.
|
||||
* Latched once per drag: "is it seated?" is only true at the seat, so
|
||||
* re-asking after the first pixel of movement answers no and the stack
|
||||
* would come apart one frame in.
|
||||
*/
|
||||
const carriedSocketRef = useRef<{ dragId: string; sockId: string } | null>(null);
|
||||
// Captures (x, y) of the dragged component at mousedown so a drag-end
|
||||
// can record the diff as a single undoable Move. Boards are intentionally
|
||||
// skipped — board moves don't go through component history.
|
||||
|
|
@ -932,14 +938,22 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
const st = useSimulatorStore.getState();
|
||||
const b = st.boards.find((bb) => bb.id === boardId);
|
||||
// Same stack rule as the mouse path: while simulating, a seated
|
||||
// board drags its socket along instead of unplugging.
|
||||
const sock =
|
||||
st.running && b
|
||||
? st.components.find((c) => {
|
||||
const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]);
|
||||
return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5;
|
||||
})
|
||||
: undefined;
|
||||
// board drags its socket along instead of unplugging. Latched at
|
||||
// drag start for the same reason (see carriedSocketRef).
|
||||
if (carriedSocketRef.current?.dragId !== touchId) {
|
||||
// Ask the DRAGGED board whether it is running, not the store's
|
||||
// top-level flag — that one mirrors the ACTIVE board only, so a
|
||||
// running non-active board read as stopped and its stack came
|
||||
// apart mid-simulation.
|
||||
const boardRunning = !!(b && (b.running || st.running));
|
||||
const seatedOn =
|
||||
boardRunning && b
|
||||
? st.components.find((c) => isBoardSeated(b.id, b.boardKind, b.x, b.y, [c]))
|
||||
: undefined;
|
||||
carriedSocketRef.current = { dragId: touchId, sockId: seatedOn?.id ?? '' };
|
||||
}
|
||||
const sockId = carriedSocketRef.current.sockId;
|
||||
const sock = sockId ? st.components.find((c) => c.id === sockId) : undefined;
|
||||
if (b && sock) {
|
||||
updateComponent(sock.id, {
|
||||
x: sock.x + (nb.x - b.x),
|
||||
|
|
@ -1602,13 +1616,23 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
// While SIMULATING, a seated stack moves as one piece: dragging the
|
||||
// board drags its socket along instead of unplugging it. Unplugging
|
||||
// is an edit-mode gesture (carrySeatedBoards is the other half).
|
||||
const sock =
|
||||
st.running && b
|
||||
? st.components.find((c) => {
|
||||
const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]);
|
||||
return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5;
|
||||
})
|
||||
: undefined;
|
||||
if (carriedSocketRef.current?.dragId !== draggedComponentId) {
|
||||
// Ask the DRAGGED board whether it is running, not the store's
|
||||
// top-level flag — that one mirrors the ACTIVE board only, so a
|
||||
// running non-active board read as stopped and its stack came
|
||||
// apart mid-simulation.
|
||||
const boardRunning = !!(b && (b.running || st.running));
|
||||
const seatedOn =
|
||||
boardRunning && b
|
||||
? st.components.find((c) => isBoardSeated(b.id, b.boardKind, b.x, b.y, [c]))
|
||||
: undefined;
|
||||
carriedSocketRef.current = {
|
||||
dragId: draggedComponentId,
|
||||
sockId: seatedOn?.id ?? '',
|
||||
};
|
||||
}
|
||||
const sockId = carriedSocketRef.current.sockId;
|
||||
const sock = sockId ? st.components.find((c) => c.id === sockId) : undefined;
|
||||
if (b && sock) {
|
||||
updateComponent(sock.id, {
|
||||
x: sock.x + (nb.x - b.x),
|
||||
|
|
@ -1940,6 +1964,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
recalculateAllWirePositions();
|
||||
setDraggedComponentId(null);
|
||||
raisedThisDragRef.current = null;
|
||||
carriedSocketRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2911,6 +2936,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
setPan({ ...panRef.current });
|
||||
setDraggedComponentId(null);
|
||||
raisedThisDragRef.current = null;
|
||||
carriedSocketRef.current = null;
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -189,10 +189,10 @@ export const ONLINE_ONLY_COMPONENT_ADS: OnlineOnlyComponentAd[] = [
|
|||
},
|
||||
{
|
||||
id: 'grove-gesture-pag7660',
|
||||
label: 'Grove Smart IR Gesture (PAG7660)',
|
||||
description: 'IR gesture recognition (rotate/push/tap/...) - available in the online editor',
|
||||
label: 'Grove Smart IR Gesture (PAG7661QN)',
|
||||
description: 'IR camera gesture sensor on a XIAO carrier: seat a XIAO on its socket and read rotate/tap/grab/pinch/swipe over I2C - available in the online editor',
|
||||
category: 'input',
|
||||
thumbnailSvg: '<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect x="8" y="12" width="50" height="40" rx="6" fill="#1d3a5f"/><circle cx="36" cy="32" r="12" fill="#0c0d10" stroke="#3a3f49" stroke-width="2"/><circle cx="36" cy="32" r="6" fill="#1b1030"/></svg>',
|
||||
thumbnailSvg: '<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect x="17" y="4" width="30" height="56" rx="5" fill="#12518f" stroke="#0d3c6b"/><circle cx="34" cy="18" r="8" fill="#0a0b0e" stroke="#3a3f49"/><circle cx="34" cy="18" r="4" fill="#1b1030"/><rect x="21" y="30" width="7" height="24" rx="1.5" fill="#15171c"/><rect x="38" y="30" width="7" height="24" rx="1.5" fill="#15171c"/><rect x="8" y="12" width="8" height="14" rx="2" fill="#e8e8ec"/></svg>',
|
||||
},
|
||||
{
|
||||
id: 'respeaker-lite',
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import {
|
|||
collectWireSegments,
|
||||
} from '../utils/wireAutoRoute';
|
||||
import { isBreadboard } from '../utils/breadboardNets';
|
||||
import { snapBoardToSocket } from '../utils/socketSnap';
|
||||
import { isBoardSeated } from '../utils/socketSnap';
|
||||
import { computeSeating } from '../utils/breadboardSnap';
|
||||
import { createSerialBatcher } from './serialBatcher';
|
||||
import {
|
||||
|
|
@ -3169,16 +3169,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
// above — raising the socket alone buried its own seated board, and a
|
||||
// buried board cannot be grabbed to unplug it.
|
||||
const seatedOnIt = s.components.some((c) => c.id === id)
|
||||
? s.boards.filter((b) => {
|
||||
const seat = snapBoardToSocket(
|
||||
b.id,
|
||||
b.boardKind,
|
||||
b.x,
|
||||
b.y,
|
||||
s.components.filter((c) => c.id === id),
|
||||
);
|
||||
return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5;
|
||||
})
|
||||
? s.boards.filter((b) =>
|
||||
isBoardSeated(b.id, b.boardKind, b.x, b.y, s.components.filter((c) => c.id === id)),
|
||||
)
|
||||
: [];
|
||||
let top = s.zTop;
|
||||
const zOrders = { ...s.zOrders, [id]: ++top };
|
||||
|
|
|
|||
|
|
@ -95,12 +95,24 @@ export function snapBoardToSocket(
|
|||
}
|
||||
|
||||
/**
|
||||
* True when the board's CURRENT position IS a socket seat (within half a
|
||||
* pixel). This is the z-order question, not the drag question: a seated
|
||||
* board must paint above its socket component, an unseated one must stay
|
||||
* below components like every other board — the blanket zIndex bump that
|
||||
* preceded this check hid a resistor behind an Arduino in every ordinary
|
||||
* example.
|
||||
* How far off the exact seat a board may sit and still count as plugged in.
|
||||
* Not zero, and deliberately far below SOCKET_SNAP_TOLERANCE: a stack the
|
||||
* magnet built lands exact, but one an EXAMPLE declares (or a project saved
|
||||
* before a socket's art was nudged) can be a fraction of a pixel out. At the
|
||||
* old half-pixel bar such a board looked seated on screen while every seat
|
||||
* test said otherwise — so it got no electrical connection and its socket
|
||||
* did not travel with it. A couple of pixels is invisible to the eye and
|
||||
* still nowhere near the next hole.
|
||||
*/
|
||||
const SEATED_EPSILON = 2;
|
||||
|
||||
/**
|
||||
* True when the board's CURRENT position IS a socket seat. This is the
|
||||
* "is it plugged in?" question, asked by z-order (a seated board must paint
|
||||
* above its socket, an unseated one stays below components like every other
|
||||
* board — a blanket zIndex bump once hid a resistor behind an Arduino),
|
||||
* by the electrical hop that makes seating mean connection, and by the drag
|
||||
* rules that keep a plugged stack together.
|
||||
*/
|
||||
export function isBoardSeated(
|
||||
boardId: string,
|
||||
|
|
@ -110,5 +122,5 @@ export function isBoardSeated(
|
|||
components: ComponentLike[],
|
||||
): boolean {
|
||||
const seat = snapBoardToSocket(boardId, boardKind, x, y, components);
|
||||
return !!seat && Math.hypot(seat.x - x, seat.y - y) < 0.5;
|
||||
return !!seat && Math.hypot(seat.x - x, seat.y - y) <= SEATED_EPSILON;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue