@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.
- package/biome.json +5 -1
- package/bun.lockb +0 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +455 -25
- package/dist/index.js.map +1 -1
- package/docs/circuit-to-svg-metadata.md +151 -0
- package/docs/dragndrop-spec.md +39 -0
- package/examples/{resistor-and-capacitor.fixture.tsx → example1-resistor-and-capacitor.fixture.tsx} +3 -2
- package/examples/example2-small-circuit.fixture.tsx +45 -0
- package/examples/example3-small-circuit-without-debug-grid.fixture.tsx +44 -0
- package/examples/example4-reset-edit-events.fixture.tsx +55 -0
- package/lib/components/ControlledSchematicViewer.tsx +29 -0
- package/lib/components/EditIcon.tsx +37 -0
- package/lib/components/SchematicViewer.tsx +109 -44
- package/lib/hooks/use-resize-handling.ts +35 -0
- package/lib/hooks/useChangeSchematicComponentLocationsInSvg.ts +117 -0
- package/lib/hooks/useChangeSchematicTracesForMovedComponents.ts +121 -0
- package/lib/hooks/useComponentDragging.ts +163 -0
- package/lib/types/edit-events.ts +16 -0
- package/lib/utils/get-component-offset-due-to-events.ts +43 -0
- package/package.json +9 -6
|
@@ -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
|
+
```
|
package/examples/{resistor-and-capacitor.fixture.tsx → example1-resistor-and-capacitor.fixture.tsx}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
-
<
|
|
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 {
|
|
4
|
-
import {
|
|
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:
|
|
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 = ({
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}
|