@tscircuit/schematic-viewer 2.0.3 → 2.0.5

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,151 @@
1
+ # circuit-to-svg
2
+
3
+ Circuit to SVG is a library that's used for converting Circuit JSON into an SVG.
4
+
5
+ Circuit to SVG attaches metadata, classes and ids to the SVG elements to allow
6
+ for interaction.
7
+
8
+ ## Metadata
9
+
10
+ - `<g data-circuit-json-type="schematic_component" data-schematic-component-id="..."` - The id of the schematic component, the
11
+ group contains all the relevant elements for the component.
12
+ - `<g data-circuit-json-type="schematic_trace" data-schematic-trace-id="..."` - The id of the schematic trace, the
13
+ group contains all the relevant elements for the trace.
14
+
15
+ ## Example
16
+
17
+ Let's consider the following example:
18
+
19
+ ```tsx
20
+ const MyCircuit = () => (
21
+ <board width="10mm" height="10mm">
22
+ <resistor name="R1" resistance={1000} schX={-2} />
23
+ <capacitor name="C1" capacitance="1uF" schX={2} />
24
+ <trace from=".R1 .pin2" to=".C1 .pin1" />
25
+ </board>
26
+ )
27
+ ```
28
+
29
+ Will become the following SVG:
30
+
31
+ ```svg
32
+ <svg xmlns="http://www.w3.org/2000/svg" width="1728" height="421" style="background-color: rgb(245, 241, 237)" data-real-to-screen-transform="matrix(264.7769766545,0,0,-264.7769766545,861.6939778772,210.5)">
33
+ <style>
34
+ .boundary {
35
+ fill: rgb(245, 241, 237);
36
+ }
37
+
38
+ .schematic-boundary {
39
+ fill: none;
40
+ stroke: #fff;
41
+ }
42
+
43
+ .component {
44
+ fill: none;
45
+ stroke: rgb(132, 0, 0);
46
+ }
47
+
48
+ .chip {
49
+ fill: rgb(255, 255, 194);
50
+ stroke: rgb(132, 0, 0);
51
+ }
52
+
53
+ .component-pin {
54
+ fill: none;
55
+ stroke: rgb(132, 0, 0);
56
+ }
57
+
58
+ .trace:hover {
59
+ filter: invert(1);
60
+ }
61
+
62
+ .trace:hover .trace-crossing-outline {
63
+ opacity: 0;
64
+ }
65
+
66
+ .text {
67
+ font-family: sans-serif;
68
+ fill: rgb(0, 150, 0);
69
+ }
70
+
71
+ .pin-number {
72
+ fill: rgb(169, 0, 0);
73
+ }
74
+
75
+ .port-label {
76
+ fill: rgb(0, 100, 100);
77
+ }
78
+
79
+ .component-name {
80
+ fill: rgb(0, 100, 100);
81
+ }
82
+ </style>
83
+ <rect class="boundary" x="0" y="0" width="1728" height="421"></rect>
84
+ <g class="grid">
85
+ <line x1="-197.41392874079997" y1="421.0000000000242" x2="-197.41392874079997" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
86
+ <line x1="67.36304791370003" y1="421.0000000000242" x2="67.36304791370003" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
87
+ <line x1="332.14002456820003" y1="421.0000000000242" x2="332.14002456820003" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
88
+ <line x1="596.9170012227" y1="421.0000000000242" x2="596.9170012227" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
89
+ <line x1="861.6939778772" y1="421.0000000000242" x2="861.6939778772" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
90
+ <line x1="1126.4709545317" y1="421.0000000000242" x2="1126.4709545317" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
91
+ <line x1="1391.2479311862" y1="421.0000000000242" x2="1391.2479311862" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
92
+ <line x1="1656.0249078407" y1="421.0000000000242" x2="1656.0249078407" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
93
+ <line x1="1920.8018844952" y1="421.0000000000242" x2="1920.8018844952" y2="-2.4243718144134618e-11" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
94
+ <line x1="31.938350863210758" y1="475.2769766545" x2="1696.0616491367432" y2="475.2769766545" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
95
+ <line x1="31.938350863210758" y1="210.5" x2="1696.0616491367432" y2="210.5" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
96
+ <line x1="31.938350863210758" y1="-54.276976654500004" x2="1696.0616491367432" y2="-54.276976654500004" stroke="rgb(181, 181, 181)" stroke-width="2.647769766545" stroke-opacity="0.5"></line>
97
+ <text x="-199.91392874079997" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-4,-1</text>
98
+ <text x="-199.91392874079997" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-4,0</text>
99
+ <text x="-199.91392874079997" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-4,1</text>
100
+ <text x="64.86304791370003" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-3,-1</text>
101
+ <text x="64.86304791370003" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-3,0</text>
102
+ <text x="64.86304791370003" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-3,1</text>
103
+ <text x="329.64002456820003" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-2,-1</text>
104
+ <text x="329.64002456820003" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-2,0</text>
105
+ <text x="329.64002456820003" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-2,1</text>
106
+ <text x="594.4170012227" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-1,-1</text>
107
+ <text x="594.4170012227" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-1,0</text>
108
+ <text x="594.4170012227" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">-1,1</text>
109
+ <text x="859.1939778772" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">0,-1</text>
110
+ <text x="859.1939778772" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">0,0</text>
111
+ <text x="859.1939778772" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">0,1</text>
112
+ <text x="1123.9709545317" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">1,-1</text>
113
+ <text x="1123.9709545317" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">1,0</text>
114
+ <text x="1123.9709545317" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">1,1</text>
115
+ <text x="1388.7479311862" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">2,-1</text>
116
+ <text x="1388.7479311862" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">2,0</text>
117
+ <text x="1388.7479311862" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">2,1</text>
118
+ <text x="1653.5249078407" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">3,-1</text>
119
+ <text x="1653.5249078407" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">3,0</text>
120
+ <text x="1653.5249078407" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">3,1</text>
121
+ <text x="1918.3018844952" y="470.2769766545" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">4,-1</text>
122
+ <text x="1918.3018844952" y="205.5" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">4,0</text>
123
+ <text x="1918.3018844952" y="-59.276976654500004" fill="rgb(181, 181, 181)" font-size="52.955395330900004" fill-opacity="0.5" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">4,1</text>
124
+ </g>
125
+ <g data-circuit-json-type="schematic_component" data-schematic-component-id="schematic_component_0" style="">
126
+ <rect class="component-overlay" x="196.61371724601327" y="174.45583073493046" width="280.2222914667798" height="70.05559272496805" fill="transparent"></rect>
127
+ <path d="M 196.61371724601327 209.48362709741448 L 266.66928349328373 209.48362709741448" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
128
+ <path d="M 406.78041598782505 209.48362709741448 L 476.83600871279305 209.48362709741448" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
129
+ <path d="M 336.7248232628566 244.5114234598985 L 406.78038951012707 244.5114234598985 L 406.78038951012707 174.45583073493046 L 266.6692305378881 174.45583073493046 L 266.6692305378881 244.5114234598985 L 336.72482326285615 244.5114234598985" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
130
+ <text x="331.762770331863" y="263.45486577694686" dominant-baseline="hanging" text-anchor="middle" font-family="sans-serif" font-size="47.659855797809996px">R1</text>
131
+ <text x="332.0174857834045" y="131.06743655760317" dominant-baseline="auto" text-anchor="middle" font-family="sans-serif" font-size="47.659855797809996px">1kΩ</text>
132
+ <circle cx="190.80453685591092" cy="209.38931353832953" r="5.29553953309px" stroke-width="5.29553953309px" fill="none" stroke="rgb(132, 0, 0)"></circle>
133
+ <circle cx="473.4755122804894" cy="209.2446129205882" r="5.29553953309px" stroke-width="5.29553953309px" fill="none" stroke="rgb(132, 0, 0)"></circle>
134
+ </g>
135
+ <g data-circuit-json-type="schematic_component" data-schematic-component-id="schematic_component_1" style="">
136
+ <rect class="component-overlay" x="1249.4875293817756" y="131.78175188522167" width="280.22229146678" height="140.11113249454112" fill="transparent"></rect>
137
+ <path d="M 1529.7098208485556 201.83731813249176 L 1410.6153608759655 201.83731813249176" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
138
+ <path d="M 1368.5820158320637 201.83731813249176 L 1249.4875293817756 201.83731813249176" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
139
+ <path d="M 1410.6153608759655 271.8928843797628 L 1410.6153608759655 131.78175188522167" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
140
+ <path d="M 1368.5820158320637 271.8928843797628 L 1368.5820158320637 131.78175188522167" stroke="rgb(132, 0, 0)" fill="none" stroke-width="5.29553953309px"></path>
141
+ <text x="1387.8417077700283" y="308.4697849218618" dominant-baseline="hanging" text-anchor="middle" font-family="sans-serif" font-size="47.659855797809996px">C1</text>
142
+ <text x="1390.2284074375918" y="86.05251741268826" dominant-baseline="auto" text-anchor="middle" font-family="sans-serif" font-size="47.659855797809996px">1µF</text>
143
+ <circle cx="1245.3003992283566" cy="201.5983039556664" r="5.29553953309px" stroke-width="5.29553953309px" fill="none" stroke="rgb(132, 0, 0)"></circle>
144
+ <circle cx="1537.1954631440433" cy="201.74300457340775" r="5.29553953309px" stroke-width="5.29553953309px" fill="none" stroke="rgb(132, 0, 0)"></circle>
145
+ </g>
146
+ <g class="trace" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_0">
147
+ <path d="M 473.47551228048934 209.2446129205882 L 1205.5838527301817 209.2446129205882 L 1245.3003992283566 209.2446129205882 L 1245.3003992283566 201.59830395566638" class="trace-invisible-hover-outline" stroke="rgb(0, 150, 0)" fill="none" stroke-width="42.36431626472px" stroke-linecap="round" opacity="0" stroke-linejoin="round"></path>
148
+ <path d="M 473.47551228048934 209.2446129205882 L 1205.5838527301817 209.2446129205882 L 1245.3003992283566 209.2446129205882 L 1245.3003992283566 201.59830395566638" stroke="rgb(0, 150, 0)" fill="none" stroke-width="5.29553953309px" stroke-linecap="round" stroke-linejoin="round"></path>
149
+ </g>
150
+ </svg>
151
+ ```
@@ -0,0 +1,39 @@
1
+ # Drag and Drop Feature Specification
2
+
3
+ Drag'n'drop allows users to move schematic components inside the schematic
4
+ viewer.
5
+
6
+ It uses the "edit event architecture" to manage edits. Here's how it works:
7
+
8
+ - When the user starts dragging a component, there is an `activeEditEvent` that
9
+ specifies the component to be moved
10
+ - When the user releases the mouse button, the `activeEditEvent` is committed
11
+ via the `onEditEvent` callback
12
+ - The schematic viewer applies any edit events passed to it via the `editEvents`
13
+ prop
14
+
15
+ ## Types
16
+
17
+ The following is an excerpt from the types of the `@tscircuit/props` package,
18
+ which should be imported.
19
+
20
+ ```tsx
21
+ export interface BaseManualEditEvent {
22
+ edit_event_id: string
23
+ in_progress?: boolean
24
+ created_at: number
25
+ }
26
+
27
+ export interface EditSchematicComponentLocationEvent
28
+ extends BaseManualEditEvent {
29
+ edit_event_type: "edit_schematic_component_location"
30
+ schematic_component_id: string
31
+ original_center: { x: number; y: number }
32
+ new_center: { x: number; y: number }
33
+ }
34
+
35
+ export type ManualEditEvent =
36
+ | EditPcbComponentLocationEvent
37
+ | EditTraceHintEvent
38
+ | EditSchematicComponentLocationEvent
39
+ ```
@@ -1,8 +1,8 @@
1
- import { SchematicViewer } from "lib/components/SchematicViewer"
1
+ import { ControlledSchematicViewer } from "lib/components/ControlledSchematicViewer"
2
2
  import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
3
3
 
4
4
  export default () => (
5
- <SchematicViewer
5
+ <ControlledSchematicViewer
6
6
  circuitJson={renderToCircuitJson(
7
7
  <board width="10mm" height="10mm">
8
8
  <resistor name="R1" resistance={1000} schX={-2} />
@@ -11,5 +11,6 @@ export default () => (
11
11
  </board>,
12
12
  )}
13
13
  containerStyle={{ height: "100%" }}
14
+ debugGrid
14
15
  />
15
16
  )
@@ -0,0 +1,45 @@
1
+ import { ControlledSchematicViewer } from "lib/components/ControlledSchematicViewer"
2
+ import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
3
+
4
+ export default () => (
5
+ <ControlledSchematicViewer
6
+ circuitJson={renderToCircuitJson(
7
+ <board width="10mm" height="10mm">
8
+ <resistor name="R1" resistance={1000} schX={-2} />
9
+ <capacitor name="C1" capacitance="1uF" schX={2} schY={2} />
10
+ <capacitor
11
+ name="C2"
12
+ schRotation={90}
13
+ capacitance="1uF"
14
+ schX={0}
15
+ schY={-4}
16
+ />
17
+ <chip
18
+ name="U1"
19
+ pinLabels={{
20
+ pin1: "D0",
21
+ pin2: "D1",
22
+ pin3: "D2",
23
+ pin4: "GND",
24
+ pin5: "D3",
25
+ pin6: "EN",
26
+ pin7: "D4",
27
+ pin8: "VCC",
28
+ }}
29
+ footprint="soic8"
30
+ schX={0}
31
+ schY={-1.5}
32
+ />
33
+
34
+ <trace from=".R1 .pin2" to=".C1 .pin1" />
35
+ <trace from=".C1 .pin2" to=".U1 .pin4" />
36
+ <trace from=".U1 .pin8" to=".C2 .pin1" />
37
+ <trace from=".C2 .pin2" to=".R1 .pin1" />
38
+ <trace from=".U1 .pin1" to=".U1 .pin5" />
39
+ </board>,
40
+ )}
41
+ editingEnabled
42
+ containerStyle={{ height: "100%" }}
43
+ debugGrid
44
+ />
45
+ )
@@ -0,0 +1,44 @@
1
+ import { ControlledSchematicViewer } from "lib/components/ControlledSchematicViewer"
2
+ import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
3
+
4
+ export default () => (
5
+ <ControlledSchematicViewer
6
+ circuitJson={renderToCircuitJson(
7
+ <board width="10mm" height="10mm">
8
+ <resistor name="R1" resistance={1000} schX={-2} />
9
+ <capacitor name="C1" capacitance="1uF" schX={2} schY={2} />
10
+ <capacitor
11
+ name="C2"
12
+ schRotation={90}
13
+ capacitance="1uF"
14
+ schX={0}
15
+ schY={-4}
16
+ />
17
+ <chip
18
+ name="U1"
19
+ pinLabels={{
20
+ pin1: "D0",
21
+ pin2: "D1",
22
+ pin3: "D2",
23
+ pin4: "GND",
24
+ pin5: "D3",
25
+ pin6: "EN",
26
+ pin7: "D4",
27
+ pin8: "VCC",
28
+ }}
29
+ footprint="soic8"
30
+ schX={0}
31
+ schY={-1.5}
32
+ />
33
+
34
+ <trace from=".R1 .pin2" to=".C1 .pin1" />
35
+ <trace from=".C1 .pin2" to=".U1 .pin4" />
36
+ <trace from=".U1 .pin8" to=".C2 .pin1" />
37
+ <trace from=".C2 .pin2" to=".R1 .pin1" />
38
+ <trace from=".U1 .pin1" to=".U1 .pin5" />
39
+ </board>,
40
+ )}
41
+ editingEnabled
42
+ containerStyle={{ height: "100%" }}
43
+ />
44
+ )
@@ -0,0 +1,55 @@
1
+ import { useState } from "react"
2
+ import { ControlledSchematicViewer } from "lib/components/ControlledSchematicViewer"
3
+ import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
4
+ import type { ManualEditEvent } from "lib/types/edit-events"
5
+ import { SchematicViewer } from "lib/index"
6
+
7
+ export default () => {
8
+ const [editEvents, setEditEvents] = useState<ManualEditEvent[]>([])
9
+
10
+ return (
11
+ <div style={{ position: "relative", height: "100%" }}>
12
+ <button
13
+ type="button"
14
+ onClick={() => setEditEvents([])}
15
+ style={{
16
+ position: "absolute",
17
+ top: "16px",
18
+ right: "64px",
19
+ zIndex: 1001,
20
+ backgroundColor: "#f44336",
21
+ color: "#fff",
22
+ padding: "8px",
23
+ borderRadius: "4px",
24
+ cursor: "pointer",
25
+ border: "none",
26
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
27
+ }}
28
+ >
29
+ Reset Edits
30
+ </button>
31
+ <SchematicViewer
32
+ editEvents={editEvents}
33
+ onEditEvent={(event) => setEditEvents([...editEvents, event])}
34
+ circuitJson={renderToCircuitJson(
35
+ <board width="10mm" height="10mm">
36
+ <resistor name="R1" resistance={1000} schX={-2} />
37
+ <capacitor name="C1" capacitance="1uF" schX={2} schY={2} />
38
+ <capacitor
39
+ name="C2"
40
+ schRotation={90}
41
+ capacitance="1uF"
42
+ schX={0}
43
+ schY={-4}
44
+ />
45
+ <trace from=".R1 .pin2" to=".C1 .pin1" />
46
+ <trace from=".C1 .pin2" to=".C2 .pin1" />
47
+ </board>,
48
+ )}
49
+ containerStyle={{ height: "100%" }}
50
+ debugGrid
51
+ editingEnabled
52
+ />
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,29 @@
1
+ import { useState } from "react"
2
+ import { SchematicViewer } from "./SchematicViewer"
3
+ import type { ManualEditEvent } from "@tscircuit/props"
4
+ import type { CircuitJson } from "circuit-json"
5
+
6
+ export const ControlledSchematicViewer = ({
7
+ circuitJson,
8
+ containerStyle,
9
+ debugGrid = false,
10
+ editingEnabled = false,
11
+ }: {
12
+ circuitJson: CircuitJson
13
+ containerStyle?: React.CSSProperties
14
+ debugGrid?: boolean
15
+ editingEnabled?: boolean
16
+ }) => {
17
+ const [editEvents, setEditEvents] = useState<ManualEditEvent[]>([])
18
+
19
+ return (
20
+ <SchematicViewer
21
+ circuitJson={circuitJson}
22
+ editEvents={editEvents}
23
+ onEditEvent={(event) => setEditEvents([...editEvents, event])}
24
+ containerStyle={containerStyle}
25
+ debugGrid={debugGrid}
26
+ editingEnabled={editingEnabled}
27
+ />
28
+ )
29
+ }
@@ -0,0 +1,37 @@
1
+ export const EditIcon = ({
2
+ onClick,
3
+ active,
4
+ }: { onClick: () => void; active: boolean }) => {
5
+ return (
6
+ <div
7
+ onClick={onClick}
8
+ style={{
9
+ position: "absolute",
10
+ top: "16px",
11
+ right: "16px",
12
+ backgroundColor: active ? "#4CAF50" : "#fff",
13
+ color: active ? "#fff" : "#000",
14
+ padding: "8px",
15
+ borderRadius: "4px",
16
+ cursor: "pointer",
17
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
18
+ display: "flex",
19
+ alignItems: "center",
20
+ gap: "4px",
21
+ zIndex: 1000,
22
+ }}
23
+ >
24
+ <svg
25
+ width="16"
26
+ height="16"
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ strokeWidth="2"
31
+ >
32
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
33
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
34
+ </svg>
35
+ </div>
36
+ )
37
+ }
@@ -1,79 +1,144 @@
1
1
  import { useMouseMatrixTransform } from "use-mouse-matrix-transform"
2
2
  import { convertCircuitJsonToSchematicSvg } from "circuit-to-svg"
3
- import { useEffect, useMemo, useRef, useState } from "react"
4
- import { toString as transformToString } from "transformation-matrix"
3
+ import { useMemo, useRef, useState } from "react"
4
+ import { EditIcon } from "./EditIcon"
5
+ import { useResizeHandling } from "../hooks/use-resize-handling"
6
+ import { useComponentDragging } from "../hooks/useComponentDragging"
7
+ import type { ManualEditEvent } from "../types/edit-events"
8
+ import {
9
+ identity,
10
+ fromString,
11
+ toString as transformToString,
12
+ } from "transformation-matrix"
13
+ import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
14
+ import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
15
+ import type { CircuitJson } from "circuit-json"
5
16
 
6
17
  interface Props {
7
- circuitJson: Array<{ type: string }>
18
+ circuitJson: CircuitJson
8
19
  containerStyle?: React.CSSProperties
20
+ editEvents?: ManualEditEvent[]
21
+ onEditEvent?: (event: ManualEditEvent) => void
22
+ defaultEditMode?: boolean
23
+ debugGrid?: boolean
24
+ editingEnabled?: boolean
9
25
  }
10
26
 
11
- export const SchematicViewer = ({ circuitJson, containerStyle }: Props) => {
27
+ export const SchematicViewer = ({
28
+ circuitJson,
29
+ containerStyle,
30
+ editEvents = [],
31
+ onEditEvent,
32
+ defaultEditMode = false,
33
+ debugGrid = false,
34
+ editingEnabled = false,
35
+ }: Props) => {
36
+ const [editModeEnabled, setEditModeEnabled] = useState(defaultEditMode)
12
37
  const svgDivRef = useRef<HTMLDivElement>(null)
13
- const { ref: containerRef } = useMouseMatrixTransform({
38
+
39
+ const {
40
+ ref: containerRef,
41
+ cancelDrag,
42
+ transform: svgToScreenProjection,
43
+ } = useMouseMatrixTransform({
14
44
  onSetTransform(transform) {
15
45
  if (!svgDivRef.current) return
16
46
  svgDivRef.current.style.transform = transformToString(transform)
17
47
  },
18
48
  })
19
- const [containerWidth, setContainerWidth] = useState(0)
20
- const [containerHeight, setContainerHeight] = useState(0)
21
-
22
- useEffect(() => {
23
- if (!containerRef.current) return
24
-
25
- const updateDimensions = () => {
26
- const rect = containerRef.current?.getBoundingClientRect()
27
-
28
- setContainerWidth(rect?.width || 0)
29
- setContainerHeight(rect?.height || 0)
30
- }
31
-
32
- // Set initial dimensions
33
- updateDimensions()
34
-
35
- // Add resize listener
36
- const resizeObserver = new ResizeObserver(updateDimensions)
37
- resizeObserver.observe(containerRef.current)
38
-
39
- // Fallback to window resize
40
- window.addEventListener("resize", updateDimensions)
41
-
42
- return () => {
43
- resizeObserver.disconnect()
44
- window.removeEventListener("resize", updateDimensions)
45
- }
46
- }, [])
47
49
 
48
- const svg = useMemo(() => {
50
+ const { containerWidth, containerHeight } = useResizeHandling(containerRef)
51
+ const svgString = useMemo(() => {
49
52
  if (!containerWidth || !containerHeight) return ""
50
53
 
51
54
  return convertCircuitJsonToSchematicSvg(circuitJson as any, {
52
55
  width: containerWidth,
53
56
  height: containerHeight || 720,
57
+ grid: !debugGrid
58
+ ? undefined
59
+ : {
60
+ cellSize: 1,
61
+ labelCells: true,
62
+ },
54
63
  })
55
64
  }, [circuitJson, containerWidth, containerHeight])
56
65
 
66
+ const realToSvgProjection = useMemo(() => {
67
+ if (!svgString) return identity()
68
+ const transformString = svgString.match(
69
+ /data-real-to-screen-transform="([^"]+)"/,
70
+ )?.[1]!
71
+
72
+ try {
73
+ return fromString(transformString)
74
+ } catch (e) {
75
+ console.error(e)
76
+ return identity()
77
+ }
78
+ }, [svgString])
79
+
80
+ const { handleMouseDown, isDragging, activeEditEvent } = useComponentDragging(
81
+ {
82
+ onEditEvent,
83
+ cancelDrag,
84
+ realToSvgProjection,
85
+ svgToScreenProjection,
86
+ circuitJson,
87
+ editEvents,
88
+ enabled: editModeEnabled,
89
+ },
90
+ )
91
+
92
+ useChangeSchematicComponentLocationsInSvg({
93
+ svgDivRef,
94
+ editEvents,
95
+ realToSvgProjection,
96
+ svgToScreenProjection,
97
+ activeEditEvent,
98
+ })
99
+
100
+ useChangeSchematicTracesForMovedComponents({
101
+ svgDivRef,
102
+ circuitJson,
103
+ activeEditEvent,
104
+ editEvents,
105
+ })
106
+
107
+ const svgDiv = useMemo(
108
+ () => (
109
+ <div
110
+ ref={svgDivRef}
111
+ style={{
112
+ pointerEvents: "auto",
113
+ transformOrigin: "0 0",
114
+ }}
115
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
116
+ dangerouslySetInnerHTML={{ __html: svgString }}
117
+ />
118
+ ),
119
+ [svgString],
120
+ )
121
+
57
122
  return (
58
123
  <div
59
124
  ref={containerRef}
60
125
  style={{
126
+ position: "relative",
61
127
  backgroundColor: "#F5F1ED",
62
128
  overflow: "hidden",
63
- cursor: "grab",
129
+ cursor: isDragging ? "grabbing" : "grab",
64
130
  minHeight: "300px",
65
131
  ...containerStyle,
66
132
  }}
133
+ onMouseDown={handleMouseDown}
67
134
  >
68
- <div
69
- ref={svgDivRef}
70
- style={{
71
- pointerEvents: "auto",
72
- transformOrigin: "0 0",
73
- }}
74
- // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
75
- dangerouslySetInnerHTML={{ __html: svg }}
76
- />
135
+ {editingEnabled && (
136
+ <EditIcon
137
+ active={editModeEnabled}
138
+ onClick={() => setEditModeEnabled(!editModeEnabled)}
139
+ />
140
+ )}
141
+ {svgDiv}
77
142
  </div>
78
143
  )
79
144
  }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export const useResizeHandling = (
4
+ containerRef: React.RefObject<HTMLElement>,
5
+ ) => {
6
+ const [containerWidth, setContainerWidth] = useState(0)
7
+ const [containerHeight, setContainerHeight] = useState(0)
8
+
9
+ useEffect(() => {
10
+ if (!containerRef.current) return
11
+
12
+ const updateDimensions = () => {
13
+ const rect = containerRef.current?.getBoundingClientRect()
14
+ setContainerWidth(rect?.width || 0)
15
+ setContainerHeight(rect?.height || 0)
16
+ }
17
+
18
+ // Set initial dimensions
19
+ updateDimensions()
20
+
21
+ // Add resize listener
22
+ const resizeObserver = new ResizeObserver(updateDimensions)
23
+ resizeObserver.observe(containerRef.current)
24
+
25
+ // Fallback to window resize
26
+ window.addEventListener("resize", updateDimensions)
27
+
28
+ return () => {
29
+ resizeObserver.disconnect()
30
+ window.removeEventListener("resize", updateDimensions)
31
+ }
32
+ }, [])
33
+
34
+ return { containerWidth, containerHeight }
35
+ }