diff --git a/src/components/EditPlacementOverlay.tsx b/src/components/EditPlacementOverlay.tsx index ec584767..1d96984b 100644 --- a/src/components/EditPlacementOverlay.tsx +++ b/src/components/EditPlacementOverlay.tsx @@ -1,13 +1,15 @@ import type { AnyCircuitElement, PcbComponent } from "circuit-json" -import { useGlobalStore } from "../global-store" -import { useEffect, useRef, useState } from "react" +import { type State, useGlobalStore } from "../global-store" +import type { ReactNode } from "react" +import { useMemo, useRef, useState } from "react" import type { Matrix } from "transformation-matrix" import { applyToPoint, identity, inverse } from "transformation-matrix" import type { ManualEditEvent } from "@tscircuit/props" +import { shouldPickPcbComponent } from "../lib/should-pick-pcb-component" interface Props { transform?: Matrix - children: any + children: ReactNode soup: AnyCircuitElement[] disabled?: boolean cancelPanDrag: () => void @@ -15,6 +17,16 @@ interface Props { onModifyEditEvent: (event: Partial) => void } +const HIT_PADDING_PX = 10 + +const selectPlacementPickerState = (state: State) => ({ + inMoveFootprintMode: state.in_move_footprint_mode, + selectedLayer: state.selected_layer, + showTopComponents: state.is_showing_top_components, + showBottomComponents: state.is_showing_bottom_components, + setIsMovingComponent: state.setIsMovingComponent, +}) + const isInsideOf = ( pcb_component: PcbComponent, point: { x: number; y: number }, @@ -52,11 +64,30 @@ export const EditPlacementOverlay = ({ edit_event_id: string } | null>(null) const isPcbComponentActive = activePcbComponentId !== null - const in_edit_mode = useGlobalStore((s) => s.in_edit_mode) - const in_move_footprint_mode = useGlobalStore((s) => s.in_move_footprint_mode) - const setIsMovingComponent = useGlobalStore((s) => s.setIsMovingComponent) + const scale = Math.abs(transform.a) + const { + inMoveFootprintMode, + selectedLayer, + showTopComponents, + showBottomComponents, + setIsMovingComponent, + } = useGlobalStore(selectPlacementPickerState) + + const pickablePcbComponents = useMemo( + () => + soup.filter( + (element): element is PcbComponent => + element.type === "pcb_component" && + shouldPickPcbComponent(element, { + selectedLayer, + showTopComponents, + showBottomComponents, + }), + ), + [soup, selectedLayer, showTopComponents, showBottomComponents], + ) - const disabled = disabledProp || !in_move_footprint_mode + const disabled = disabledProp || !inMoveFootprintMode return (
{ + onMouseUp={() => { if (!activePcbComponentId) return setActivePcbComponent(null) setIsMovingComponent(false) @@ -154,33 +183,31 @@ export const EditPlacementOverlay = ({ > {children} {!disabled && - soup - .filter((e): e is PcbComponent => e.type === "pcb_component") - .map((e) => { - if (!e?.center) return null - const projectedCenter = applyToPoint(transform, e.center) + pickablePcbComponents.map((e) => { + if (!e?.center) return null + const projectedCenter = applyToPoint(transform, e.center) - return ( -
- ) - })} + return ( +
+ ) + })}
) } diff --git a/src/global-store.ts b/src/global-store.ts index 489febbf..aaccb812 100644 --- a/src/global-store.ts +++ b/src/global-store.ts @@ -36,6 +36,10 @@ export interface State { is_showing_solder_mask: boolean is_showing_silkscreen: boolean is_showing_fabrication_notes: boolean + // These defaults keep component picking aligned with any host app that + // exposes side-specific component visibility toggles. + is_showing_top_components: boolean + is_showing_bottom_components: boolean pcb_group_view_mode: "all" | "named_only" hovered_error_id: string | null @@ -62,9 +66,11 @@ export interface State { setFocusedErrorId: (errorId: string | null) => void } -export type StateProps = { - [key in keyof State]: State[key] extends boolean ? boolean : never -} +type BooleanStateKeys = { + [Key in keyof T]: Exclude extends boolean ? Key : never +}[keyof T] + +export type StateProps = Pick> const DEFAULT_PCB_GROUP_VIEW_MODE: "all" | "named_only" = process.env.NODE_ENV !== "production" ? "named_only" : "all" @@ -119,6 +125,8 @@ export const createStore = ( STORAGE_KEYS.IS_SHOWING_FABRICATION_NOTES, false, ), + is_showing_top_components: true, + is_showing_bottom_components: true, pcb_group_view_mode: disablePcbGroups ? "all" : (getStoredString( diff --git a/src/lib/should-pick-pcb-component.ts b/src/lib/should-pick-pcb-component.ts new file mode 100644 index 00000000..d852c3e9 --- /dev/null +++ b/src/lib/should-pick-pcb-component.ts @@ -0,0 +1,26 @@ +import type { LayerRef } from "circuit-json" + +interface PickabilityOptions { + selectedLayer: LayerRef + showTopComponents?: boolean + showBottomComponents?: boolean +} + +export const shouldPickPcbComponent = ( + component: { layer?: LayerRef | null }, + { + selectedLayer, + showTopComponents = true, + showBottomComponents = true, + }: PickabilityOptions, +) => { + if (component.layer !== "top" && component.layer !== "bottom") { + // Through-hole style components can be interacted with from either side. + return true + } + + if (component.layer === "top" && !showTopComponents) return false + if (component.layer === "bottom" && !showBottomComponents) return false + + return component.layer === selectedLayer +} diff --git a/tests/lib/should-pick-pcb-component.test.ts b/tests/lib/should-pick-pcb-component.test.ts new file mode 100644 index 00000000..8eaf45de --- /dev/null +++ b/tests/lib/should-pick-pcb-component.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "bun:test" +import { shouldPickPcbComponent } from "../../src/lib/should-pick-pcb-component" + +describe("shouldPickPcbComponent", () => { + it("allows a component on the selected side", () => { + expect( + shouldPickPcbComponent({ layer: "top" }, { selectedLayer: "top" }), + ).toBe(true) + }) + + it("blocks a component on the non-selected side", () => { + expect( + shouldPickPcbComponent( + { layer: "top" }, + { + selectedLayer: "bottom", + }, + ), + ).toBe(false) + }) + + it("blocks hidden top-side components even when top is selected", () => { + expect( + shouldPickPcbComponent( + { layer: "top" }, + { + selectedLayer: "top", + showTopComponents: false, + }, + ), + ).toBe(false) + }) + + it("blocks hidden bottom-side components even when bottom is selected", () => { + expect( + shouldPickPcbComponent( + { layer: "bottom" }, + { + selectedLayer: "bottom", + showBottomComponents: false, + }, + ), + ).toBe(false) + }) + + it("keeps layerless components pickable regardless of side filters", () => { + expect( + shouldPickPcbComponent( + { layer: undefined }, + { + selectedLayer: "inner1", + showTopComponents: false, + showBottomComponents: false, + }, + ), + ).toBe(true) + }) +})