thibaud frere commited on
Commit
36a0415
·
1 Parent(s): 1f9a800

update multi images

Browse files
app/scripts/latex-to-mdx/mdx-converter.mjs CHANGED
@@ -123,6 +123,104 @@ function addComponentImports(content) {
123
  }
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  /**
127
  * Transform images to ResponsiveImage components
128
  * @param {string} content - MDX content
@@ -724,6 +822,7 @@ function processMdxContent(content, latexContent = '') {
724
  processedContent = formatDisplayMathBlocks(processedContent);
725
  processedContent = removeHtmlComments(processedContent);
726
  processedContent = cleanMdxSyntax(processedContent);
 
727
  processedContent = transformImages(processedContent);
728
  processedContent = transformStyledSpans(processedContent);
729
  processedContent = transformReferenceLinks(processedContent);
 
123
  }
124
 
125
 
126
+ /**
127
+ * Convert grouped figures (subfigures) to MultiImage components
128
+ * @param {string} content - MDX content
129
+ * @returns {string} - Content with MultiImage components for grouped figures
130
+ */
131
+ function convertSubfiguresToMultiImage(content) {
132
+ console.log(' 🖼️✨ Converting subfigures to MultiImage components...');
133
+
134
+ let convertedCount = 0;
135
+
136
+ // Pattern to match: <figure> containing multiple <figure> elements with a global caption
137
+ // This matches the LaTeX subfigure pattern that gets converted by Pandoc
138
+ const subfigureGroupPattern = /<figure>\s*((?:<figure>[\s\S]*?<\/figure>\s*){2,})<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
139
+
140
+ const convertedContent = content.replace(subfigureGroupPattern, (match, figuresMatch, globalCaption) => {
141
+ convertedCount++;
142
+
143
+ // Extract individual figures within the group
144
+ // This pattern is more flexible to handle variations in HTML structure
145
+ const individualFigurePattern = /<figure>\s*<img src="([^"]*)"[^>]*\/>\s*<p>&lt;span id="([^"]*)"[^&]*&gt;&lt;\/span&gt;<\/p>\s*<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
146
+
147
+ const images = [];
148
+ let figureMatch;
149
+
150
+ while ((figureMatch = individualFigurePattern.exec(figuresMatch)) !== null) {
151
+ const [, src, id, caption] = figureMatch;
152
+
153
+ // Clean the source path (similar to existing transformImages function)
154
+ const cleanSrc = src.replace(/.*\/output\/assets\//, './assets/')
155
+ .replace(/\/Users\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/app\/scripts\/latex-to-markdown\/output\/assets\//, './assets/');
156
+
157
+ // Clean caption text (remove HTML, normalize whitespace)
158
+ const cleanCaption = caption
159
+ .replace(/<[^>]*>/g, '')
160
+ .replace(/\n/g, ' ')
161
+ .replace(/\s+/g, ' ')
162
+ .replace(/'/g, "\\'")
163
+ .trim();
164
+
165
+ // Generate alt text from caption
166
+ const altText = cleanCaption.length > 100
167
+ ? cleanCaption.substring(0, 100) + '...'
168
+ : cleanCaption;
169
+
170
+ // Generate variable name for import
171
+ const varName = generateImageVarName(cleanSrc);
172
+ imageImports.set(cleanSrc, varName);
173
+
174
+ images.push({
175
+ src: varName,
176
+ alt: altText,
177
+ caption: cleanCaption,
178
+ id: id
179
+ });
180
+ }
181
+
182
+ // Clean global caption
183
+ const cleanGlobalCaption = globalCaption
184
+ .replace(/<[^>]*>/g, '')
185
+ .replace(/\n/g, ' ')
186
+ .replace(/\s+/g, ' ')
187
+ .replace(/'/g, "\\'")
188
+ .trim();
189
+
190
+ // Mark MultiImage component as used
191
+ usedComponents.add('MultiImage');
192
+
193
+ // Determine layout based on number of images
194
+ let layout = 'auto';
195
+ if (images.length === 2) layout = '2-column';
196
+ else if (images.length === 3) layout = '3-column';
197
+ else if (images.length === 4) layout = '4-column';
198
+
199
+ // Generate MultiImage component
200
+ const imagesJson = images.map(img =>
201
+ ` {\n src: ${img.src},\n alt: "${img.alt}",\n caption: "${img.caption}",\n id: "${img.id}"\n }`
202
+ ).join(',\n');
203
+
204
+ return `<MultiImage
205
+ images={[
206
+ ${imagesJson}
207
+ ]}
208
+ layout="${layout}"
209
+ zoomable
210
+ downloadable
211
+ caption="${cleanGlobalCaption}"
212
+ />`;
213
+ });
214
+
215
+ if (convertedCount > 0) {
216
+ console.log(` ✅ Converted ${convertedCount} subfigure group(s) to MultiImage component(s)`);
217
+ } else {
218
+ console.log(' ℹ️ No subfigure groups found');
219
+ }
220
+
221
+ return convertedContent;
222
+ }
223
+
224
  /**
225
  * Transform images to ResponsiveImage components
226
  * @param {string} content - MDX content
 
822
  processedContent = formatDisplayMathBlocks(processedContent);
823
  processedContent = removeHtmlComments(processedContent);
824
  processedContent = cleanMdxSyntax(processedContent);
825
+ processedContent = convertSubfiguresToMultiImage(processedContent);
826
  processedContent = transformImages(processedContent);
827
  processedContent = transformStyledSpans(processedContent);
828
  processedContent = transformReferenceLinks(processedContent);
app/scripts/latex-to-mdx/output/main.mdx CHANGED
@@ -18,15 +18,16 @@ published: "Sep 18, 2025"
18
  tableOfContentsAutoCollapse: true
19
  ---
20
 
 
21
  import ResponsiveImage from '../components/ResponsiveImage.astro';
 
 
 
22
  import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
23
  import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
24
  import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
25
  import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
26
  import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
27
- import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
28
- import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
29
- import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
30
  import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
31
  import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
32
  import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
@@ -304,42 +305,32 @@ Further, let us make the simplifying assumption that actuators can produce rotat
304
 
305
  All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
306
 
307
- <figure>
308
- <figure>
309
- <ResponsiveImage
310
- src={ch2_planar_manipulator_free}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  zoomable
312
  downloadable
313
- layout="fixed"
314
- alt="Figure"
315
  />
316
- <span id="planar-manipulation-simple" style="position: absolute;"></span>
317
- <figcaption>Free to move</figcaption>
318
- </figure>
319
- <figure>
320
- <ResponsiveImage
321
- src={ch2_planar_manipulator_floor}
322
- zoomable
323
- downloadable
324
- layout="fixed"
325
- alt="Figure"
326
- />
327
- <span id="planar-manipulator-floor" style="position: absolute;"></span>
328
- <figcaption>Constrained by the surface</figcaption>
329
- </figure>
330
- <figure>
331
- <ResponsiveImage
332
- src={ch2_planar_manipulator_floor_shelf}
333
- zoomable
334
- downloadable
335
- layout="fixed"
336
- alt="Figure"
337
- />
338
- <span id="planar-manipulator-floor-shelf" style="position: absolute;"></span>
339
- <figcaption>Constrained by surface and (fixed) obstacle</figcaption>
340
- </figure>
341
- <figcaption>Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint.</figcaption>
342
- </figure>
343
 
344
  Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
345
 
 
18
  tableOfContentsAutoCollapse: true
19
  ---
20
 
21
+ import MultiImage from '../components/MultiImage.astro';
22
  import ResponsiveImage from '../components/ResponsiveImage.astro';
23
+ import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
24
+ import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
25
+ import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
26
  import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
27
  import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
28
  import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
29
  import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
30
  import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
 
 
 
31
  import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
32
  import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
33
  import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
 
305
 
306
  All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
307
 
308
+ <MultiImage
309
+ images={[
310
+ {
311
+ src: ch2_planar_manipulator_free,
312
+ alt: "Free to move",
313
+ caption: "Free to move",
314
+ id: "planar-manipulation-simple"
315
+ },
316
+ {
317
+ src: ch2_planar_manipulator_floor,
318
+ alt: "Constrained by the surface",
319
+ caption: "Constrained by the surface",
320
+ id: "planar-manipulator-floor"
321
+ },
322
+ {
323
+ src: ch2_planar_manipulator_floor_shelf,
324
+ alt: "Constrained by surface and (fixed) obstacle",
325
+ caption: "Constrained by surface and (fixed) obstacle",
326
+ id: "planar-manipulator-floor-shelf"
327
+ }
328
+ ]}
329
+ layout="3-column"
330
  zoomable
331
  downloadable
332
+ caption="Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint."
 
333
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
336
 
app/src/components/MultiImage.astro ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ // @ts-ignore - types provided by Astro at runtime
3
+ import ResponsiveImage from "./ResponsiveImage.astro";
4
+
5
+ interface ImageItem {
6
+ /** Source image imported via astro:assets */
7
+ src: any;
8
+ /** Alt text for accessibility */
9
+ alt: string;
10
+ /** Individual caption for this image */
11
+ caption?: string;
12
+ /** Optional individual image ID for referencing */
13
+ id?: string;
14
+ /** Enable zoom on this specific image (defaults to parent zoomable setting) */
15
+ zoomable?: boolean;
16
+ /** Enable download on this specific image (defaults to parent downloadable setting) */
17
+ downloadable?: boolean;
18
+ }
19
+
20
+ interface Props {
21
+ /** Array of images to display */
22
+ images: ImageItem[];
23
+ /** Global caption for the entire figure */
24
+ caption?: string;
25
+ /** Layout mode: number of columns or 'auto' for responsive */
26
+ layout?: "2-column" | "3-column" | "4-column" | "auto";
27
+ /** Enable medium-zoom behavior on all images (can be overridden per image) */
28
+ zoomable?: boolean;
29
+ /** Show download buttons on all images (can be overridden per image) */
30
+ downloadable?: boolean;
31
+ /** Optional class to apply on the wrapper */
32
+ class?: string;
33
+ /** Optional global ID for the multi-image figure */
34
+ id?: string;
35
+ }
36
+
37
+ const {
38
+ images,
39
+ caption,
40
+ layout = "3-column",
41
+ zoomable = false,
42
+ downloadable = false,
43
+ class: className,
44
+ id,
45
+ } = Astro.props as Props;
46
+
47
+ const hasCaptionSlot = Astro.slots.has("caption");
48
+ const hasCaption =
49
+ hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
50
+ const uid = `mi_${Math.random().toString(36).slice(2)}`;
51
+
52
+ // Generate CSS grid columns based on layout
53
+ const getGridColumns = () => {
54
+ switch (layout) {
55
+ case "2-column":
56
+ return "repeat(2, 1fr)";
57
+ case "3-column":
58
+ return "repeat(3, 1fr)";
59
+ case "4-column":
60
+ return "repeat(4, 1fr)";
61
+ case "auto":
62
+ return "repeat(auto-fit, minmax(200px, 1fr))";
63
+ default:
64
+ return "repeat(3, 1fr)";
65
+ }
66
+ };
67
+
68
+ const gridColumns = getGridColumns();
69
+ ---
70
+
71
+ <div
72
+ class={`multi-image ${className || ""}`}
73
+ data-mi-root={uid}
74
+ data-layout={layout}
75
+ {id}
76
+ >
77
+ {
78
+ hasCaption ? (
79
+ <figure class="multi-image-figure">
80
+ <div
81
+ class="multi-image-grid"
82
+ style={`grid-template-columns: ${gridColumns}`}
83
+ >
84
+ {images.map((image, index) => (
85
+ <div class="multi-image-item">
86
+ <ResponsiveImage
87
+ src={image.src}
88
+ alt={image.alt}
89
+ zoomable={image.zoomable ?? zoomable}
90
+ downloadable={
91
+ image.downloadable ?? downloadable
92
+ }
93
+ class="multi-image-img"
94
+ />
95
+ {image.caption && (
96
+ <div class="multi-image-subcaption">
97
+ {image.caption}
98
+ </div>
99
+ )}
100
+ {image.id && (
101
+ <span
102
+ id={image.id}
103
+ style="position: absolute;"
104
+ />
105
+ )}
106
+ </div>
107
+ ))}
108
+ </div>
109
+ <figcaption class="multi-image-caption">
110
+ {hasCaptionSlot ? (
111
+ <slot name="caption" />
112
+ ) : (
113
+ caption && <span set:html={caption} />
114
+ )}
115
+ </figcaption>
116
+ </figure>
117
+ ) : (
118
+ <div
119
+ class="multi-image-grid"
120
+ style={`grid-template-columns: ${gridColumns}`}
121
+ >
122
+ {images.map((image, index) => (
123
+ <div class="multi-image-item">
124
+ <ResponsiveImage
125
+ src={image.src}
126
+ alt={image.alt}
127
+ zoomable={image.zoomable ?? zoomable}
128
+ downloadable={image.downloadable ?? downloadable}
129
+ class="multi-image-img"
130
+ />
131
+ {image.caption && (
132
+ <div class="multi-image-subcaption">
133
+ {image.caption}
134
+ </div>
135
+ )}
136
+ {image.id && (
137
+ <span id={image.id} style="position: absolute;" />
138
+ )}
139
+ </div>
140
+ ))}
141
+ </div>
142
+ )
143
+ }
144
+ </div>
145
+
146
+ <style>
147
+ .multi-image {
148
+ margin: var(--block-spacing-y) 0;
149
+ }
150
+
151
+ .multi-image-figure {
152
+ margin: 0;
153
+ }
154
+
155
+ .multi-image-grid {
156
+ display: grid;
157
+ gap: 1rem;
158
+ align-items: start;
159
+ }
160
+
161
+ .multi-image-item {
162
+ display: flex;
163
+ flex-direction: column;
164
+ text-align: center;
165
+ }
166
+
167
+ .multi-image-item :global(.ri-root) {
168
+ margin: 0;
169
+ }
170
+
171
+ .multi-image-item :global(figure) {
172
+ margin: 0;
173
+ }
174
+
175
+ .multi-image-img {
176
+ width: 100%;
177
+ height: auto;
178
+ object-fit: contain;
179
+ }
180
+
181
+ .multi-image-subcaption {
182
+ font-size: 0.85rem;
183
+ color: var(--muted-color);
184
+ margin-top: 0.5rem;
185
+ line-height: 1.4;
186
+ }
187
+
188
+ .multi-image-caption {
189
+ text-align: left;
190
+ font-size: 0.9rem;
191
+ color: var(--muted-color);
192
+ margin-top: 1rem;
193
+ line-height: 1.4;
194
+ }
195
+
196
+ /* Responsive behavior */
197
+ @media (max-width: 768px) {
198
+ .multi-image-grid[style*="repeat(3, 1fr)"],
199
+ .multi-image-grid[style*="repeat(4, 1fr)"] {
200
+ grid-template-columns: 1fr !important;
201
+ gap: 1.5rem;
202
+ }
203
+
204
+ .multi-image-grid[style*="repeat(2, 1fr)"] {
205
+ grid-template-columns: 1fr !important;
206
+ gap: 1.5rem;
207
+ }
208
+ }
209
+
210
+ @media (min-width: 769px) and (max-width: 1024px) {
211
+ .multi-image-grid[style*="repeat(4, 1fr)"] {
212
+ grid-template-columns: repeat(2, 1fr) !important;
213
+ }
214
+ }
215
+
216
+ /* Equal height images when desired */
217
+ .multi-image[data-layout*="column"] .multi-image-item :global(img) {
218
+ height: 200px;
219
+ object-fit: contain;
220
+ }
221
+
222
+ /* Auto layout gets flexible heights */
223
+ .multi-image[data-layout="auto"] .multi-image-item :global(img) {
224
+ height: auto;
225
+ }
226
+
227
+ /* Ensure images maintain aspect ratio */
228
+ .multi-image-item :global(img) {
229
+ max-width: 100%;
230
+ display: block;
231
+ margin: 0 auto;
232
+ }
233
+ </style>
app/src/content/article.mdx CHANGED
@@ -18,15 +18,16 @@ published: "Sep 18, 2025"
18
  tableOfContentsAutoCollapse: true
19
  ---
20
 
 
21
  import ResponsiveImage from '../components/ResponsiveImage.astro';
 
 
 
22
  import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
23
  import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
24
  import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
25
  import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
26
  import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
27
- import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
28
- import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
29
- import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
30
  import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
31
  import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
32
  import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
@@ -304,42 +305,32 @@ Further, let us make the simplifying assumption that actuators can produce rotat
304
 
305
  All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
306
 
307
- <figure>
308
- <figure>
309
- <ResponsiveImage
310
- src={ch2_planar_manipulator_free}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  zoomable
312
  downloadable
313
- layout="fixed"
314
- alt="Figure"
315
  />
316
- <span id="planar-manipulation-simple" style="position: absolute;"></span>
317
- <figcaption>Free to move</figcaption>
318
- </figure>
319
- <figure>
320
- <ResponsiveImage
321
- src={ch2_planar_manipulator_floor}
322
- zoomable
323
- downloadable
324
- layout="fixed"
325
- alt="Figure"
326
- />
327
- <span id="planar-manipulator-floor" style="position: absolute;"></span>
328
- <figcaption>Constrained by the surface</figcaption>
329
- </figure>
330
- <figure>
331
- <ResponsiveImage
332
- src={ch2_planar_manipulator_floor_shelf}
333
- zoomable
334
- downloadable
335
- layout="fixed"
336
- alt="Figure"
337
- />
338
- <span id="planar-manipulator-floor-shelf" style="position: absolute;"></span>
339
- <figcaption>Constrained by surface and (fixed) obstacle</figcaption>
340
- </figure>
341
- <figcaption>Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint.</figcaption>
342
- </figure>
343
 
344
  Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
345
 
 
18
  tableOfContentsAutoCollapse: true
19
  ---
20
 
21
+ import MultiImage from '../components/MultiImage.astro';
22
  import ResponsiveImage from '../components/ResponsiveImage.astro';
23
+ import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
24
+ import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
25
+ import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
26
  import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
27
  import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
28
  import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
29
  import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
30
  import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
 
 
 
31
  import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
32
  import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
33
  import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
 
305
 
306
  All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
307
 
308
+ <MultiImage
309
+ images={[
310
+ {
311
+ src: ch2_planar_manipulator_free,
312
+ alt: "Free to move",
313
+ caption: "Free to move",
314
+ id: "planar-manipulation-simple"
315
+ },
316
+ {
317
+ src: ch2_planar_manipulator_floor,
318
+ alt: "Constrained by the surface",
319
+ caption: "Constrained by the surface",
320
+ id: "planar-manipulator-floor"
321
+ },
322
+ {
323
+ src: ch2_planar_manipulator_floor_shelf,
324
+ alt: "Constrained by surface and (fixed) obstacle",
325
+ caption: "Constrained by surface and (fixed) obstacle",
326
+ id: "planar-manipulator-floor-shelf"
327
+ }
328
+ ]}
329
+ layout="3-column"
330
  zoomable
331
  downloadable
332
+ caption="Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint."
 
333
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
336