@tscircuit/schematic-viewer 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ import { su } from "@tscircuit/soup-util"
2
+ import type {
3
+ ManualEditEvent,
4
+ EditSchematicComponentLocationEventWithElement,
5
+ } from "lib/types/edit-events"
6
+ import { type Matrix, compose, applyToPoint } from "transformation-matrix"
7
+ import { useEffect, useRef } from "react"
8
+ import { getComponentOffsetDueToEvents } from "lib/utils/get-component-offset-due-to-events"
9
+ import type { CircuitJson } from "circuit-json"
10
+
11
+ /**
12
+ * This hook automatically applies the edit events to the schematic components
13
+ * inside the svg div.
14
+ *
15
+ * Schematic components are "<g>" elements with a "data-circuit-json-type"
16
+ * attribute equal to "schematic_component", these elements also have a
17
+ * data-schematic-component-id attribute equal to the schematic_component_id
18
+ */
19
+ export const useChangeSchematicComponentLocationsInSvg = ({
20
+ svgDivRef,
21
+ realToSvgProjection,
22
+ svgToScreenProjection,
23
+ activeEditEvent,
24
+ editEvents,
25
+ }: {
26
+ svgDivRef: React.RefObject<HTMLDivElement | null>
27
+ realToSvgProjection: Matrix
28
+ svgToScreenProjection: Matrix
29
+ activeEditEvent: EditSchematicComponentLocationEventWithElement | null
30
+ editEvents: ManualEditEvent[]
31
+ }) => {
32
+ // Keep track of the last known SVG content
33
+ const lastSvgContentRef = useRef<string | null>(null)
34
+
35
+ useEffect(() => {
36
+ const svg = svgDivRef.current
37
+ if (!svg) return
38
+
39
+ // Create a MutationObserver to watch for changes in the div's content
40
+ const observer = new MutationObserver((mutations) => {
41
+ // Check if the SVG content has changed
42
+ const currentSvgContent = svg.innerHTML
43
+ if (currentSvgContent !== lastSvgContentRef.current) {
44
+ lastSvgContentRef.current = currentSvgContent
45
+
46
+ // Apply the transforms
47
+ applyTransforms()
48
+ }
49
+ })
50
+
51
+ // Function to apply transforms to components
52
+ const applyTransforms = () => {
53
+ const componentsThatHaveBeenMoved = new Set<string>()
54
+ for (const event of editEvents) {
55
+ if (
56
+ "edit_event_type" in event &&
57
+ event.edit_event_type === "edit_schematic_component_location"
58
+ ) {
59
+ componentsThatHaveBeenMoved.add(event.schematic_component_id)
60
+ }
61
+ }
62
+ if (activeEditEvent) {
63
+ componentsThatHaveBeenMoved.add(activeEditEvent.schematic_component_id)
64
+ }
65
+
66
+ // Reset all transforms
67
+ const allComponents = svg.querySelectorAll(
68
+ '[data-circuit-json-type="schematic_component"]',
69
+ )
70
+
71
+ for (const component of Array.from(allComponents)) {
72
+ const schematic_component_id = component.getAttribute(
73
+ "data-schematic-component-id",
74
+ )!
75
+
76
+ const offsetMm = getComponentOffsetDueToEvents({
77
+ editEvents: [
78
+ ...editEvents,
79
+ ...(activeEditEvent ? [activeEditEvent] : []),
80
+ ],
81
+ schematic_component_id,
82
+ })
83
+
84
+ const offsetPx = {
85
+ x: offsetMm.x * realToSvgProjection.a,
86
+ y: offsetMm.y * realToSvgProjection.d,
87
+ }
88
+
89
+ const style: any = (component as any).style
90
+ style.transform = `translate(${offsetPx.x}px, ${offsetPx.y}px)`
91
+ if (
92
+ activeEditEvent?.schematic_component_id === schematic_component_id
93
+ ) {
94
+ style.outline = "solid 2px rgba(255,0,0,0.5)"
95
+ style.outlineOffset = "5px"
96
+ } else if (style.outline) {
97
+ style.outline = ""
98
+ }
99
+ }
100
+ }
101
+
102
+ // Start observing the div for changes
103
+ observer.observe(svg, {
104
+ childList: true, // Watch for changes to the child elements
105
+ subtree: false, // Watch for changes in the entire subtree
106
+ characterData: false, // Watch for changes to text content
107
+ })
108
+
109
+ // Apply transforms immediately on mount or when editEvents change
110
+ applyTransforms()
111
+
112
+ // Cleanup function
113
+ return () => {
114
+ observer.disconnect()
115
+ }
116
+ }, [svgDivRef, editEvents, activeEditEvent]) // Dependencies remain the same
117
+ }
@@ -0,0 +1,121 @@
1
+ import { useEffect, useRef } from "react"
2
+ import { su } from "@tscircuit/soup-util"
3
+ import type { ManualEditEvent } from "../types/edit-events"
4
+ import type { CircuitJson } from "circuit-json"
5
+
6
+ /**
7
+ * This hook makes traces dashed when their connected components are being moved
8
+ */
9
+ export const useChangeSchematicTracesForMovedComponents = ({
10
+ svgDivRef,
11
+ circuitJson,
12
+ activeEditEvent,
13
+ editEvents,
14
+ }: {
15
+ svgDivRef: React.RefObject<HTMLDivElement | null>
16
+ circuitJson: CircuitJson
17
+ activeEditEvent: ManualEditEvent | null
18
+ editEvents: ManualEditEvent[]
19
+ }) => {
20
+ // Keep track of the last known SVG content
21
+ const lastSvgContentRef = useRef<string | null>(null)
22
+
23
+ useEffect(() => {
24
+ const svg = svgDivRef.current
25
+ if (!svg) return
26
+
27
+ const updateTraceStyles = () => {
28
+ // Reset all traces to solid
29
+ const allTraces = svg.querySelectorAll(
30
+ '[data-circuit-json-type="schematic_trace"] path',
31
+ )
32
+
33
+ // Reset all traces to solid
34
+ for (const trace of Array.from(allTraces)) {
35
+ trace.setAttribute("stroke-dasharray", "0")
36
+ ;(trace as any).style.animation = ""
37
+ }
38
+
39
+ // If there's an active edit event, make connected traces dashed
40
+ for (const editEvent of [
41
+ ...editEvents,
42
+ ...(activeEditEvent ? [activeEditEvent] : []),
43
+ ]) {
44
+ if (
45
+ "schematic_component_id" in editEvent &&
46
+ editEvent.edit_event_type === "edit_schematic_component_location"
47
+ ) {
48
+ const sch_component = su(circuitJson).schematic_component.get(
49
+ editEvent.schematic_component_id,
50
+ )
51
+ if (!sch_component) return
52
+
53
+ const src_ports = su(circuitJson).source_port.list({
54
+ source_component_id: sch_component.source_component_id,
55
+ })
56
+ const src_port_ids = new Set(src_ports.map((sp) => sp.source_port_id))
57
+ const src_traces = su(circuitJson)
58
+ .source_trace.list()
59
+ .filter((st) =>
60
+ st.connected_source_port_ids?.some((spi) =>
61
+ src_port_ids.has(spi),
62
+ ),
63
+ )
64
+ const src_trace_ids = new Set(
65
+ src_traces.map((st) => st.source_trace_id),
66
+ )
67
+ const schematic_traces = su(circuitJson)
68
+ .schematic_trace.list()
69
+ .filter((st) => src_trace_ids.has(st.source_trace_id))
70
+
71
+ // Make the connected traces dashed
72
+ schematic_traces.forEach((trace) => {
73
+ const traceElements = svg.querySelectorAll(
74
+ `[data-schematic-trace-id="${trace.schematic_trace_id}"] path`,
75
+ )
76
+ for (const traceElement of Array.from(traceElements)) {
77
+ if (traceElement.getAttribute("class")?.includes("invisible"))
78
+ continue
79
+ traceElement.setAttribute("stroke-dasharray", "20,20")
80
+ ;(traceElement as any).style.animation =
81
+ "dash-animation 350ms linear infinite, pulse-animation 900ms linear infinite"
82
+
83
+ if (!svg.querySelector("style#dash-animation")) {
84
+ const style = document.createElement("style")
85
+ style.id = "dash-animation"
86
+ style.textContent = `
87
+ @keyframes dash-animation {
88
+ to {
89
+ stroke-dashoffset: -40;
90
+ }
91
+ }
92
+ @keyframes pulse-animation {
93
+ 0% { opacity: 0.6; }
94
+ 50% { opacity: 0.2; }
95
+ 100% { opacity: 0.6; }
96
+ }
97
+ `
98
+ svg.appendChild(style)
99
+ }
100
+ }
101
+ })
102
+ }
103
+ }
104
+ }
105
+
106
+ // Apply styles immediately
107
+ updateTraceStyles()
108
+
109
+ // Cleanup function
110
+ const observer = new MutationObserver(updateTraceStyles)
111
+ observer.observe(svg, {
112
+ childList: true, // Watch for changes to the child elements
113
+ subtree: false, // Watch for changes in the entire subtree
114
+ characterData: false, // Watch for changes to text content
115
+ })
116
+
117
+ return () => {
118
+ observer.disconnect()
119
+ }
120
+ }, [svgDivRef, activeEditEvent, circuitJson, editEvents])
121
+ }
@@ -0,0 +1,163 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+ import type {
3
+ EditSchematicComponentLocationEventWithElement,
4
+ ManualEditEvent,
5
+ } from "../types/edit-events"
6
+ import {
7
+ type Matrix,
8
+ applyToPoint,
9
+ inverse,
10
+ compose,
11
+ } from "transformation-matrix"
12
+ import { getComponentOffsetDueToEvents } from "lib/utils/get-component-offset-due-to-events"
13
+ import type { CircuitJson } from "circuit-json"
14
+ import { su } from "@tscircuit/soup-util"
15
+
16
+ export const useComponentDragging = ({
17
+ onEditEvent,
18
+ editEvents = [],
19
+ circuitJson,
20
+ cancelDrag,
21
+ svgToScreenProjection,
22
+ realToSvgProjection,
23
+ enabled = false,
24
+ }: {
25
+ circuitJson: any[]
26
+ editEvents: ManualEditEvent[]
27
+ /** The projection returned from use-mouse-matrix-transform, indicating zoom on svg */
28
+ svgToScreenProjection: Matrix
29
+ /** The projection returned from circuit-to-svg, mm to svg */
30
+ realToSvgProjection: Matrix
31
+ onEditEvent?: (event: ManualEditEvent) => void
32
+ cancelDrag?: () => void
33
+ enabled?: boolean
34
+ }): {
35
+ handleMouseDown: (e: React.MouseEvent) => void
36
+ isDragging: boolean
37
+ activeEditEvent: EditSchematicComponentLocationEventWithElement | null
38
+ } => {
39
+ const [activeEditEvent, setActiveEditEvent] =
40
+ useState<EditSchematicComponentLocationEventWithElement | null>(null)
41
+ const realToScreenProjection = compose(
42
+ realToSvgProjection,
43
+ svgToScreenProjection,
44
+ )
45
+
46
+ /**
47
+ * Drag start position in screen space
48
+ */
49
+ const dragStartPosRef = useRef<{
50
+ x: number
51
+ y: number
52
+ } | null>(null)
53
+
54
+ const activeEditEventRef =
55
+ useRef<EditSchematicComponentLocationEventWithElement | null>(null)
56
+
57
+ const handleMouseDown = useCallback(
58
+ (e: React.MouseEvent) => {
59
+ if (!enabled) return
60
+
61
+ const target = e.target as SVGElement
62
+ const componentGroup = target.closest(
63
+ '[data-circuit-json-type="schematic_component"]',
64
+ )
65
+ if (!componentGroup) return
66
+
67
+ const schematic_component_id = componentGroup.getAttribute(
68
+ "data-schematic-component-id",
69
+ )
70
+ if (!schematic_component_id) return
71
+
72
+ if (cancelDrag) cancelDrag()
73
+
74
+ const schematic_component = su(circuitJson).schematic_component.get(
75
+ schematic_component_id,
76
+ )
77
+ if (!schematic_component) return
78
+ const editEventOffset = getComponentOffsetDueToEvents({
79
+ editEvents,
80
+ schematic_component_id: schematic_component_id,
81
+ })
82
+
83
+ dragStartPosRef.current = {
84
+ x: e.clientX,
85
+ y: e.clientY,
86
+ }
87
+
88
+ const original_center = {
89
+ x: schematic_component.center.x + editEventOffset.x,
90
+ y: schematic_component.center.y + editEventOffset.y,
91
+ }
92
+
93
+ const newEditEvent: EditSchematicComponentLocationEventWithElement = {
94
+ edit_event_id: Math.random().toString(36).substr(2, 9),
95
+ edit_event_type: "edit_schematic_component_location",
96
+ schematic_component_id: schematic_component_id,
97
+ original_center,
98
+ new_center: { ...original_center },
99
+ in_progress: true,
100
+ created_at: Date.now(),
101
+ _element: componentGroup as any,
102
+ }
103
+
104
+ activeEditEventRef.current = newEditEvent
105
+ },
106
+ [cancelDrag, enabled],
107
+ )
108
+
109
+ const handleMouseMove = useCallback(
110
+ (e: MouseEvent) => {
111
+ if (!activeEditEventRef.current || !dragStartPosRef.current) return
112
+
113
+ const screenDelta = {
114
+ x: e.clientX - dragStartPosRef.current.x,
115
+ y: e.clientY - dragStartPosRef.current.y,
116
+ }
117
+
118
+ const mmDelta = {
119
+ x: screenDelta.x / realToScreenProjection.a,
120
+ y: screenDelta.y / realToScreenProjection.d,
121
+ }
122
+
123
+ const newEditEvent = {
124
+ ...activeEditEventRef.current,
125
+ new_center: {
126
+ x: activeEditEventRef.current.original_center.x + mmDelta.x,
127
+ y: activeEditEventRef.current.original_center.y + mmDelta.y,
128
+ },
129
+ }
130
+
131
+ activeEditEventRef.current = newEditEvent
132
+ setActiveEditEvent(newEditEvent)
133
+ },
134
+ [realToScreenProjection],
135
+ )
136
+
137
+ const handleMouseUp = useCallback(() => {
138
+ if (!activeEditEventRef.current) return
139
+ const finalEvent = {
140
+ ...activeEditEventRef.current,
141
+ in_progress: false,
142
+ }
143
+ if (onEditEvent) onEditEvent(finalEvent)
144
+ activeEditEventRef.current = null
145
+ dragStartPosRef.current = null
146
+ setActiveEditEvent(null)
147
+ }, [onEditEvent])
148
+
149
+ useEffect(() => {
150
+ window.addEventListener("mousemove", handleMouseMove)
151
+ window.addEventListener("mouseup", handleMouseUp)
152
+ return () => {
153
+ window.removeEventListener("mousemove", handleMouseMove)
154
+ window.removeEventListener("mouseup", handleMouseUp)
155
+ }
156
+ }, [handleMouseMove, handleMouseUp])
157
+
158
+ return {
159
+ handleMouseDown,
160
+ isDragging: !!activeEditEventRef.current,
161
+ activeEditEvent: activeEditEvent,
162
+ }
163
+ }
@@ -0,0 +1,16 @@
1
+ import type {
2
+ BaseManualEditEvent,
3
+ EditSchematicComponentLocationEvent,
4
+ ManualEditEvent,
5
+ } from "@tscircuit/props"
6
+
7
+ export type EditSchematicComponentLocationEventWithElement =
8
+ EditSchematicComponentLocationEvent & {
9
+ _element: SVGElement
10
+ }
11
+
12
+ export type {
13
+ BaseManualEditEvent,
14
+ EditSchematicComponentLocationEvent,
15
+ ManualEditEvent,
16
+ }
@@ -0,0 +1,43 @@
1
+ import type {
2
+ EditSchematicComponentLocationEvent,
3
+ EditSchematicComponentLocationEventWithElement,
4
+ ManualEditEvent,
5
+ } from "lib/types/edit-events"
6
+
7
+ /**
8
+ * Returns the total offset of a component due to a set of edit events in
9
+ * mm
10
+ */
11
+ export const getComponentOffsetDueToEvents = ({
12
+ editEvents,
13
+ schematic_component_id,
14
+ }: {
15
+ editEvents: ManualEditEvent[]
16
+ schematic_component_id: string
17
+ }) => {
18
+ const editEventsForComponent: EditSchematicComponentLocationEvent[] =
19
+ editEvents
20
+ .filter(
21
+ (event) =>
22
+ "schematic_component_id" in event &&
23
+ event.schematic_component_id === schematic_component_id,
24
+ )
25
+ .filter(
26
+ (event) =>
27
+ "edit_event_type" in event &&
28
+ event.edit_event_type === "edit_schematic_component_location",
29
+ )
30
+
31
+ const totalOffsetX = editEventsForComponent.reduce((acc, event) => {
32
+ return acc + event.new_center.x - event.original_center.x
33
+ }, 0)
34
+
35
+ const totalOffsetY = editEventsForComponent.reduce((acc, event) => {
36
+ return acc + event.new_center.y - event.original_center.y
37
+ }, 0)
38
+
39
+ return {
40
+ x: totalOffsetX,
41
+ y: totalOffsetY,
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,22 +1,24 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
8
8
  "build": "tsup-node ./lib/index.ts --dts --format esm --sourcemap",
9
9
  "build:site": "cosmos-export",
10
- "vercel-build": "bun run build:site"
10
+ "vercel-build": "bun run build:site",
11
+ "format": "biome format --write .",
12
+ "format:check": "biome format ."
11
13
  },
12
14
  "devDependencies": {
13
15
  "@biomejs/biome": "^1.9.4",
14
- "@tscircuit/core": "^0.0.222",
16
+ "@tscircuit/core": "^0.0.236",
15
17
  "@types/bun": "latest",
16
18
  "@types/react": "^19.0.1",
17
19
  "@types/react-dom": "^19.0.2",
18
20
  "@vitejs/plugin-react": "^4.3.4",
19
- "circuit-json": "^0.0.111",
21
+ "circuit-json": "^0.0.117",
20
22
  "react": "18",
21
23
  "react-cosmos": "^6.2.1",
22
24
  "react-cosmos-plugin-vite": "^6.2.0",
@@ -29,9 +31,10 @@
29
31
  "typescript": "^5.0.0"
30
32
  },
31
33
  "dependencies": {
32
- "circuit-to-svg": "^0.0.90",
34
+ "@tscircuit/props": "^0.0.113",
35
+ "circuit-to-svg": "^0.0.96",
33
36
  "debug": "^4.4.0",
34
37
  "performance-now": "^2.1.0",
35
- "use-mouse-matrix-transform": "^1.1.13"
38
+ "use-mouse-matrix-transform": "^1.2.2"
36
39
  }
37
40
  }