@guinetik/gcanvas 1.0.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.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bundle4llm
|
|
5
|
+
* -------------
|
|
6
|
+
* Create a comprehensive, single-file, import/export-free JavaScript bundle
|
|
7
|
+
* ideal for feeding into LLMs without worrying about dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Recursively resolves and includes all exported modules
|
|
11
|
+
* - Flattens directory structure
|
|
12
|
+
* - Orders files by export order in index files
|
|
13
|
+
* - Sanitizes code based on selected preset (defaults to ES2020)
|
|
14
|
+
* - Skips index.js files from output (used only for ordering)
|
|
15
|
+
* - Ensures comprehensive module inclusion
|
|
16
|
+
* - Supports verbose logging with --verbose or -v
|
|
17
|
+
* - Optional comment stripping with --strip-comments
|
|
18
|
+
* - Accurate token estimation for various LLM models
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* bundle4llm --src ./src --out ./dist --file bundle.js --preset es2020 --strip-comments --model claude -v
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { argv } from 'process';
|
|
27
|
+
|
|
28
|
+
const args = Object.fromEntries(
|
|
29
|
+
argv.slice(2).map(arg => {
|
|
30
|
+
const [key, val] = arg.replace(/^--?/, '').split('=');
|
|
31
|
+
return [key, val ?? true];
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const srcDir = path.resolve(args.src || './src');
|
|
36
|
+
const outDir = path.resolve(args.out || './dist');
|
|
37
|
+
const outFile = args.file || 'llm-bundle.js';
|
|
38
|
+
const verbose = args.v || args.verbose;
|
|
39
|
+
const stripComments = args['strip-comments'] || false;
|
|
40
|
+
const preset = args.preset || 'es2020';
|
|
41
|
+
const model = args.model || 'claude'; // Default to Claude model
|
|
42
|
+
|
|
43
|
+
// Deep file collection with recursive export resolution
|
|
44
|
+
function collectModules(indexPath, fileMap = {}, visited = new Set(), depth = 0) {
|
|
45
|
+
// Prevent infinite recursion
|
|
46
|
+
if (depth > 10) return fileMap;
|
|
47
|
+
|
|
48
|
+
// Prevent revisiting the same index file
|
|
49
|
+
if (visited.has(indexPath)) return fileMap;
|
|
50
|
+
visited.add(indexPath);
|
|
51
|
+
|
|
52
|
+
// Read index file contents
|
|
53
|
+
if (!fs.existsSync(indexPath)) return fileMap;
|
|
54
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
55
|
+
|
|
56
|
+
// Find all export declarations
|
|
57
|
+
const exportMatches = [
|
|
58
|
+
...content.matchAll(/export\s+(?:\*|{[^}]*})\s*from\s+['"](.+)['"]/g),
|
|
59
|
+
...content.matchAll(/export\s+{[^}]*}\s*from\s+['"](.+)['"]/g)
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Resolve and process each exported module
|
|
63
|
+
for (const match of exportMatches) {
|
|
64
|
+
const rawPath = match[1];
|
|
65
|
+
const modulePath = rawPath.startsWith('.')
|
|
66
|
+
? path.resolve(path.dirname(indexPath), rawPath)
|
|
67
|
+
: path.resolve(srcDir, rawPath);
|
|
68
|
+
|
|
69
|
+
// Normalize path
|
|
70
|
+
const normalizedPath = modulePath.replace(/\.js$/, '');
|
|
71
|
+
const jsPath = normalizedPath + '.js';
|
|
72
|
+
|
|
73
|
+
// If it's a directory, look for its index file
|
|
74
|
+
const indexInDir = path.join(normalizedPath, 'index.js');
|
|
75
|
+
|
|
76
|
+
if (fs.existsSync(jsPath) && !fileMap[jsPath]) {
|
|
77
|
+
// Add the module file
|
|
78
|
+
fileMap[jsPath] = jsPath;
|
|
79
|
+
|
|
80
|
+
// If it's a directory with an index, recursively collect its exports
|
|
81
|
+
if (fs.existsSync(indexInDir)) {
|
|
82
|
+
collectModules(indexInDir, fileMap, visited, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
} else if (fs.existsSync(indexInDir)) {
|
|
85
|
+
// Recursively collect exports from subdirectory index
|
|
86
|
+
collectModules(indexInDir, fileMap, visited, depth + 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return fileMap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Order modules based on index file export order
|
|
94
|
+
function orderModules(srcDir) {
|
|
95
|
+
const indexPath = path.join(srcDir, 'index.js');
|
|
96
|
+
const fileMap = collectModules(indexPath);
|
|
97
|
+
|
|
98
|
+
// Convert to array and sort
|
|
99
|
+
return Object.values(fileMap)
|
|
100
|
+
.filter(filePath => !filePath.endsWith('index.js'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Sanitization presets
|
|
104
|
+
const sanitizationPresets = {
|
|
105
|
+
// ES2020 sanitization - strips imports and optionally comments
|
|
106
|
+
es2020: (code, options = {}) => {
|
|
107
|
+
let result = code;
|
|
108
|
+
|
|
109
|
+
// Strip import statements
|
|
110
|
+
result = result
|
|
111
|
+
.split('\n')
|
|
112
|
+
.filter(line => !/^\s*import\b/.test(line))
|
|
113
|
+
.join('\n');
|
|
114
|
+
|
|
115
|
+
// Optionally strip comments
|
|
116
|
+
if (options.stripComments) {
|
|
117
|
+
// Strip block comments
|
|
118
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
119
|
+
// Strip line comments
|
|
120
|
+
result = result.replace(/\/\/.*$/gm, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result.trim();
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// ES2015 sanitization - more conservative approach
|
|
127
|
+
es2015: (code, options = {}) => {
|
|
128
|
+
let result = code;
|
|
129
|
+
|
|
130
|
+
// Strip import statements
|
|
131
|
+
result = result
|
|
132
|
+
.split('\n')
|
|
133
|
+
.filter(line => !/^\s*import\b/.test(line))
|
|
134
|
+
.join('\n');
|
|
135
|
+
|
|
136
|
+
// Optionally strip comments
|
|
137
|
+
if (options.stripComments) {
|
|
138
|
+
// Strip block comments
|
|
139
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
140
|
+
// Strip line comments
|
|
141
|
+
result = result.replace(/\/\/.*$/gm, '');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result.trim();
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Minimal sanitization - only strips imports
|
|
148
|
+
minimal: (code, options = {}) => {
|
|
149
|
+
let result = code
|
|
150
|
+
.split('\n')
|
|
151
|
+
.filter(line => !/^\s*import\b/.test(line))
|
|
152
|
+
.join('\n');
|
|
153
|
+
|
|
154
|
+
return result.trim();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Generic sanitize function that uses the selected preset
|
|
159
|
+
function sanitizeCode(code, presetName, options = {}) {
|
|
160
|
+
const sanitizer = sanitizationPresets[presetName] || sanitizationPresets.es2020;
|
|
161
|
+
return sanitizer(code, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Improved token estimation function supporting multiple LLM models
|
|
166
|
+
*
|
|
167
|
+
* @param {string} text - The text to estimate tokens for
|
|
168
|
+
* @param {string} model - Model identifier ('claude', 'gpt3', 'gpt4', etc.)
|
|
169
|
+
* @return {number} Estimated token count
|
|
170
|
+
*/
|
|
171
|
+
function estimateTokens(text, model = 'claude') {
|
|
172
|
+
// Basic character counting
|
|
173
|
+
const charCount = text.length;
|
|
174
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
175
|
+
|
|
176
|
+
// Model-specific token ratio estimations
|
|
177
|
+
const modelRatios = {
|
|
178
|
+
// Anthropic Claude models
|
|
179
|
+
'claude': {
|
|
180
|
+
charRatio: 3.5, // ~3.5 chars per token for Claude
|
|
181
|
+
wordRatio: 0.75, // ~0.75 words per token for Claude
|
|
182
|
+
},
|
|
183
|
+
// OpenAI models
|
|
184
|
+
'gpt3': {
|
|
185
|
+
charRatio: 4.0, // ~4 chars per token for GPT-3
|
|
186
|
+
wordRatio: 0.75, // ~0.75 words per token for GPT-3
|
|
187
|
+
},
|
|
188
|
+
'gpt4': {
|
|
189
|
+
charRatio: 3.8, // ~3.8 chars per token for GPT-4
|
|
190
|
+
wordRatio: 0.7, // ~0.7 words per token for GPT-4
|
|
191
|
+
},
|
|
192
|
+
// Default fallback
|
|
193
|
+
'default': {
|
|
194
|
+
charRatio: 4.0,
|
|
195
|
+
wordRatio: 0.75,
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Get the appropriate ratio for the model
|
|
200
|
+
const ratio = modelRatios[model] || modelRatios.default;
|
|
201
|
+
|
|
202
|
+
// Estimate based on character count and word count, using an average of both methods
|
|
203
|
+
const charBasedEstimate = Math.ceil(charCount / ratio.charRatio);
|
|
204
|
+
const wordBasedEstimate = Math.ceil(wordCount / ratio.wordRatio);
|
|
205
|
+
|
|
206
|
+
// Language features that tend to increase token count
|
|
207
|
+
const codeFeatures = {
|
|
208
|
+
symbols: (text.match(/[{}[\]()<>:;,."'`~!@#$%^&*+=|\\/?-]/g) || []).length,
|
|
209
|
+
indentation: (text.match(/^ +/gm) || []).length,
|
|
210
|
+
camelCase: (text.match(/[a-z][A-Z]/g) || []).length,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Adjustment for code-specific features (symbols, indentation, camelCase)
|
|
214
|
+
const codeAdjustment = codeFeatures.symbols * 0.1 +
|
|
215
|
+
codeFeatures.indentation * 0.05 +
|
|
216
|
+
codeFeatures.camelCase * 0.2;
|
|
217
|
+
|
|
218
|
+
// Calculate weighted average with more weight on char-based for code
|
|
219
|
+
const weightedEstimate = (charBasedEstimate * 0.7) + (wordBasedEstimate * 0.3);
|
|
220
|
+
|
|
221
|
+
// Add code-specific adjustment
|
|
222
|
+
const finalEstimate = Math.ceil(weightedEstimate + codeAdjustment);
|
|
223
|
+
|
|
224
|
+
return finalEstimate;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Main bundling function
|
|
228
|
+
function bundleLLMBuild(srcDir, outDir, outFile, options = {}) {
|
|
229
|
+
const orderedFiles = orderModules(srcDir);
|
|
230
|
+
const { verbose, stripComments, preset, model } = options;
|
|
231
|
+
|
|
232
|
+
let output = '/**\n * bundle4llm Output\n';
|
|
233
|
+
output += ` * Generated: ${new Date().toISOString()}\n`;
|
|
234
|
+
output += ` * Source: ${srcDir}\n`;
|
|
235
|
+
output += ` * Preset: ${preset}\n`;
|
|
236
|
+
if (stripComments) output += ` * Comments: Stripped\n`;
|
|
237
|
+
output += ` */\n\n`;
|
|
238
|
+
|
|
239
|
+
let currentDir = '';
|
|
240
|
+
for (const file of orderedFiles) {
|
|
241
|
+
const fileDir = path.dirname(file).replace(srcDir, '');
|
|
242
|
+
if (fileDir !== currentDir) {
|
|
243
|
+
currentDir = fileDir;
|
|
244
|
+
output += `\n// =========================================\n`;
|
|
245
|
+
output += `// DIRECTORY: ${currentDir || '/'}\n`;
|
|
246
|
+
output += `// =========================================\n\n`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let contents = fs.readFileSync(file, 'utf-8');
|
|
250
|
+
contents = sanitizeCode(contents, preset, { stripComments });
|
|
251
|
+
|
|
252
|
+
if (verbose) console.log('📦 Including:', file);
|
|
253
|
+
output += contents + '\n\n';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Ensure output directory exists
|
|
257
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
258
|
+
|
|
259
|
+
// Write the bundled file
|
|
260
|
+
const fullOutputPath = path.join(outDir, outFile);
|
|
261
|
+
fs.writeFileSync(fullOutputPath, output);
|
|
262
|
+
|
|
263
|
+
const totalTokens = estimateTokens(output, model).toLocaleString();
|
|
264
|
+
console.log(`✅ Created LLM build → ${fullOutputPath}`);
|
|
265
|
+
console.log(`📊 Total modules included: ${orderedFiles.length}`);
|
|
266
|
+
console.log(`🔧 Preset: ${preset}${stripComments ? ', comments stripped' : ''}`);
|
|
267
|
+
console.log(`🔍 Estimated tokens (${model}): ${totalTokens}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Execute the bundle
|
|
271
|
+
bundleLLMBuild(srcDir, outDir, outFile, {
|
|
272
|
+
verbose,
|
|
273
|
+
stripComments,
|
|
274
|
+
preset,
|
|
275
|
+
model
|
|
276
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const targetDir = process.argv[2] || "./src"; // default to ./src
|
|
7
|
+
|
|
8
|
+
function isLineCommented(line) {
|
|
9
|
+
return /^\s*\/\/.*console\.log/.test(line);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isInsideBlockComment(lines, index) {
|
|
13
|
+
for (let i = index; i >= 0; i--) {
|
|
14
|
+
if (lines[i].includes("*/")) break;
|
|
15
|
+
if (lines[i].includes("/*")) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function processFile(filePath) {
|
|
21
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
22
|
+
const lines = content.split("\n");
|
|
23
|
+
const modified = lines.map((line, i) => {
|
|
24
|
+
if (
|
|
25
|
+
line.includes("console.log") &&
|
|
26
|
+
!isLineCommented(line) &&
|
|
27
|
+
!isInsideBlockComment(lines, i)
|
|
28
|
+
) {
|
|
29
|
+
return line.replace(/(.*?)(console\.log.*)/, "$1// $2");
|
|
30
|
+
}
|
|
31
|
+
return line;
|
|
32
|
+
});
|
|
33
|
+
fs.writeFileSync(filePath, modified.join("\n"), "utf8");
|
|
34
|
+
console.log(`✔ Commented console.log in: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function walkDir(dir) {
|
|
38
|
+
fs.readdirSync(dir).forEach((file) => {
|
|
39
|
+
const fullPath = path.join(dir, file);
|
|
40
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
41
|
+
walkDir(fullPath);
|
|
42
|
+
} else if (/\.(js|ts|jsx|tsx)$/.test(fullPath)) {
|
|
43
|
+
processFile(fullPath);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
walkDir(targetDir);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { Collision } from "./collision.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CollisionSystem - Manages collision groups and detection
|
|
5
|
+
*
|
|
6
|
+
* Provides an organized way to manage multiple groups of collidable objects
|
|
7
|
+
* and efficiently check collisions between them.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const collisions = new CollisionSystem();
|
|
11
|
+
*
|
|
12
|
+
* // Register groups
|
|
13
|
+
* collisions.createGroup('players');
|
|
14
|
+
* collisions.createGroup('enemies');
|
|
15
|
+
* collisions.createGroup('bullets');
|
|
16
|
+
*
|
|
17
|
+
* // Add objects to groups
|
|
18
|
+
* collisions.add('players', player);
|
|
19
|
+
* collisions.add('enemies', alien);
|
|
20
|
+
* collisions.add('bullets', bullet);
|
|
21
|
+
*
|
|
22
|
+
* // Set up collision handlers
|
|
23
|
+
* collisions.onCollision('bullets', 'enemies', (bullet, enemy) => {
|
|
24
|
+
* bullet.destroy();
|
|
25
|
+
* enemy.takeDamage();
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Check collisions each frame
|
|
29
|
+
* collisions.update();
|
|
30
|
+
*/
|
|
31
|
+
export class CollisionSystem {
|
|
32
|
+
constructor() {
|
|
33
|
+
/** @type {Map<string, Set<Object>>} */
|
|
34
|
+
this.groups = new Map();
|
|
35
|
+
|
|
36
|
+
/** @type {Array<Object>} Collision pair definitions */
|
|
37
|
+
this.pairs = [];
|
|
38
|
+
|
|
39
|
+
/** @type {boolean} Whether to use quadtree optimization */
|
|
40
|
+
this.useQuadtree = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new collision group
|
|
45
|
+
*
|
|
46
|
+
* @param {string} name - Unique group name
|
|
47
|
+
* @returns {CollisionSystem} this for chaining
|
|
48
|
+
*/
|
|
49
|
+
createGroup(name) {
|
|
50
|
+
if (!this.groups.has(name)) {
|
|
51
|
+
this.groups.set(name, new Set());
|
|
52
|
+
}
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add an object to a collision group
|
|
58
|
+
* Object must have either getBounds() method or bounds property
|
|
59
|
+
*
|
|
60
|
+
* @param {string} groupName - Group to add to
|
|
61
|
+
* @param {Object} obj - Object with getBounds() or bounds property
|
|
62
|
+
* @returns {CollisionSystem} this for chaining
|
|
63
|
+
*/
|
|
64
|
+
add(groupName, obj) {
|
|
65
|
+
const group = this.groups.get(groupName);
|
|
66
|
+
if (!group) {
|
|
67
|
+
throw new Error(`Collision group '${groupName}' does not exist. Call createGroup('${groupName}') first.`);
|
|
68
|
+
}
|
|
69
|
+
group.add(obj);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove an object from a collision group
|
|
75
|
+
*
|
|
76
|
+
* @param {string} groupName - Group to remove from
|
|
77
|
+
* @param {Object} obj - Object to remove
|
|
78
|
+
* @returns {boolean} True if object was in the group
|
|
79
|
+
*/
|
|
80
|
+
remove(groupName, obj) {
|
|
81
|
+
const group = this.groups.get(groupName);
|
|
82
|
+
if (!group) return false;
|
|
83
|
+
return group.delete(obj);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove an object from all groups
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} obj - Object to remove
|
|
90
|
+
*/
|
|
91
|
+
removeFromAll(obj) {
|
|
92
|
+
for (const group of this.groups.values()) {
|
|
93
|
+
group.delete(obj);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Clear all objects from a group
|
|
99
|
+
*
|
|
100
|
+
* @param {string} groupName - Group to clear
|
|
101
|
+
*/
|
|
102
|
+
clearGroup(groupName) {
|
|
103
|
+
const group = this.groups.get(groupName);
|
|
104
|
+
if (group) {
|
|
105
|
+
group.clear();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all groups
|
|
111
|
+
*/
|
|
112
|
+
clearAll() {
|
|
113
|
+
for (const group of this.groups.values()) {
|
|
114
|
+
group.clear();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all objects in a group
|
|
120
|
+
*
|
|
121
|
+
* @param {string} groupName - Group name
|
|
122
|
+
* @returns {Array<Object>} Array of objects in the group
|
|
123
|
+
*/
|
|
124
|
+
getGroup(groupName) {
|
|
125
|
+
const group = this.groups.get(groupName);
|
|
126
|
+
return group ? Array.from(group) : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a collision callback between two groups
|
|
131
|
+
*
|
|
132
|
+
* @param {string} groupA - First group name
|
|
133
|
+
* @param {string} groupB - Second group name
|
|
134
|
+
* @param {Function} callback - Called with (objA, objB) on collision
|
|
135
|
+
* @param {Object} [options={}] - Additional options
|
|
136
|
+
* @param {boolean} [options.once=false] - If true, only trigger once per pair per frame
|
|
137
|
+
* @returns {CollisionSystem} this for chaining
|
|
138
|
+
*/
|
|
139
|
+
onCollision(groupA, groupB, callback, options = {}) {
|
|
140
|
+
this.pairs.push({
|
|
141
|
+
groupA,
|
|
142
|
+
groupB,
|
|
143
|
+
callback,
|
|
144
|
+
once: options.once ?? false,
|
|
145
|
+
});
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove all collision callbacks for a pair of groups
|
|
151
|
+
*
|
|
152
|
+
* @param {string} groupA - First group name
|
|
153
|
+
* @param {string} groupB - Second group name
|
|
154
|
+
*/
|
|
155
|
+
offCollision(groupA, groupB) {
|
|
156
|
+
this.pairs = this.pairs.filter(
|
|
157
|
+
(pair) => !(pair.groupA === groupA && pair.groupB === groupB)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check and handle all registered collision pairs
|
|
163
|
+
* Call this each frame in your update loop
|
|
164
|
+
*/
|
|
165
|
+
update() {
|
|
166
|
+
for (const pair of this.pairs) {
|
|
167
|
+
this._checkPair(pair);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check collisions between two specific groups (without callbacks)
|
|
173
|
+
* Returns array of colliding pairs
|
|
174
|
+
*
|
|
175
|
+
* @param {string} groupA - First group name
|
|
176
|
+
* @param {string} groupB - Second group name
|
|
177
|
+
* @returns {Array<Array>} Array of [objA, objB] colliding pairs
|
|
178
|
+
*/
|
|
179
|
+
check(groupA, groupB) {
|
|
180
|
+
const setA = this.groups.get(groupA);
|
|
181
|
+
const setB = this.groups.get(groupB);
|
|
182
|
+
if (!setA || !setB) return [];
|
|
183
|
+
|
|
184
|
+
const collisions = [];
|
|
185
|
+
|
|
186
|
+
for (const objA of setA) {
|
|
187
|
+
if (!this._isActive(objA)) continue;
|
|
188
|
+
const boundsA = this._getBounds(objA);
|
|
189
|
+
if (!boundsA) continue;
|
|
190
|
+
|
|
191
|
+
for (const objB of setB) {
|
|
192
|
+
if (objA === objB) continue;
|
|
193
|
+
if (!this._isActive(objB)) continue;
|
|
194
|
+
|
|
195
|
+
const boundsB = this._getBounds(objB);
|
|
196
|
+
if (!boundsB) continue;
|
|
197
|
+
|
|
198
|
+
if (Collision.rectRect(boundsA, boundsB)) {
|
|
199
|
+
collisions.push([objA, objB]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return collisions;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if an object collides with any object in a group
|
|
209
|
+
*
|
|
210
|
+
* @param {Object} obj - Object to test
|
|
211
|
+
* @param {string} groupName - Group to test against
|
|
212
|
+
* @returns {Object|null} First colliding object, or null
|
|
213
|
+
*/
|
|
214
|
+
checkAgainstGroup(obj, groupName) {
|
|
215
|
+
const group = this.groups.get(groupName);
|
|
216
|
+
if (!group) return null;
|
|
217
|
+
|
|
218
|
+
const boundsA = this._getBounds(obj);
|
|
219
|
+
if (!boundsA) return null;
|
|
220
|
+
|
|
221
|
+
for (const other of group) {
|
|
222
|
+
if (obj === other) continue;
|
|
223
|
+
if (!this._isActive(other)) continue;
|
|
224
|
+
|
|
225
|
+
const boundsB = this._getBounds(other);
|
|
226
|
+
if (!boundsB) continue;
|
|
227
|
+
|
|
228
|
+
if (Collision.rectRect(boundsA, boundsB)) {
|
|
229
|
+
return other;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if an object collides with any object in a group
|
|
238
|
+
* Returns all colliding objects
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} obj - Object to test
|
|
241
|
+
* @param {string} groupName - Group to test against
|
|
242
|
+
* @returns {Array<Object>} All colliding objects
|
|
243
|
+
*/
|
|
244
|
+
checkAllAgainstGroup(obj, groupName) {
|
|
245
|
+
const group = this.groups.get(groupName);
|
|
246
|
+
if (!group) return [];
|
|
247
|
+
|
|
248
|
+
const boundsA = this._getBounds(obj);
|
|
249
|
+
if (!boundsA) return [];
|
|
250
|
+
|
|
251
|
+
const collisions = [];
|
|
252
|
+
|
|
253
|
+
for (const other of group) {
|
|
254
|
+
if (obj === other) continue;
|
|
255
|
+
if (!this._isActive(other)) continue;
|
|
256
|
+
|
|
257
|
+
const boundsB = this._getBounds(other);
|
|
258
|
+
if (!boundsB) continue;
|
|
259
|
+
|
|
260
|
+
if (Collision.rectRect(boundsA, boundsB)) {
|
|
261
|
+
collisions.push(other);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return collisions;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_checkPair(pair) {
|
|
272
|
+
const setA = this.groups.get(pair.groupA);
|
|
273
|
+
const setB = this.groups.get(pair.groupB);
|
|
274
|
+
if (!setA || !setB) return;
|
|
275
|
+
|
|
276
|
+
for (const objA of setA) {
|
|
277
|
+
if (!this._isActive(objA)) continue;
|
|
278
|
+
const boundsA = this._getBounds(objA);
|
|
279
|
+
if (!boundsA) continue;
|
|
280
|
+
|
|
281
|
+
for (const objB of setB) {
|
|
282
|
+
if (objA === objB) continue;
|
|
283
|
+
if (!this._isActive(objB)) continue;
|
|
284
|
+
|
|
285
|
+
const boundsB = this._getBounds(objB);
|
|
286
|
+
if (!boundsB) continue;
|
|
287
|
+
|
|
288
|
+
if (Collision.rectRect(boundsA, boundsB)) {
|
|
289
|
+
pair.callback(objA, objB);
|
|
290
|
+
|
|
291
|
+
// If 'once' option, skip remaining checks for this objA
|
|
292
|
+
if (pair.once) break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get bounds from an object (supports getBounds() method or bounds property)
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
_getBounds(obj) {
|
|
303
|
+
if (typeof obj.getBounds === "function") {
|
|
304
|
+
return obj.getBounds();
|
|
305
|
+
}
|
|
306
|
+
if (obj.bounds) {
|
|
307
|
+
return obj.bounds;
|
|
308
|
+
}
|
|
309
|
+
// Fallback: try to construct bounds from x, y, width, height
|
|
310
|
+
if (obj.x !== undefined && obj.y !== undefined) {
|
|
311
|
+
return {
|
|
312
|
+
x: obj.x - (obj.width || 0) / 2,
|
|
313
|
+
y: obj.y - (obj.height || 0) / 2,
|
|
314
|
+
width: obj.width || 0,
|
|
315
|
+
height: obj.height || 0,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Check if an object is still active (for filtering dead objects)
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
_isActive(obj) {
|
|
326
|
+
// Support common patterns for "active" objects
|
|
327
|
+
if (obj.active === false) return false;
|
|
328
|
+
if (obj.destroyed === true) return false;
|
|
329
|
+
if (obj.alive === false) return false;
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|