Compare commits

..

6 Commits

Author SHA1 Message Date
a2nr 2b1747041b merge: sinkronkan upstream v3.2 ke elemes-v3.2
- ambil 4 fix upstream: socket latch saat drag, run-state board, toleransi
  posisi board di socket, koreksi kartu gesture sensor di picker
- pertahankan custom Elemes: EmbedBridge, DeployProgress, backend auth/projects,
  nginx.conf
- kembalikan EditorToolbar.tsx/.css + SimulatorCanvas.css (masih di-import
  oleh EditorPage/SimulatorCanvas; baseline sebelumnya broken build)
2026-08-01 14:07:51 +00:00
a2nr 35155e6597 chore: remove unused files 2026-08-01 07:01:17 +00:00
David Montero Crespo 32e5e9c50b ui(picker): the gesture sensor's card shows what it really is 2026-08-01 08:26:16 +02:00
David Montero Crespo 13a8403d6d fix(sockets): a board a hair off the seat is still plugged in 2026-08-01 06:14:43 +02:00
David Montero Crespo f2e6464c36 fix(canvas): the dragged board's own run state decides if the stack holds 2026-08-01 05:55:35 +02:00
David Montero Crespo faed282135 fix(canvas): latch the socket at drag start so the stack cannot come apart 2026-08-01 05:32:46 +02:00
4 changed files with 70 additions and 39 deletions

View File

@ -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();

View File

@ -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',

View File

@ -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 };

View File

@ -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;
}