cyclecad 3.0.0 → 3.2.0

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.
Files changed (67) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/index.html +93 -0
  18. package/app/js/billing-ui.js +990 -0
  19. package/app/js/brep-kernel.js +933 -981
  20. package/app/js/collab-client.js +750 -0
  21. package/app/js/mobile-nav.js +623 -0
  22. package/app/js/mobile-toolbar.js +476 -0
  23. package/app/js/modules/billing-module.js +724 -0
  24. package/app/js/modules/step-module-enhanced.js +938 -0
  25. package/app/js/offline-manager.js +705 -0
  26. package/app/js/responsive-init.js +360 -0
  27. package/app/js/touch-handler.js +429 -0
  28. package/app/manifest.json +211 -0
  29. package/app/offline.html +508 -0
  30. package/app/sw.js +571 -0
  31. package/app/tests/billing-tests.html +779 -0
  32. package/app/tests/brep-tests.html +980 -0
  33. package/app/tests/collab-tests.html +743 -0
  34. package/app/tests/mobile-tests.html +1299 -0
  35. package/app/tests/pwa-tests.html +1134 -0
  36. package/app/tests/step-tests.html +1042 -0
  37. package/app/tests/test-agent-v3.html +719 -0
  38. package/docker-compose.yml +225 -0
  39. package/docs/BILLING-HELP.json +260 -0
  40. package/docs/BILLING-README.md +639 -0
  41. package/docs/BILLING-TUTORIAL.md +736 -0
  42. package/docs/BREP-HELP.json +326 -0
  43. package/docs/BREP-TUTORIAL.md +802 -0
  44. package/docs/COLLABORATION-HELP.json +228 -0
  45. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  46. package/docs/DOCKER-HELP.json +224 -0
  47. package/docs/DOCKER-TUTORIAL.md +974 -0
  48. package/docs/MOBILE-HELP.json +243 -0
  49. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  50. package/docs/MOBILE-TUTORIAL.md +747 -0
  51. package/docs/PWA-HELP.json +228 -0
  52. package/docs/PWA-README.md +662 -0
  53. package/docs/PWA-TUTORIAL.md +757 -0
  54. package/docs/STEP-HELP.json +481 -0
  55. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  56. package/docs/TESTING-GUIDE.md +528 -0
  57. package/docs/TESTING-HELP.json +182 -0
  58. package/fusion-vs-cyclecad.html +1771 -0
  59. package/nginx.conf +237 -0
  60. package/package.json +1 -1
  61. package/server/Dockerfile.converter +51 -0
  62. package/server/Dockerfile.signaling +28 -0
  63. package/server/billing-server.js +487 -0
  64. package/server/converter-enhanced.py +528 -0
  65. package/server/requirements-converter.txt +29 -0
  66. package/server/signaling-server.js +801 -0
  67. package/tests/docker-tests.sh +389 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Touch Handler for cycleCAD Mobile
3
+ * Comprehensive touch gesture detection and handling
4
+ * Supports tap, double-tap, long-press, swipe, pinch, and rotation
5
+ */
6
+
7
+ class TouchHandler {
8
+ constructor(viewport, options = {}) {
9
+ this.viewport = viewport;
10
+ this.options = {
11
+ tapDelay: 300,
12
+ longPressDelay: 500,
13
+ swipeThreshold: 50,
14
+ pinchThreshold: 20,
15
+ ...options
16
+ };
17
+
18
+ this.state = {
19
+ touchCount: 0,
20
+ isMultiTouch: false,
21
+ startX: 0,
22
+ startY: 0,
23
+ startDistance: 0,
24
+ startRotation: 0,
25
+ tapTimer: null,
26
+ longPressTimer: null,
27
+ isSwiping: false,
28
+ isPinching: false,
29
+ isRotating: false,
30
+ touches: [],
31
+ lastTap: 0
32
+ };
33
+
34
+ this.callbacks = {
35
+ onTap: options.onTap || (() => {}),
36
+ onDoubleTap: options.onDoubleTap || (() => {}),
37
+ onLongPress: options.onLongPress || (() => {}),
38
+ onSwipeLeft: options.onSwipeLeft || (() => {}),
39
+ onSwipeRight: options.onSwipeRight || (() => {}),
40
+ onSwipeUp: options.onSwipeUp || (() => {}),
41
+ onSwipeDown: options.onSwipeDown || (() => {}),
42
+ onPinch: options.onPinch || (() => {}),
43
+ onRotate: options.onRotate || (() => {}),
44
+ onPan: options.onPan || (() => {}),
45
+ onContextMenu: options.onContextMenu || (() => {})
46
+ };
47
+
48
+ this.init();
49
+ }
50
+
51
+ init() {
52
+ // Pointer events for cross-device support
53
+ this.viewport.addEventListener('pointerdown', (e) => this.handlePointerDown(e), false);
54
+ this.viewport.addEventListener('pointermove', (e) => this.handlePointerMove(e), false);
55
+ this.viewport.addEventListener('pointerup', (e) => this.handlePointerUp(e), false);
56
+ this.viewport.addEventListener('pointercancel', (e) => this.handlePointerCancel(e), false);
57
+
58
+ // Fallback for browsers without pointer events
59
+ if (!('PointerEvent' in window)) {
60
+ this.viewport.addEventListener('touchstart', (e) => this.handleTouchStart(e), false);
61
+ this.viewport.addEventListener('touchmove', (e) => this.handleTouchMove(e), false);
62
+ this.viewport.addEventListener('touchend', (e) => this.handleTouchEnd(e), false);
63
+ this.viewport.addEventListener('touchcancel', (e) => this.handleTouchCancel(e), false);
64
+ }
65
+
66
+ // Context menu
67
+ this.viewport.addEventListener('contextmenu', (e) => this.handleContextMenu(e), false);
68
+
69
+ // Gesture events for iOS Safari
70
+ if ('GestureEvent' in window) {
71
+ this.viewport.addEventListener('gesturestart', (e) => this.handleGestureStart(e), false);
72
+ this.viewport.addEventListener('gesturechange', (e) => this.handleGestureChange(e), false);
73
+ this.viewport.addEventListener('gestureend', (e) => this.handleGestureEnd(e), false);
74
+ }
75
+ }
76
+
77
+ handlePointerDown(e) {
78
+ // Ignore non-touch pointers (mouse, pen)
79
+ if (e.pointerType !== 'touch') return;
80
+
81
+ e.preventDefault();
82
+
83
+ this.state.touches.push({
84
+ id: e.pointerId,
85
+ x: e.clientX,
86
+ y: e.clientY,
87
+ startX: e.clientX,
88
+ startY: e.clientY,
89
+ startTime: Date.now()
90
+ });
91
+
92
+ this.state.touchCount = this.state.touches.length;
93
+ this.state.isMultiTouch = this.state.touchCount > 1;
94
+
95
+ if (this.state.touchCount === 1) {
96
+ this.handleSingleTouchStart(e);
97
+ } else if (this.state.touchCount === 2) {
98
+ this.handleMultiTouchStart(e);
99
+ } else if (this.state.touchCount === 3) {
100
+ // Three-finger tap = undo
101
+ this.handleThreeFingerTap(e);
102
+ }
103
+ }
104
+
105
+ handlePointerMove(e) {
106
+ if (e.pointerType !== 'touch') return;
107
+
108
+ const touch = this.state.touches.find(t => t.id === e.pointerId);
109
+ if (!touch) return;
110
+
111
+ touch.x = e.clientX;
112
+ touch.y = e.clientY;
113
+
114
+ if (this.state.touchCount === 1) {
115
+ this.handleSingleTouchMove(e);
116
+ } else if (this.state.touchCount === 2) {
117
+ this.handleMultiTouchMove(e);
118
+ }
119
+ }
120
+
121
+ handlePointerUp(e) {
122
+ if (e.pointerType !== 'touch') return;
123
+
124
+ const touchIndex = this.state.touches.findIndex(t => t.id === e.pointerId);
125
+ if (touchIndex === -1) return;
126
+
127
+ const touch = this.state.touches[touchIndex];
128
+ const duration = Date.now() - touch.startTime;
129
+
130
+ this.state.touches.splice(touchIndex, 1);
131
+ this.state.touchCount = this.state.touches.length;
132
+ this.state.isMultiTouch = this.state.touchCount > 0;
133
+
134
+ if (duration < 500 && this.state.touchCount === 0) {
135
+ this.handleSingleTouchEnd(touch);
136
+ }
137
+
138
+ // Clear multi-touch states
139
+ if (this.state.touchCount === 0) {
140
+ this.state.isSwiping = false;
141
+ this.state.isPinching = false;
142
+ this.state.isRotating = false;
143
+ }
144
+ }
145
+
146
+ handlePointerCancel(e) {
147
+ if (e.pointerType !== 'touch') return;
148
+
149
+ this.state.touches = this.state.touches.filter(t => t.id !== e.pointerId);
150
+ this.state.touchCount = this.state.touches.length;
151
+
152
+ this.clearTimers();
153
+ }
154
+
155
+ // Single touch handling
156
+ handleSingleTouchStart(e) {
157
+ const touch = this.state.touches[0];
158
+
159
+ // Clear existing timers
160
+ clearTimeout(this.state.tapTimer);
161
+ clearTimeout(this.state.longPressTimer);
162
+
163
+ // Long press detection
164
+ this.state.longPressTimer = setTimeout(() => {
165
+ this.callbacks.onLongPress(touch);
166
+ navigator.vibrate?.(50);
167
+ }, this.options.longPressDelay);
168
+
169
+ // Double tap detection
170
+ const now = Date.now();
171
+ if (now - this.state.lastTap < this.options.tapDelay) {
172
+ clearTimeout(this.state.longPressTimer);
173
+ this.callbacks.onDoubleTap(touch);
174
+ navigator.vibrate?.(30);
175
+ this.state.lastTap = 0; // Prevent triple-tap
176
+ } else {
177
+ this.state.lastTap = now;
178
+ }
179
+ }
180
+
181
+ handleSingleTouchMove(e) {
182
+ const touch = this.state.touches[0];
183
+ const dx = touch.x - touch.startX;
184
+ const dy = touch.y - touch.startY;
185
+ const distance = Math.sqrt(dx * dx + dy * dy);
186
+
187
+ // Detect swipe if movement exceeds threshold
188
+ if (distance > this.options.swipeThreshold && !this.state.isSwiping) {
189
+ clearTimeout(this.state.longPressTimer);
190
+ this.state.isSwiping = true;
191
+
192
+ const angle = Math.atan2(dy, dx);
193
+ const angleDeg = angle * (180 / Math.PI);
194
+
195
+ // Determine swipe direction (45 degree tolerance)
196
+ if (angleDeg > -45 && angleDeg < 45) {
197
+ // Right swipe
198
+ this.callbacks.onSwipeRight({ distance, touch });
199
+ } else if (angleDeg > 45 && angleDeg < 135) {
200
+ // Down swipe
201
+ this.callbacks.onSwipeDown({ distance, touch });
202
+ } else if ((angleDeg > 135 && angleDeg < 180) || (angleDeg > -180 && angleDeg < -135)) {
203
+ // Left swipe
204
+ this.callbacks.onSwipeLeft({ distance, touch });
205
+ } else if (angleDeg > -135 && angleDeg < -45) {
206
+ // Up swipe
207
+ this.callbacks.onSwipeUp({ distance, touch });
208
+ }
209
+ }
210
+
211
+ // Pan for continuous movement
212
+ if (this.state.isSwiping) {
213
+ this.callbacks.onPan({ dx, dy, touch });
214
+ }
215
+ }
216
+
217
+ handleSingleTouchEnd(touch) {
218
+ clearTimeout(this.state.tapTimer);
219
+ clearTimeout(this.state.longPressTimer);
220
+
221
+ const duration = Date.now() - touch.startTime;
222
+ const dx = touch.x - touch.startX;
223
+ const dy = touch.y - touch.startY;
224
+ const distance = Math.sqrt(dx * dx + dy * dy);
225
+
226
+ // Tap if short duration and minimal movement
227
+ if (duration < this.options.longPressDelay && distance < this.options.swipeThreshold) {
228
+ this.callbacks.onTap(touch);
229
+ navigator.vibrate?.(10);
230
+ }
231
+
232
+ this.state.isSwiping = false;
233
+ }
234
+
235
+ // Multi-touch handling
236
+ handleMultiTouchStart(e) {
237
+ clearTimeout(this.state.longPressTimer);
238
+
239
+ if (this.state.touchCount === 2) {
240
+ this.state.startDistance = this.getTouchDistance();
241
+ this.state.startRotation = this.getTouchRotation();
242
+ this.state.isPinching = true;
243
+ this.state.isRotating = true;
244
+ }
245
+ }
246
+
247
+ handleMultiTouchMove(e) {
248
+ if (this.state.touchCount !== 2) return;
249
+
250
+ const currentDistance = this.getTouchDistance();
251
+ const currentRotation = this.getTouchRotation();
252
+
253
+ // Pinch detection
254
+ const distanceDelta = currentDistance - this.state.startDistance;
255
+ if (Math.abs(distanceDelta) > this.options.pinchThreshold) {
256
+ this.state.isPinching = true;
257
+ const scale = currentDistance / this.state.startDistance;
258
+ this.callbacks.onPinch({ scale, distance: currentDistance });
259
+ }
260
+
261
+ // Rotation detection
262
+ const rotationDelta = currentRotation - this.state.startRotation;
263
+ if (Math.abs(rotationDelta) > 5) { // 5 degree threshold
264
+ this.state.isRotating = true;
265
+ this.callbacks.onRotate({ rotation: currentRotation, delta: rotationDelta });
266
+ }
267
+
268
+ // Two-finger pan
269
+ if (this.state.touchCount === 2) {
270
+ const touch1 = this.state.touches[0];
271
+ const touch2 = this.state.touches[1];
272
+ const centerX = (touch1.x + touch2.x) / 2;
273
+ const centerY = (touch1.y + touch2.y) / 2;
274
+ const startCenterX = (touch1.startX + touch2.startX) / 2;
275
+ const startCenterY = (touch1.startY + touch2.startY) / 2;
276
+
277
+ const dx = centerX - startCenterX;
278
+ const dy = centerY - startCenterY;
279
+
280
+ this.callbacks.onPan({ dx, dy, touch1, touch2 });
281
+ }
282
+ }
283
+
284
+ handleThreeFingerTap(e) {
285
+ // Three-finger tap triggers undo
286
+ if (window.app && window.app.undo) {
287
+ window.app.undo();
288
+ navigator.vibrate?.([30, 20, 30]);
289
+ }
290
+ }
291
+
292
+ handleContextMenu(e) {
293
+ e.preventDefault();
294
+
295
+ // Get touch position
296
+ const rect = this.viewport.getBoundingClientRect();
297
+ const x = e.clientX - rect.left;
298
+ const y = e.clientY - rect.top;
299
+
300
+ this.callbacks.onContextMenu({ x, y, event: e });
301
+ }
302
+
303
+ handleGestureStart(e) {
304
+ e.preventDefault();
305
+ this.state.startDistance = e.scale;
306
+ this.state.startRotation = e.rotation;
307
+ }
308
+
309
+ handleGestureChange(e) {
310
+ e.preventDefault();
311
+
312
+ if (e.scale !== this.state.startDistance) {
313
+ const scale = e.scale;
314
+ this.callbacks.onPinch({ scale, distance: scale });
315
+ }
316
+
317
+ if (e.rotation !== this.state.startRotation) {
318
+ const delta = e.rotation - this.state.startRotation;
319
+ this.callbacks.onRotate({ rotation: e.rotation, delta });
320
+ }
321
+ }
322
+
323
+ handleGestureEnd(e) {
324
+ e.preventDefault();
325
+ this.state.isPinching = false;
326
+ this.state.isRotating = false;
327
+ }
328
+
329
+ // Fallback touch handlers
330
+ handleTouchStart(e) {
331
+ Array.from(e.touches).forEach(touch => {
332
+ this.state.touches.push({
333
+ id: touch.identifier,
334
+ x: touch.clientX,
335
+ y: touch.clientY,
336
+ startX: touch.clientX,
337
+ startY: touch.clientY,
338
+ startTime: Date.now()
339
+ });
340
+ });
341
+
342
+ this.state.touchCount = this.state.touches.length;
343
+ if (this.state.touchCount === 1) {
344
+ this.handleSingleTouchStart(e);
345
+ } else if (this.state.touchCount === 2) {
346
+ this.handleMultiTouchStart(e);
347
+ }
348
+ }
349
+
350
+ handleTouchMove(e) {
351
+ Array.from(e.touches).forEach(touch => {
352
+ const t = this.state.touches.find(st => st.id === touch.identifier);
353
+ if (t) {
354
+ t.x = touch.clientX;
355
+ t.y = touch.clientY;
356
+ }
357
+ });
358
+
359
+ if (this.state.touchCount === 1) {
360
+ this.handleSingleTouchMove(e);
361
+ } else if (this.state.touchCount === 2) {
362
+ this.handleMultiTouchMove(e);
363
+ }
364
+ }
365
+
366
+ handleTouchEnd(e) {
367
+ Array.from(e.changedTouches).forEach(touch => {
368
+ const index = this.state.touches.findIndex(t => t.id === touch.identifier);
369
+ if (index !== -1) {
370
+ const t = this.state.touches[index];
371
+ this.state.touches.splice(index, 1);
372
+
373
+ if (this.state.touches.length === 0) {
374
+ this.handleSingleTouchEnd(t);
375
+ }
376
+ }
377
+ });
378
+
379
+ this.state.touchCount = this.state.touches.length;
380
+ }
381
+
382
+ handleTouchCancel(e) {
383
+ this.state.touches = [];
384
+ this.state.touchCount = 0;
385
+ this.clearTimers();
386
+ }
387
+
388
+ // Utility methods
389
+ getTouchDistance() {
390
+ if (this.state.touches.length !== 2) return 0;
391
+
392
+ const t1 = this.state.touches[0];
393
+ const t2 = this.state.touches[1];
394
+ const dx = t2.x - t1.x;
395
+ const dy = t2.y - t1.y;
396
+
397
+ return Math.sqrt(dx * dx + dy * dy);
398
+ }
399
+
400
+ getTouchRotation() {
401
+ if (this.state.touches.length !== 2) return 0;
402
+
403
+ const t1 = this.state.touches[0];
404
+ const t2 = this.state.touches[1];
405
+ const dx = t2.x - t1.x;
406
+ const dy = t2.y - t1.y;
407
+
408
+ return Math.atan2(dy, dx) * (180 / Math.PI);
409
+ }
410
+
411
+ clearTimers() {
412
+ clearTimeout(this.state.tapTimer);
413
+ clearTimeout(this.state.longPressTimer);
414
+ }
415
+
416
+ destroy() {
417
+ this.clearTimers();
418
+ this.viewport.removeEventListener('pointerdown', this.handlePointerDown.bind(this));
419
+ this.viewport.removeEventListener('pointermove', this.handlePointerMove.bind(this));
420
+ this.viewport.removeEventListener('pointerup', this.handlePointerUp.bind(this));
421
+ this.viewport.removeEventListener('pointercancel', this.handlePointerCancel.bind(this));
422
+ this.viewport.removeEventListener('contextmenu', this.handleContextMenu.bind(this));
423
+ }
424
+ }
425
+
426
+ // Export for use in modules
427
+ if (typeof module !== 'undefined' && module.exports) {
428
+ module.exports = TouchHandler;
429
+ }
@@ -0,0 +1,211 @@
1
+ {
2
+ "name": "cycleCAD — Browser-Based 3D CAD Modeler",
3
+ "short_name": "cycleCAD",
4
+ "description": "Free, open-source parametric 3D CAD modeler. No install required. Works offline.",
5
+ "start_url": "/app/?utm_source=pwa",
6
+ "scope": "/app/",
7
+ "display": "standalone",
8
+ "orientation": "landscape",
9
+ "background_color": "#1e1e1e",
10
+ "theme_color": "#0284C7",
11
+ "categories": ["productivity", "design", "utilities"],
12
+ "screenshots": [
13
+ {
14
+ "src": "/app/icons/screenshot-540.png",
15
+ "sizes": "540x720",
16
+ "type": "image/png",
17
+ "form_factor": "narrow",
18
+ "platform": "narrow"
19
+ },
20
+ {
21
+ "src": "/app/icons/screenshot-1280.png",
22
+ "sizes": "1280x720",
23
+ "type": "image/png",
24
+ "form_factor": "wide",
25
+ "platform": "wide"
26
+ }
27
+ ],
28
+ "icons": [
29
+ {
30
+ "src": "/app/icons/icon-72.png",
31
+ "sizes": "72x72",
32
+ "type": "image/png",
33
+ "purpose": "any"
34
+ },
35
+ {
36
+ "src": "/app/icons/icon-96.png",
37
+ "sizes": "96x96",
38
+ "type": "image/png",
39
+ "purpose": "any"
40
+ },
41
+ {
42
+ "src": "/app/icons/icon-128.png",
43
+ "sizes": "128x128",
44
+ "type": "image/png",
45
+ "purpose": "any"
46
+ },
47
+ {
48
+ "src": "/app/icons/icon-144.png",
49
+ "sizes": "144x144",
50
+ "type": "image/png",
51
+ "purpose": "any"
52
+ },
53
+ {
54
+ "src": "/app/icons/icon-152.png",
55
+ "sizes": "152x152",
56
+ "type": "image/png",
57
+ "purpose": "any"
58
+ },
59
+ {
60
+ "src": "/app/icons/icon-192.png",
61
+ "sizes": "192x192",
62
+ "type": "image/png",
63
+ "purpose": "any"
64
+ },
65
+ {
66
+ "src": "/app/icons/icon-384.png",
67
+ "sizes": "384x384",
68
+ "type": "image/png",
69
+ "purpose": "any"
70
+ },
71
+ {
72
+ "src": "/app/icons/icon-512.png",
73
+ "sizes": "512x512",
74
+ "type": "image/png",
75
+ "purpose": "any"
76
+ },
77
+ {
78
+ "src": "/app/icons/icon-192-maskable.png",
79
+ "sizes": "192x192",
80
+ "type": "image/png",
81
+ "purpose": "maskable"
82
+ },
83
+ {
84
+ "src": "/app/icons/icon-512-maskable.png",
85
+ "sizes": "512x512",
86
+ "type": "image/png",
87
+ "purpose": "maskable"
88
+ }
89
+ ],
90
+ "shortcuts": [
91
+ {
92
+ "name": "New Project",
93
+ "short_name": "New",
94
+ "description": "Start a new CAD project",
95
+ "url": "/app/?action=new",
96
+ "icons": [
97
+ {
98
+ "src": "/app/icons/shortcut-new-192.png",
99
+ "sizes": "192x192",
100
+ "type": "image/png"
101
+ }
102
+ ]
103
+ },
104
+ {
105
+ "name": "Open Project",
106
+ "short_name": "Open",
107
+ "description": "Open an existing project",
108
+ "url": "/app/?action=open",
109
+ "icons": [
110
+ {
111
+ "src": "/app/icons/shortcut-open-192.png",
112
+ "sizes": "192x192",
113
+ "type": "image/png"
114
+ }
115
+ ]
116
+ },
117
+ {
118
+ "name": "Import STEP",
119
+ "short_name": "Import",
120
+ "description": "Import a STEP file",
121
+ "url": "/app/?action=import",
122
+ "icons": [
123
+ {
124
+ "src": "/app/icons/shortcut-import-192.png",
125
+ "sizes": "192x192",
126
+ "type": "image/png"
127
+ }
128
+ ]
129
+ }
130
+ ],
131
+ "share_target": {
132
+ "action": "/app/",
133
+ "method": "POST",
134
+ "enctype": "multipart/form-data",
135
+ "params": {
136
+ "title": "title",
137
+ "text": "text",
138
+ "url": "url",
139
+ "files": [
140
+ {
141
+ "name": "file",
142
+ "accept": [
143
+ "model/step",
144
+ "model/x.step",
145
+ "model/stp",
146
+ "model/x.stp",
147
+ "model/stl",
148
+ "model/x.stl",
149
+ "model/obj",
150
+ "model/x.obj",
151
+ "model/gltf+json",
152
+ "model/gltf-binary",
153
+ "application/json"
154
+ ]
155
+ }
156
+ ]
157
+ }
158
+ },
159
+ "file_handlers": [
160
+ {
161
+ "action": "/app/",
162
+ "accept": {
163
+ "model/step": [".step", ".STEP"],
164
+ "model/x.step": [".stp", ".STP"]
165
+ },
166
+ "icons": [
167
+ {
168
+ "src": "/app/icons/icon-192.png",
169
+ "sizes": "192x192"
170
+ }
171
+ ]
172
+ },
173
+ {
174
+ "action": "/app/",
175
+ "accept": {
176
+ "model/stl": [".stl", ".STL"],
177
+ "model/obj": [".obj", ".OBJ"],
178
+ "model/gltf-binary": [".glb", ".GLB"]
179
+ },
180
+ "icons": [
181
+ {
182
+ "src": "/app/icons/icon-192.png",
183
+ "sizes": "192x192"
184
+ }
185
+ ]
186
+ }
187
+ ],
188
+ "launch_handler": {
189
+ "client_mode": ["navigate-new", "auto"]
190
+ },
191
+ "protocol_handlers": [
192
+ {
193
+ "protocol": "web+cyclecad",
194
+ "url": "/app/?project=%s"
195
+ }
196
+ ],
197
+ "prefer_related_applications": false,
198
+ "related_applications": [],
199
+ "screenshots_details": [
200
+ {
201
+ "form_factor": "narrow",
202
+ "src": "/app/icons/screenshot-540.png",
203
+ "type": "image/png"
204
+ },
205
+ {
206
+ "form_factor": "wide",
207
+ "src": "/app/icons/screenshot-1280.png",
208
+ "type": "image/png"
209
+ }
210
+ ]
211
+ }