Skip to content

Commit 4c19f77

Browse files
committed
Fix event bounding box calculation to account for rotated diamond
1 parent d80b707 commit 4c19f77

File tree

9 files changed

+335
-282
lines changed

9 files changed

+335
-282
lines changed

src/lib/export/svg/renderer.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,26 @@ function calculateBounds(nodes: NodeInstance[], events: EventInstance[]): Bounds
356356
}
357357

358358
for (const event of events) {
359-
// Events also use center-origin
360-
const left = event.position.x - EVENT.size / 2;
361-
const top = event.position.y - EVENT.size / 2;
359+
// Events use center-origin, get actual bounding box from DOM
360+
const wrapper = document.querySelector(`[data-id="${event.id}"]`) as HTMLElement;
361+
let boundingSize = EVENT.size; // Fallback
362+
363+
if (wrapper) {
364+
const diamondEl = wrapper.querySelector('.diamond') as HTMLElement;
365+
if (diamondEl) {
366+
const zoom = getZoom();
367+
const diamondSize = diamondEl.getBoundingClientRect().width / zoom;
368+
// Rotated 45° square has bounding box of size * sqrt(2)
369+
boundingSize = diamondSize * Math.SQRT2;
370+
}
371+
}
372+
373+
const left = event.position.x - boundingSize / 2;
374+
const top = event.position.y - boundingSize / 2;
362375
bounds.minX = Math.min(bounds.minX, left);
363376
bounds.minY = Math.min(bounds.minY, top);
364-
bounds.maxX = Math.max(bounds.maxX, left + EVENT.size);
365-
bounds.maxY = Math.max(bounds.maxY, top + EVENT.size);
377+
bounds.maxX = Math.max(bounds.maxX, left + boundingSize);
378+
bounds.maxY = Math.max(bounds.maxY, top + boundingSize);
366379
}
367380

368381
return isFinite(bounds.minX) ? bounds : { minX: 0, minY: 0, maxX: 200, maxY: 200 };

static/examples/bouncing-ball-dark.svg

Lines changed: 67 additions & 52 deletions
Loading

static/examples/bouncing-ball-light.svg

Lines changed: 67 additions & 52 deletions
Loading

static/examples/bouncing-ball.json

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"version": "1.0.0",
33
"metadata": {
4-
"created": "2026-01-19T16:03:09.415Z",
5-
"modified": "2026-01-19T16:03:09.415Z",
4+
"created": "2026-01-19T22:28:52.786Z",
5+
"modified": "2026-01-19T22:28:52.786Z",
66
"name": "bouncing-ball"
77
},
88
"graph": {
@@ -37,14 +37,17 @@
3737
],
3838
"params": {
3939
"initial_value": "x0 "
40-
}
40+
},
41+
"pinnedParams": [
42+
"initial_value"
43+
]
4144
},
4245
{
4346
"id": "d6b8ebf1-173b-4f71-a160-d86545a80cb9",
4447
"type": "Integrator",
4548
"name": "vel",
4649
"position": {
47-
"x": 530,
50+
"x": 490,
4851
"y": 250
4952
},
5053
"inputs": [
@@ -67,14 +70,17 @@
6770
"color": "#969696"
6871
}
6972
],
70-
"params": {}
73+
"params": {},
74+
"pinnedParams": [
75+
"initial_value"
76+
]
7177
},
7278
{
7379
"id": "c76c21f3-21cd-453e-9437-8e8af946d1c7",
7480
"type": "Scope",
7581
"name": "x(t)",
7682
"position": {
77-
"x": 860,
83+
"x": 880,
7884
"y": 340
7985
},
8086
"inputs": [
@@ -97,8 +103,8 @@
97103
"type": "Constant",
98104
"name": "gravity",
99105
"position": {
100-
"x": 430,
101-
"y": 160
106+
"x": 360,
107+
"y": 150
102108
},
103109
"inputs": [],
104110
"outputs": [
@@ -114,7 +120,10 @@
114120
"params": {
115121
"value": "-g",
116122
"_rotation": 1
117-
}
123+
},
124+
"pinnedParams": [
125+
"value"
126+
]
118127
},
119128
{
120129
"id": "d73713d6-eb61-442e-a69b-c4d168180ef4",
@@ -142,10 +151,10 @@
142151
{
143152
"id": "db488fa8-9004-40f7-a724-bc9846dd2f85",
144153
"type": "Function",
145-
"name": "total energy",
154+
"name": "energy",
146155
"position": {
147156
"x": 750,
148-
"y": 160
157+
"y": 130
149158
},
150159
"inputs": [
151160
{
@@ -178,14 +187,15 @@
178187
"params": {
179188
"func": "lambda v, x: m*(v**2/2 + g*x)",
180189
"_rotation": 0
181-
}
190+
},
191+
"pinnedParams": []
182192
},
183193
{
184194
"id": "78f720fe-8774-42c3-be6b-5a4ef30e00df",
185195
"type": "Scope",
186196
"name": "E(t)",
187197
"position": {
188-
"x": 1100,
198+
"x": 1140,
189199
"y": 340
190200
},
191201
"inputs": [
@@ -259,8 +269,8 @@
259269
{
260270
"id": "deeecd86-71ac-4a13-a289-c64c946e667c",
261271
"position": {
262-
"x": -50,
263-
"y": 60
272+
"x": -160,
273+
"y": 40
264274
},
265275
"content": "# Bouncing Ball Physics\n\nThis is an example of a hybrid system with continuous dynamics (free falling ball) and discrete events (bounces).\n\nIntegrating the gravitational acceleration gives the ball velocity in free fall:\n\n$$ v(t) = - \\int_0^t g \\, d\\tau $$\n\nAnd the position is the integrated velocity:\n\n$$ x(t) = \\int_0^t v(\\tau) \\, d\\tau $$\n\nWe use an event node to define the discrete dynamics of the ball hitting the ground. Whenever the position crosses zero\n\n$$ |x(t)| = 0 $$\n\nthe sign of the velocity is flipped and a coefficient of restitution ($b$) is applied to model the energy loss from the deformation of the ball:\n\n$$ y(t) \\leftarrow -b \\, y(t) $$\n\nWhen the total energy drops below a certain threshold we consider that a Zeno state and stop the simulation.",
266276
"width": 425,
@@ -274,7 +284,7 @@
274284
"type": "pathsim.events.ZeroCrossing",
275285
"name": "Bounce",
276286
"position": {
277-
"x": 980,
287+
"x": 1000,
278288
"y": 260
279289
},
280290
"params": {
@@ -287,7 +297,7 @@
287297
"type": "pathsim.events.Condition",
288298
"name": "Zeno",
289299
"position": {
290-
"x": 1200,
300+
"x": 1260,
291301
"y": 260
292302
},
293303
"params": {
@@ -297,7 +307,7 @@
297307
}
298308
],
299309
"codeContext": {
300-
"code": "# initial position \nx0 = 1\n\n# mass\nm = 1\n\n# gravity\ng = 9.81\n\n# bounce back coefficient (randomized)\nb = 0.85 + 0.1*np.random.rand()\n\n# zeno energy threshold\ne_th = 0.5\n\n# event detection function\ndef bounce_detect(t):\n return pos.engine.state\n\n# event resolution function\ndef bounce_resolve(t):\n pos.engine.set(-pos.engine.state)\n vel.engine.set(-b*vel.engine.state)\n\n# zeno state detection\ndef zeno_detect(t):\n e = total_energy.outputs[0]\n return e < e_th\n\n# stop simulation \ndef zeno_resolve(t):\n sim.stop()\n"
310+
"code": "# initial position \nx0 = 1\n\n# mass\nm = 1\n\n# gravity\ng = 9.81\n\n# bounce back coefficient (randomized)\nb = 0.85 + 0.1*np.random.rand()\n\n# zeno energy threshold\ne_th = 0.5\n\n# event detection function\ndef bounce_detect(t):\n return pos.engine.state\n\n# event resolution function\ndef bounce_resolve(t):\n pos.engine.set(-pos.engine.state)\n vel.engine.set(-b*vel.engine.state)\n\n# zeno state detection\ndef zeno_detect(t):\n e = energy.outputs[0]\n return e < e_th\n\n# stop simulation \ndef zeno_resolve(t):\n sim.stop()\n"
301311
},
302312
"simulationSettings": {
303313
"duration": "50",

0 commit comments

Comments
 (0)