star-build-plan.html raw
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>否 The Star — Fabrication Plan</title>
7 <style>
8 @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap');
9
10 :root {
11 --bg: #0a0a0c;
12 --fg: #c8c4b8;
13 --accent: #e8d5a3;
14 --dim: #555;
15 --fire: #c44040;
16 --wood: #4a9e4a;
17 --earth: #c4a040;
18 --metal: #a0a0a8;
19 --water: #4060c4;
20 --panel-bg: rgba(12,12,14,0.95);
21 }
22
23 * { margin: 0; padding: 0; box-sizing: border-box; }
24
25 body {
26 background: var(--bg);
27 color: var(--fg);
28 font-family: 'EB Garamond', serif;
29 font-size: 17px;
30 line-height: 1.7;
31 }
32
33 .viewer-wrap {
34 position: relative;
35 width: 100%;
36 height: 70vh;
37 min-height: 500px;
38 border-bottom: 1px solid #1a1a1a;
39 }
40
41 #canvas3d { display: block; width: 100%; height: 100%; }
42
43 .viewer-label {
44 position: absolute;
45 bottom: 12px; left: 16px;
46 font-family: 'JetBrains Mono', monospace;
47 font-size: 10px;
48 color: var(--dim);
49 letter-spacing: .12em;
50 text-transform: uppercase;
51 }
52
53 .viewer-controls {
54 position: absolute;
55 top: 12px; right: 16px;
56 display: flex; gap: 6px;
57 }
58
59 .viewer-controls button {
60 background: rgba(30,30,34,0.8);
61 border: 1px solid #2a2a2e;
62 color: var(--accent);
63 font-family: 'JetBrains Mono', monospace;
64 font-size: 10px;
65 padding: 4px 10px;
66 cursor: pointer;
67 text-transform: uppercase;
68 letter-spacing: .08em;
69 }
70
71 .viewer-controls button:hover { background: rgba(50,50,55,0.9); }
72 .viewer-controls button.active { background: rgba(232,213,163,0.15); border-color: var(--accent); }
73
74 main {
75 max-width: 780px;
76 margin: 0 auto;
77 padding: 60px 24px 120px;
78 }
79
80 h1 {
81 font-family: 'JetBrains Mono', monospace;
82 font-weight: 600;
83 font-size: 13px;
84 letter-spacing: .2em;
85 text-transform: uppercase;
86 color: var(--accent);
87 border-bottom: 1px solid #2a2218;
88 padding-bottom: 10px;
89 margin-bottom: 40px;
90 }
91
92 h2 {
93 font-family: 'JetBrains Mono', monospace;
94 font-weight: 600;
95 font-size: 11px;
96 letter-spacing: .16em;
97 text-transform: uppercase;
98 color: var(--accent);
99 margin-top: 50px;
100 margin-bottom: 16px;
101 }
102
103 h3 {
104 font-family: 'JetBrains Mono', monospace;
105 font-weight: 400;
106 font-size: 10px;
107 letter-spacing: .12em;
108 text-transform: uppercase;
109 color: var(--dim);
110 margin-top: 30px;
111 margin-bottom: 10px;
112 }
113
114 p { margin-bottom: 14px; }
115
116 .spec-grid {
117 display: grid;
118 grid-template-columns: 160px 1fr;
119 gap: 2px 20px;
120 font-family: 'JetBrains Mono', monospace;
121 font-size: 12px;
122 margin: 16px 0 24px;
123 }
124
125 .spec-grid .label { color: var(--dim); }
126 .spec-grid .value { color: var(--fg); }
127
128 .element-row {
129 display: grid;
130 grid-template-columns: 20px 60px 50px 1fr;
131 gap: 8px;
132 font-family: 'JetBrains Mono', monospace;
133 font-size: 12px;
134 padding: 4px 0;
135 align-items: center;
136 }
137
138 .element-dot {
139 width: 10px; height: 10px;
140 border-radius: 50%;
141 display: inline-block;
142 }
143
144 .bom-table {
145 width: 100%;
146 border-collapse: collapse;
147 font-family: 'JetBrains Mono', monospace;
148 font-size: 12px;
149 margin: 16px 0 24px;
150 }
151
152 .bom-table th {
153 text-align: left;
154 color: var(--dim);
155 font-weight: 400;
156 font-size: 10px;
157 text-transform: uppercase;
158 letter-spacing: .1em;
159 border-bottom: 1px solid #2a2a2e;
160 padding: 6px 8px;
161 }
162
163 .bom-table td {
164 padding: 6px 8px;
165 border-bottom: 1px solid #151518;
166 }
167
168 .bom-table .cost { text-align: right; color: var(--accent); }
169 .bom-table .subtotal { border-top: 1px solid var(--accent); font-weight: 600; }
170
171 .step {
172 counter-increment: step-counter;
173 padding-left: 36px;
174 position: relative;
175 margin-bottom: 20px;
176 }
177
178 .step::before {
179 content: counter(step-counter);
180 position: absolute;
181 left: 0; top: 2px;
182 font-family: 'JetBrains Mono', monospace;
183 font-size: 11px;
184 color: var(--accent);
185 width: 24px;
186 text-align: right;
187 }
188
189 .steps { counter-reset: step-counter; }
190
191 .warn {
192 border-left: 2px solid var(--fire);
193 padding-left: 14px;
194 margin: 16px 0;
195 color: #d09090;
196 font-size: 15px;
197 }
198
199 .note {
200 border-left: 2px solid var(--water);
201 padding-left: 14px;
202 margin: 16px 0;
203 font-size: 15px;
204 color: #90a0d0;
205 }
206
207 .test-result {
208 display: grid;
209 grid-template-columns: 100px 1fr;
210 gap: 8px 16px;
211 font-family: 'JetBrains Mono', monospace;
212 font-size: 12px;
213 margin: 12px 0;
214 }
215
216 .test-result .outcome { color: var(--accent); }
217
218 .phase-label {
219 display: inline-block;
220 font-family: 'JetBrains Mono', monospace;
221 font-size: 10px;
222 letter-spacing: .1em;
223 text-transform: uppercase;
224 padding: 2px 8px;
225 border: 1px solid;
226 margin-right: 8px;
227 }
228
229 .phase-glow { border-color: var(--fire); color: var(--fire); }
230 .phase-emf { border-color: var(--water); color: var(--water); }
231 </style>
232 </head>
233 <body>
234
235 <!-- 3D Viewer -->
236 <div class="viewer-wrap">
237 <canvas id="canvas3d"></canvas>
238 <div class="viewer-label">Drag to rotate · Scroll to zoom · Click nodes for element info</div>
239 <div class="viewer-controls">
240 <button id="btn-core" class="active">Core</button>
241 <button id="btn-jig">Jig</button>
242 <button id="btn-wired">Wired</button>
243 <button id="btn-all">Assembly</button>
244 </div>
245 </div>
246
247 <!-- Plan Content -->
248 <main>
249
250 <h1>否 The Star — Ferrite-Epoxy Prototype Fabrication Plan</h1>
251
252 <p>A continuous-topology ferrite core in Wu Xing knot geometry, wound with dual-pitch coils following the shēng and kè cycles, energized by Fibonacci-ratio supercapacitor banks at each node. Test objective: anomalous voltage multiplication and neon ionization at the geometric center.</p>
253
254 <h2>Geometry</h2>
255
256 <div class="spec-grid">
257 <span class="label">Pentagon circumradius</span><span class="value">R = 50.0 mm</span>
258 <span class="label">Edge length</span><span class="value">s = 58.8 mm</span>
259 <span class="label">Optimal height</span><span class="value">h = s√φ = 74.8 mm</span>
260 <span class="label">Bar diameter</span><span class="value">8.0 mm (4.0 mm radius)</span>
261 <span class="label">Node diameter</span><span class="value">16.0 mm</span>
262 <span class="label">Shēng bar length</span><span class="value">95.1 mm (51.83° from horizontal)</span>
263 <span class="label">Kè bar length</span><span class="value">121.0 mm (38.17° from horizontal)</span>
264 <span class="label">Bar ratio kè/shēng</span><span class="value">√φ = 1.2720</span>
265 <span class="label">Angle sum</span><span class="value">51.83° + 38.17° = 90.00°</span>
266 <span class="label">Overall envelope</span><span class="value">~108 × 108 × 91 mm</span>
267 </div>
268
269 <h3>Node Assignments</h3>
270
271 <div class="element-row"><span class="element-dot" style="background:var(--wood)"></span><span>Wood 木</span><span>1 cap</span><span>Bottom center (shēng source)</span></div>
272 <div class="element-row"><span class="element-dot" style="background:var(--fire)"></span><span>Fire 火</span><span>2 caps</span><span>Right</span></div>
273 <div class="element-row"><span class="element-dot" style="background:var(--earth)"></span><span>Earth 土</span><span>3 caps</span><span>Upper right</span></div>
274 <div class="element-row"><span class="element-dot" style="background:var(--metal)"></span><span>Metal 金</span><span>5 caps</span><span>Upper left</span></div>
275 <div class="element-row"><span class="element-dot" style="background:var(--water)"></span><span>Water 水</span><span>8 caps</span><span>Left</span></div>
276
277 <p>Total: 19 identical supercapacitors. 19 is a Lucas number — L(7). The ratio between adjacent node capacitances converges on φ from alternating sides at every step of the shēng cycle. Sum of all node capacitances = 19, which is also the number of steady-state microamps in the pair-breaker model. Possibly a coincidence. Possibly a penguin wearing a monocle.</p>
278
279 <h2>Bill of Materials</h2>
280
281 <table class="bom-table">
282 <tr><th>Item</th><th>Qty</th><th>Source</th><th class="cost">Est. €</th></tr>
283 <tr><td>FDM 3D printer (Creality Ender 3 V3 SE or similar, 220×220×250mm, heated bed, prints PVA at 190-210°C)</td><td>1</td><td>Amazon / 3DJake / local</td><td class="cost">150</td></tr>
284 <tr><td>PVA filament (1.75mm, water-soluble)</td><td>~50g</td><td>Amazon/3DJake</td><td class="cost">5</td></tr>
285 <tr><td>PLA filament (for test jig)</td><td>~30g</td><td>existing stock</td><td class="cost">0</td></tr>
286 <tr><td>Plaster of Paris (1kg bag)</td><td>1</td><td>Pevex</td><td class="cost">3</td></tr>
287 <tr><td>Dead ferrite cores (CRT/SMPS salvage)</td><td>5-10</td><td>e-waste / local recycler</td><td class="cost">0</td></tr>
288 <tr><td>Two-part epoxy (slow cure, 30min+)</td><td>100ml</td><td>Pevex</td><td class="cost">6</td></tr>
289 <tr><td>Enameled copper wire 0.5mm</td><td>20m</td><td>electronics shop / Chipoteka</td><td class="cost">4</td></tr>
290 <tr><td>NE-2 neon indicator lamps</td><td>5</td><td>Chipoteka / eBay</td><td class="cost">2</td></tr>
291 <tr><td>1F 5.5V supercapacitors (identical)</td><td>19</td><td>AliExpress / Chipoteka</td><td class="cost">8</td></tr>
292 <tr><td>3.6V lithium coin cell (CR2032 holder + cell)</td><td>1</td><td>Pevex</td><td class="cost">2</td></tr>
293 <tr><td>SPST toggle switch (mini)</td><td>1</td><td>Chipoteka</td><td class="cost">1</td></tr>
294 <tr><td>1N4148 signal diode</td><td>1</td><td>Chipoteka</td><td class="cost">0.20</td></tr>
295 <tr><td>1kΩ resistor</td><td>1</td><td>Chipoteka</td><td class="cost">0.10</td></tr>
296 <tr><td>Thin hookup wire (various colors)</td><td>2m</td><td>Chipoteka</td><td class="cost">1</td></tr>
297 <tr><td>Beeswax or paraffin (tube sealing)</td><td>small</td><td>existing / candle</td><td class="cost">0</td></tr>
298 <tr><td colspan="3" class="subtotal">Total estimated (consumables only)</td><td class="cost subtotal">~€32</td></tr>
299 <tr><td colspan="3" class="subtotal">Total with printer</td><td class="cost subtotal">~€182</td></tr>
300 </table>
301
302 <p class="note">The 1N4148 diode goes between battery and circuit — allows the CR2032 to seed but not sink current. Once the caps begin charging, the battery is isolated. The 1kΩ resistor is the parallel load that lets voltage build across the caps rather than being absorbed by the cell's low impedance.</p>
303
304 <h2>Construction Sequence</h2>
305
306 <div class="steps">
307
308 <div class="step">
309 <strong>Print the core form in PVA.</strong> Use the star_core.stl file. Layer height 0.2mm, 100% infill (solid — it needs to hold shape during plaster casting). Print with enclosed chamber or low humidity. Bag with silica gel immediately after print completes.
310 </div>
311
312 <div class="step">
313 <strong>Print the test jig in PLA.</strong> Use the test_jig.stl file. Same print settings. The five socket posts align exactly with the core's node positions. The central column holds the neon tube at the geometric center of the knot. This is the permanent test fixture.
314 </div>
315
316 <div class="step">
317 <strong>Cast plaster mold.</strong> Mix plaster of Paris to thick cream consistency. Suspend the PVA form (hanging from a wire through one bar) inside a container with 15mm clearance on all sides. Pour plaster around it. Allow to cure 24 hours minimum. The plaster expands ~0.5% as it sets, pressing into every detail.
318 </div>
319
320 <div class="step">
321 <strong>Dissolve the PVA.</strong> Submerge the plaster block in warm water (40-50°C). The PVA softens and dissolves over 4-8 hours. Agitate periodically. Change water if it gets viscous. The result: a plaster block with a precise negative of the star core geometry inside it, accessible through the bar openings.
322 </div>
323
324 <div class="step">
325 <strong>Prepare ferrite-epoxy slurry.</strong> Crush salvaged ferrite cores: wrap in cloth, break with hammer, then refine in a jar with steel ball bearings on a drill (improvised ball mill, run 2+ hours). Target particle size: fine powder, the finer the better. Mix with slow-cure epoxy at maximum loading — roughly 80% ferrite powder by volume, 20% epoxy. Add ferrite until the mixture is thick but still pourable/injectable. You have 30 minutes working time with slow-cure epoxy.
326 </div>
327
328 <div class="step">
329 <strong>Fill the mold.</strong> Inject ferrite-epoxy slurry into the plaster channels through the exposed bar openings. Use a syringe for precision. Vibrate the mold during filling — tape a phone running a vibration app to the mold, or place it on a speaker playing 50Hz. This prevents air traps. Fill from the lowest point, let slurry rise through the geometry. Overfill slightly.
330 </div>
331
332 <div class="step">
333 <strong>Cure.</strong> Allow epoxy to fully cure per manufacturer instructions (typically 24 hours at room temperature, longer is better for maximum hardness). Do not heat-cure — differential expansion between plaster and ferrite-epoxy may crack the mold prematurely.
334 </div>
335
336 <div class="step">
337 <strong>Demold.</strong> Crack the plaster off with a chisel and light hammer. Start at edges, work inward. Plaster fractures cleanly. For tight spots around the knot geometry, soak briefly in dilute vinegar — plaster dissolves, ferrite-epoxy does not. Clean all plaster residue from the core surface.
338 </div>
339
340 <div class="step">
341 <strong>Wind the shēng coil.</strong> Using 0.5mm enameled copper wire, wind uniformly along each of the five pentagon-edge bars. Close-wound: each turn touching the next, no visible gap. Wind continuously through all five shēng bars without cutting — one continuous coil following the generating cycle. Leave 50mm lead tails at start and end.
342 </div>
343
344 <div class="step">
345 <strong>Wind the kè coil.</strong> Same wire, same pitch — close-wound, each turn touching the next. Wound along each of the five pentagram-diagonal bars. One continuous coil following the overcoming cycle. The inductance ratio between the two cycles comes from the bar length ratio (√φ), not from different winding densities. Same wire, same pitch, different bar lengths = different inductance. Leave 50mm lead tails.
346 </div>
347
348 <div class="step">
349 <strong>Solder cap banks.</strong> At each node, connect the specified number of identical 1F supercapacitors in parallel: Wood=1, Fire=2, Earth=3, Metal=5, Water=8. Wire each bank to the coil junction points at that node. All caps oriented with the same polarity relative to the coil direction.
350 </div>
351
352 <div class="step">
353 <strong>Wire the test circuit.</strong> Select one bar's node pair as the battery/switch connection point (preferably the Wood node — smallest capacitance, fastest initial response). Connect: CR2032 holder → 1N4148 diode (anode to battery +) → SPST switch → one coil terminal at the Wood node. Return path from the other coil terminal at Wood back to battery −, with the 1kΩ resistor in parallel across the battery.
354 </div>
355
356 <div class="step">
357 <strong>Seat the core in the jig.</strong> Place the finished core (with coils and caps attached) into the PLA test jig. The five lower nodes drop into the socket cups. The core should sit securely with no wobble.
358 </div>
359
360 <div class="step">
361 <strong>Mount the neon lamp.</strong> Insert an NE-2 neon indicator lamp into the central tube holder on the jig. The lamp's glass envelope should be at the geometric center of the knot — the point where all ten bar flux paths converge. Seal with a drop of wax to prevent vibration displacement. Do not connect the neon lamp's leads to anything. It is a passive detector — if the field at center is strong enough, it ionizes without external connection.
362 </div>
363
364 </div>
365
366 <h2>Test Procedure</h2>
367
368 <h3><span class="phase-label phase-glow">Phase 1</span> Glow Test</h3>
369
370 <p>Close the switch. The CR2032 seeds current through the diode into the circuit. The caps begin charging. If the theory holds, the phi-ratio resonance amplifies voltage beyond what the 3.6V seed provides. Watch the neon lamp.</p>
371
372 <div class="test-result">
373 <span class="outcome">Immediate glow</span><span>Strong anomaly. The topology is pumping hard. Proceed to Phase 2 immediately. Go make coffee first because you'll be here a while. Possibly forever.</span>
374 <span class="outcome">Glow after delay</span><span>Caps are charging through resonance. The delay = time to reach NE-2 strike voltage (~65V) from 3.6V seed. Measure the delay — it encodes the Q factor and any anomalous gain.</span>
375 <span class="outcome">Flicker / pulse</span><span>Borderline. The circuit is reaching near-strike voltage but not sustaining. Try in a dark room — your eyes adapt and can see sub-ionization glow. Reduce neon tube gas pressure (use a longer tube, or try a 1/2W neon indicator which has lower strike voltage).</span>
376 <span class="outcome">Nothing</span><span>Either: (a) ferrite-epoxy Q too low (losses eating the margin), (b) winding pitch ratio not hitting the pair resonance, (c) cap bank values need adjustment, or (d) the theory is wrong. All except (d) are fixable. Build the sintered version before concluding (d).</span>
377 </div>
378
379 <h3><span class="phase-label phase-emf">Phase 2</span> EMF Measurement</h3>
380
381 <p>If Phase 1 shows any anomaly (glow, flicker, or measured voltage exceeding the 3.6V seed after disconnecting the battery):</p>
382
383 <p>Disconnect the battery. Measure voltage across the cap bank at each node with a multimeter. If any node reads above 3.6V with the battery disconnected and diode blocking backflow, the circuit is generating energy from somewhere the standard model says it shouldn't.</p>
384
385 <p>Connect an SDR (RTL-SDR dongle, ~€25 if you don't have one) and point the antenna at the center of the knot. Scan the spectrum. Three peaks at Fibonacci-spaced frequencies = the shēng cycle is pumping. Broadband noise floor between peaks = pair-breaking stochastic emission. The device will announce its own operating status via RF.</p>
386
387 <p>For continuous monitoring: replace the neon lamp with a photodiode aimed at a second neon lamp wired across one node. The photodiode output feeds into an Arduino ADC logging to SD card. You now have a time series of the pair-breaking rhythm — the stochastic GC signal.</p>
388
389 <div class="warn">Do not scale up power before understanding the operating mode. If the glow test shows genuine anomalous gain, the sintered ferrite version at the same geometry will be significantly more powerful due to 100x higher permeability. Proceed incrementally. The device regulates itself through the neon lamp (which clamps voltage at sustain level), but removing the lamp removes the governor.</div>
390
391 <h2>Scaling Path</h2>
392
393 <p>If the ferrite-epoxy prototype shows any anomalous effect, the next build uses sintered ferrite from the same salvaged cores. The fabrication chain changes: PLA print (not PVA) → investment in plaster → burn out PLA at 400°C → fill with pure milled ferrite powder under vibration → sinter at 1200-1300°C. This requires a kiln. Sintered MnZn ferrite gives μr of 2000-10000 versus 20-50 for the epoxy composite — two orders of magnitude improvement in inductance, Q factor, and energy storage per cycle.</p>
394
395 <p>The geometry, winding ratios, and cap values stay identical. Only the core material changes. Same test jig, same neon lamp position, same circuit. The jig is the constant across all iterations.</p>
396
397 <h2>Files</h2>
398
399 <p style="font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--dim);">
400 star_core.stl — Complete core form (print in PVA)<br>
401 star_core_top.stl — Top half reference<br>
402 star_core_bottom.stl — Bottom half reference<br>
403 test_jig.stl — Test seat with neon holder (print in PLA)<br>
404 </p>
405
406 </main>
407
408 <!-- Three.js 3D Viewer -->
409 <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
410 <script>
411 const PHI = (1 + Math.sqrt(5)) / 2;
412 const SQRT_PHI = Math.sqrt(PHI);
413 const R = 5.0; // Scene units (50mm / 10)
414 const BAR_R = 0.4;
415 const NODE_R = 0.8;
416 const s = 2 * R * Math.sin(Math.PI / 5);
417 const h = s * SQRT_PHI;
418 const hh = h / 2;
419
420 const ELEMENTS = [
421 { name: 'Wood', ch: '木', col: 0x4a9e4a, caps: 1 },
422 { name: 'Fire', ch: '火', col: 0xc44040, caps: 2 },
423 { name: 'Earth', ch: '土', col: 0xc4a040, caps: 3 },
424 { name: 'Metal', ch: '金', col: 0xa0a0a8, caps: 5 },
425 { name: 'Water', ch: '水', col: 0x4060c4, caps: 8 },
426 ];
427
428 function vPos(i) {
429 const a = -Math.PI/2 + i * 2 * Math.PI / 5;
430 return new THREE.Vector3(R * Math.cos(a), 0, R * Math.sin(a));
431 }
432
433 function upper(i) { const v = vPos(i); v.y = hh; return v; }
434 function lower(i) { const v = vPos(i); v.y = -hh; return v; }
435
436 // Scene setup
437 const canvas = document.getElementById('canvas3d');
438 const scene = new THREE.Scene();
439 scene.background = new THREE.Color(0x0a0a0c);
440 const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 200);
441 const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
442 renderer.setSize(canvas.clientWidth, canvas.clientHeight);
443 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
444
445 // Lighting
446 scene.add(new THREE.AmbientLight(0x404040, 0.6));
447 const dLight = new THREE.DirectionalLight(0xfff8e0, 0.8);
448 dLight.position.set(5, 10, 7);
449 scene.add(dLight);
450 const pLight = new THREE.PointLight(0xe8d5a3, 0.4, 30);
451 pLight.position.set(0, 0, 0);
452 scene.add(pLight);
453
454 // Materials
455 const coreMat = new THREE.MeshStandardMaterial({ color: 0x2a2a30, metalness: 0.7, roughness: 0.3 });
456 const coilMat = new THREE.MeshStandardMaterial({ color: 0xc87533, metalness: 0.8, roughness: 0.2 });
457 const jigMat = new THREE.MeshStandardMaterial({ color: 0x303040, metalness: 0.1, roughness: 0.8, transparent: true, opacity: 0.6 });
458 const neonMat = new THREE.MeshPhysicalMaterial({ color: 0xff6633, transparent: true, opacity: 0.7, emissive: 0xff4411, emissiveIntensity: 0.3, roughness: 0.1 });
459 const glassMat = new THREE.MeshPhysicalMaterial({ color: 0xccddee, transparent: true, opacity: 0.15, roughness: 0.05, side: 2 });
460 const capMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2a, metalness: 0.5, roughness: 0.5 });
461 const wireMat = new THREE.MeshStandardMaterial({ color: 0xc87533, metalness: 0.6, roughness: 0.3 });
462
463 function mkCyl(p1, p2, r, mat) {
464 const d = new THREE.Vector3().subVectors(p2, p1);
465 const len = d.length();
466 if (len < 0.01) return new THREE.Object3D();
467 const geo = new THREE.CylinderGeometry(r, r, len, 12);
468 const m = new THREE.Mesh(geo, mat);
469 const mid = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5);
470 m.position.copy(mid);
471 m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize());
472 return m;
473 }
474
475 function mkSphere(pos, r, mat) {
476 const m = new THREE.Mesh(new THREE.SphereGeometry(r, 16, 16), mat);
477 m.position.copy(pos);
478 return m;
479 }
480
481 // === Build groups ===
482 const coreGroup = new THREE.Group();
483 const jigGroup = new THREE.Group();
484 const capsGroup = new THREE.Group();
485 const coilGroup = new THREE.Group();
486 const neonGroup = new THREE.Group();
487
488 // Core: bars and nodes
489 for (let i = 0; i < 5; i++) {
490 // Node sphere
491 const nMat = new THREE.MeshStandardMaterial({ color: ELEMENTS[i].col, metalness: 0.5, roughness: 0.4 });
492 // Upper/lower junctions only — NO vertical ferrite connection
493 coreGroup.add(mkSphere(upper(i), NODE_R * 0.7, coreMat));
494 coreGroup.add(mkSphere(lower(i), NODE_R * 0.7, coreMat));
495 // Glass sheaths at both junctions
496 coreGroup.add(mkSphere(upper(i), NODE_R * 1.0, new THREE.MeshPhysicalMaterial({
497 color: ELEMENTS[i].col, transparent: true, opacity: 0.1, roughness: 0.05, side: 2
498 })));
499 coreGroup.add(mkSphere(lower(i), NODE_R * 1.0, new THREE.MeshPhysicalMaterial({
500 color: ELEMENTS[i].col, transparent: true, opacity: 0.1, roughness: 0.05, side: 2
501 })));
502 }
503
504 // Shēng bars
505 for (let i = 0; i < 5; i++) {
506 const j = (i + 1) % 5;
507 coreGroup.add(mkCyl(lower(i), upper(j), BAR_R, coreMat));
508 }
509 // Kè bars
510 for (let i = 0; i < 5; i++) {
511 const j = (i + 2) % 5;
512 coreGroup.add(mkCyl(upper(i), lower(j), BAR_R, coreMat));
513 }
514
515 // Coil visualization (helical wraps — tight close-wound)
516 function addCoilWrap(p1, p2, barR, turnsPerUnit, mat, group) {
517 const d = new THREE.Vector3().subVectors(p2, p1);
518 const len = d.length();
519 const turns = Math.round(len * turnsPerUnit);
520 const pts = [];
521 const up = d.clone().normalize();
522 let perp = new THREE.Vector3().crossVectors(up, new THREE.Vector3(0, 1, 0));
523 if (perp.length() < 0.01) perp = new THREE.Vector3().crossVectors(up, new THREE.Vector3(1, 0, 0));
524 perp.normalize();
525 const perp2 = new THREE.Vector3().crossVectors(up, perp);
526 const coilR = barR * 1.15;
527 const samplesPerTurn = 12;
528 const totalSamples = turns * samplesPerTurn;
529 for (let t = 0; t <= totalSamples; t++) {
530 const frac = t / totalSamples;
531 const angle = frac * turns * 2 * Math.PI;
532 const pos = p1.clone().add(d.clone().multiplyScalar(frac));
533 pos.add(perp.clone().multiplyScalar(Math.cos(angle) * coilR));
534 pos.add(perp2.clone().multiplyScalar(Math.sin(angle) * coilR));
535 pts.push(pos);
536 }
537 const curve = new THREE.CatmullRomCurve3(pts);
538 const tubeGeo = new THREE.TubeGeometry(curve, totalSamples, 0.04, 5, false);
539 group.add(new THREE.Mesh(tubeGeo, mat));
540 }
541
542 // Shēng coils (close-wound)
543 for (let i = 0; i < 5; i++) {
544 const j = (i + 1) % 5;
545 addCoilWrap(lower(i), upper(j), BAR_R, 3.5, coilMat, coilGroup);
546 }
547 // Kè coils (same pitch — ratio is in the geometry, not the winding)
548 for (let i = 0; i < 5; i++) {
549 const j = (i + 2) % 5;
550 addCoilWrap(upper(i), lower(j), BAR_R, 3.5, coilMat, coilGroup);
551 }
552
553 // Cap banks at each node — external circuit bridging upper↔lower
554 for (let i = 0; i < 5; i++) {
555 const n = ELEMENTS[i].caps;
556 const up = upper(i);
557 const lo = lower(i);
558 const midpoint = new THREE.Vector3().addVectors(up, lo).multiplyScalar(0.5);
559 const outDir = midpoint.clone().normalize();
560 const capCenter = midpoint.clone().add(outDir.clone().multiplyScalar(NODE_R * 2.2));
561
562 for (let c = 0; c < n; c++) {
563 const angle = (2 * Math.PI * c) / Math.max(n, 1);
564 const spread = n > 1 ? 0.4 : 0;
565 const off = new THREE.Vector3(
566 Math.cos(angle) * spread,
567 (c - (n-1)/2) * 0.35,
568 Math.sin(angle) * spread
569 );
570 const capPos = capCenter.clone().add(off);
571 const capMesh = new THREE.Mesh(
572 new THREE.CylinderGeometry(0.18, 0.18, 0.45, 8),
573 new THREE.MeshStandardMaterial({ color: ELEMENTS[i].col, metalness: 0.4, roughness: 0.5 })
574 );
575 capMesh.position.copy(capPos);
576 capsGroup.add(capMesh);
577 // Wire from cap to upper junction
578 capsGroup.add(mkCyl(up, capPos, 0.025, wireMat));
579 // Wire from cap to lower junction
580 capsGroup.add(mkCyl(lo, capPos, 0.025, wireMat));
581 }
582 }
583
584 // Battery, diode, switch on Wood node (index 0) — outside the knot
585 {
586 const woodUp = upper(0);
587 const woodLo = lower(0);
588 const woodMid = new THREE.Vector3().addVectors(woodUp, woodLo).multiplyScalar(0.5);
589 const woodOut = woodMid.clone().normalize();
590 // Place battery further outward past the cap bank, away from center
591 const batteryPos = woodMid.clone().add(woodOut.clone().multiplyScalar(NODE_R * 5.5));
592
593 // CR2032 battery — flat silver disc
594 const battMat = new THREE.MeshStandardMaterial({ color: 0xbbbbcc, metalness: 0.9, roughness: 0.15 });
595 const battery = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 0.15, 16), battMat);
596 battery.position.copy(batteryPos);
597 capsGroup.add(battery);
598 // + symbol on battery (small red dot)
599 const plusDot = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8),
600 new THREE.MeshStandardMaterial({ color: 0xcc3333, emissive: 0xcc3333, emissiveIntensity: 0.3 }));
601 plusDot.position.copy(batteryPos.clone().add(new THREE.Vector3(0, 0.1, 0)));
602 capsGroup.add(plusDot);
603
604 // Diode — small dark cylinder with band
605 const diodePos = batteryPos.clone().lerp(woodUp, 0.35);
606 const diode = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.4, 8),
607 new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.3, roughness: 0.6 }));
608 diode.position.copy(diodePos);
609 diode.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),
610 new THREE.Vector3().subVectors(woodUp, batteryPos).normalize());
611 capsGroup.add(diode);
612 // Diode band
613 const bandPos = diodePos.clone().add(new THREE.Vector3().subVectors(woodUp, batteryPos).normalize().multiplyScalar(0.12));
614 const band = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 0.05, 8),
615 new THREE.MeshStandardMaterial({ color: 0xcccccc }));
616 band.position.copy(bandPos);
617 band.quaternion.copy(diode.quaternion);
618 capsGroup.add(band);
619
620 // Switch — small box
621 const switchPos = batteryPos.clone().lerp(woodLo, 0.35);
622 const switchMat = new THREE.MeshStandardMaterial({ color: 0x444455, metalness: 0.3, roughness: 0.5 });
623 const switchBody = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.2, 0.2), switchMat);
624 switchBody.position.copy(switchPos);
625 capsGroup.add(switchBody);
626 // Toggle lever
627 const leverMat = new THREE.MeshStandardMaterial({ color: 0xe8d5a3, metalness: 0.6, roughness: 0.3 });
628 const lever = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.2, 6), leverMat);
629 lever.position.copy(switchPos.clone().add(new THREE.Vector3(0, 0.15, 0)));
630 lever.rotation.z = 0.4;
631 capsGroup.add(lever);
632
633 // Resistor — small striped cylinder near battery
634 const resPos = batteryPos.clone().add(new THREE.Vector3(0, -0.5, 0));
635 const resMat = new THREE.MeshStandardMaterial({ color: 0xd4aa60, metalness: 0.1, roughness: 0.8 });
636 const resistor = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.35, 8), resMat);
637 resistor.position.copy(resPos);
638 resistor.rotation.z = Math.PI / 2;
639 capsGroup.add(resistor);
640
641 // Wiring: battery+ → diode → upper junction
642 capsGroup.add(mkCyl(batteryPos, diodePos, 0.025, wireMat));
643 capsGroup.add(mkCyl(diodePos, woodUp, 0.025, wireMat));
644 // Wiring: battery- → switch → lower junction
645 capsGroup.add(mkCyl(batteryPos, switchPos, 0.025, wireMat));
646 capsGroup.add(mkCyl(switchPos, woodLo, 0.025, wireMat));
647 // Resistor across battery
648 capsGroup.add(mkCyl(batteryPos, resPos, 0.02, wireMat));
649 capsGroup.add(mkCyl(resPos, switchPos, 0.02, wireMat));
650 }
651
652 // Jig
653 const jigBase_y = -hh - NODE_R - 0.5;
654 // Base disc
655 const baseGeo = new THREE.CylinderGeometry(R + 2.5, R + 2.5, 0.4, 48);
656 const baseMesh = new THREE.Mesh(baseGeo, jigMat);
657 baseMesh.position.set(0, jigBase_y + 0.2, 0);
658 jigGroup.add(baseMesh);
659
660 // Socket posts
661 for (let i = 0; i < 5; i++) {
662 const pos = vPos(i);
663 const postH = (-hh - NODE_R * 0.3) - (jigBase_y + 0.4);
664 if (postH > 0.1) {
665 const post = new THREE.Mesh(
666 new THREE.CylinderGeometry(NODE_R * 1.1, NODE_R * 1.1, postH, 12),
667 jigMat
668 );
669 post.position.set(pos.x, jigBase_y + 0.4 + postH / 2, pos.z);
670 jigGroup.add(post);
671 }
672 // Socket ring
673 const ring = new THREE.Mesh(
674 new THREE.TorusGeometry(NODE_R * 1.1, 0.15, 8, 16),
675 jigMat
676 );
677 ring.position.set(pos.x, -hh - NODE_R * 0.3, pos.z);
678 ring.rotation.x = Math.PI / 2;
679 jigGroup.add(ring);
680 }
681
682 // Central column + neon holder
683 const colH = 0 - (jigBase_y + 0.4);
684 const col = new THREE.Mesh(
685 new THREE.CylinderGeometry(0.3, 0.3, colH, 8),
686 jigMat
687 );
688 col.position.set(0, jigBase_y + 0.4 + colH / 2, 0);
689 jigGroup.add(col);
690 // Holder ring at top
691 const holderRing = new THREE.Mesh(
692 new THREE.TorusGeometry(0.85, 0.1, 8, 16), jigMat);
693 holderRing.position.set(0, 0, 0);
694 holderRing.rotation.x = Math.PI / 2;
695 jigGroup.add(holderRing);
696
697 // Neon tube — larger to capture convergence zone
698 const neonBulb = new THREE.Mesh(new THREE.SphereGeometry(0.7, 16, 16), neonMat);
699 neonBulb.position.set(0, 0, 0);
700 neonGroup.add(neonBulb);
701 const neonGlass = new THREE.Mesh(new THREE.SphereGeometry(0.9, 16, 16), glassMat);
702 neonGlass.position.set(0, 0, 0);
703 neonGroup.add(neonGlass);
704 // Neon leads
705 neonGroup.add(mkCyl(new THREE.Vector3(0, -0.7, 0), new THREE.Vector3(0, -colH * 0.6, 0), 0.03, wireMat));
706
707 // Add all to scene
708 scene.add(coreGroup);
709 scene.add(jigGroup);
710 scene.add(capsGroup);
711 scene.add(coilGroup);
712 scene.add(neonGroup);
713
714 // View modes
715 const modes = { core: true, jig: true, wired: true };
716 function updateVisibility() {
717 coreGroup.visible = true; // always
718 jigGroup.visible = modes.jig;
719 capsGroup.visible = modes.wired;
720 coilGroup.visible = modes.wired;
721 neonGroup.visible = modes.jig;
722 }
723
724 document.getElementById('btn-core').onclick = () => {
725 modes.jig = false; modes.wired = false;
726 updateVisibility(); updateButtons();
727 };
728 document.getElementById('btn-jig').onclick = () => {
729 modes.jig = true; modes.wired = false;
730 updateVisibility(); updateButtons();
731 };
732 document.getElementById('btn-wired').onclick = () => {
733 modes.jig = false; modes.wired = true;
734 updateVisibility(); updateButtons();
735 };
736 document.getElementById('btn-all').onclick = () => {
737 modes.jig = true; modes.wired = true;
738 updateVisibility(); updateButtons();
739 };
740
741 function updateButtons() {
742 document.getElementById('btn-core').className = (!modes.jig && !modes.wired) ? 'active' : '';
743 document.getElementById('btn-jig').className = (modes.jig && !modes.wired) ? 'active' : '';
744 document.getElementById('btn-wired').className = (!modes.jig && modes.wired) ? 'active' : '';
745 document.getElementById('btn-all').className = (modes.jig && modes.wired) ? 'active' : '';
746 }
747
748 updateVisibility();
749
750 // Camera + orbit
751 let sph = { theta: Math.PI / 4, phi: Math.PI / 3, r: 22 };
752 let dragging = false, prevMouse = { x: 0, y: 0 };
753
754 function updateCamera() {
755 camera.position.set(
756 sph.r * Math.sin(sph.phi) * Math.cos(sph.theta),
757 sph.r * Math.cos(sph.phi),
758 sph.r * Math.sin(sph.phi) * Math.sin(sph.theta)
759 );
760 camera.lookAt(0, 0, 0);
761 }
762 updateCamera();
763
764 canvas.addEventListener('mousedown', e => { dragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
765 window.addEventListener('mouseup', () => dragging = false);
766 canvas.addEventListener('mousemove', e => {
767 if (!dragging) return;
768 sph.theta += (e.clientX - prevMouse.x) * 0.008;
769 sph.phi = Math.max(0.1, Math.min(Math.PI - 0.1, sph.phi - (e.clientY - prevMouse.y) * 0.008));
770 prevMouse = { x: e.clientX, y: e.clientY };
771 updateCamera();
772 });
773 canvas.addEventListener('wheel', e => {
774 sph.r = Math.max(8, Math.min(50, sph.r + e.deltaY * 0.02));
775 updateCamera();
776 e.preventDefault();
777 }, { passive: false });
778
779 // Touch support
780 canvas.addEventListener('touchstart', e => {
781 if (e.touches.length === 1) {
782 dragging = true;
783 prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
784 }
785 });
786 canvas.addEventListener('touchmove', e => {
787 if (!dragging || e.touches.length !== 1) return;
788 const t = e.touches[0];
789 sph.theta += (t.clientX - prevMouse.x) * 0.008;
790 sph.phi = Math.max(0.1, Math.min(Math.PI - 0.1, sph.phi - (t.clientY - prevMouse.y) * 0.008));
791 prevMouse = { x: t.clientX, y: t.clientY };
792 updateCamera();
793 e.preventDefault();
794 }, { passive: false });
795 canvas.addEventListener('touchend', () => dragging = false);
796
797 // Auto-rotate when idle
798 let idleTimer = 0;
799 const IDLE_THRESHOLD = 3000;
800 let lastInteraction = Date.now();
801
802 canvas.addEventListener('mousedown', () => lastInteraction = Date.now());
803 canvas.addEventListener('wheel', () => lastInteraction = Date.now());
804 canvas.addEventListener('touchstart', () => lastInteraction = Date.now());
805
806 // Neon glow animation
807 let time = 0;
808
809 function animate() {
810 requestAnimationFrame(animate);
811 time += 0.016;
812
813 // Auto-rotate when idle
814 if (Date.now() - lastInteraction > IDLE_THRESHOLD && !dragging) {
815 sph.theta += 0.003;
816 updateCamera();
817 }
818
819 // Neon pulse
820 if (neonGroup.visible) {
821 const pulse = 0.3 + 0.15 * Math.sin(time * 3) + 0.05 * Math.sin(time * 7.3);
822 neonMat.emissiveIntensity = pulse;
823 neonMat.opacity = 0.5 + pulse * 0.5;
824 }
825
826 renderer.render(scene, camera);
827 }
828 animate();
829
830 // Resize
831 window.addEventListener('resize', () => {
832 const w = canvas.clientWidth, hh = canvas.clientHeight;
833 camera.aspect = w / hh;
834 camera.updateProjectionMatrix();
835 renderer.setSize(w, hh);
836 });
837 </script>
838
839 </body>
840 </html>
841